x64用主要アセンブラの構文差異クイズ
初めに
これはx64用JITアセンブラXbyakや静的アセンブラs_xbyakを開発するときに、各種アセンブラの差異についてはまったり調べたりしたことをまとめるにあたり、せっかくなのでクイズ形式にしたものです。
中級以降は主にAVX-512に関するかなりマニアックで瑣末な知識です。何を聞かれてるのか分からなくても殆どの場合、何の問題もありません。
前置き
ここで扱うアセンブラは
- GAS (GNU Assembler) 2.38
- Netwide Assembler (NASM) 2.16
- Microsoft Macro Assembler 14.35.32217.1
です。MASMはWindows専用、GASは主にLinuxで扱われますが、Windowsでもmingwなどで使えます。NASMはwin64, elf64, macho64 (Intel macOS)用のオブジェクトコードを生成できます。
問題は概ねIntelのSDMかXEDの表記にしたがいます。GAS/NASM/MASMでどう書くかを答えてください。自分が慣れてるアセンブラのみの回答でもOKです。
s_xbyakでは、アドレス参照についてXbyakのptr[...]がptr(...)になる以外は概ね同じです。またアセンブラの中で{}を使う部分はC++やPythonでは扱えないので|を使い、特殊なシンボルの先頭にはT_をつけています。
問題 : 以下の文章に相当するASMを書いてください。
初級編
Q1
movでraxにrbxの値をコピーする。答えA1
Q2
raxの値に0x123をaddする。答えA2
Q3
raxで指定されたアドレスの64bit整数をincする。答えA3
Q4
ラベルbufという名前がつけられた8byteの整数データが定義されているとき、それをRIP相対でraxに読み込む。答えA4
中級編
Q5
アドレスraxにあるfloat値をブロードキャストしてzmm1にvaddpsしてzmm0に代入する。答えA5
Q6
zmm1のfloat値16個を負の無限大方向(rd-sae)に丸めてzmm0にvcvtps2dqする。答えA6
Q7
zmm1のfloat値16個を例外抑制しつつ整数値に銀行丸め(.5は偶数に丸める)でzmm0にvrndscalepsする。答えA7
上級編
Q8
アドレスraxにあるdouble値2個をxmm0にcvtpd2dqする。答えA8
Q9
アドレスraxにあるdouble値をm256としてブロードキャストしてxmm0にcvtpd2dqする。答えA9
Q10
第2世代Xeon SPで使えるようになったAVX-512 VNNI命令vpdpbusdをAVX-512をサポートしていないAlder Lakeで使えるようにAVX-VNNIとしてエンコードする。答えA10
回答
A1
- Xbyak :
mov(rax, rbx) - MASM :
mov rax, rbx - NASM :
mov rax, rbx - GAS :
mov %rbx, %rax
GASは引数の順序がIntelのマニュアルと逆順になり、レジスタには%をつける必要があります。問題1
A2
- Xbyak :
add(rax, 0x123) - MASM :
add rax, 123h - NASM :
add rax, 0x123 - GAS :
add $0x123, %rax
MASMは0x...という16進数表記はエラーで、...hという表記でなければなりません。
GASは数値の先頭に$をつける必要があります。問題2
A3
- Xbyak :
inc(qword[rax]) - MASM :
inc qword ptr[rax] - NASM :
inc qword[rax] - GAS :
incq (%rax)
サイズを明示するときはGAS以外はメモリ側につけるのですがGASはニーモニックの後ろに添え字をつけます。問題3
A4
- s_xbyak :
mov(rax, ptr(rip+'buf')) - MASM :
mov rax, qword ptr buf - NASM :
mov rax, [rel buf] - GAS :
mov buf(%rip), %rax
RIP相対はx64で現在の場所から32bit相対でアドレスを指定する方法です。共有ライブラリではPIC(位置独立コード)のためにRIP相対を使うのが望ましいです。
WindowsでRIP相対を使わないと/LARGEADDRESSAWARE:NOが必要になったり、macOSではエラーになったりします。
MASMのmovは[]を無視するので注意してください。
| 機能 | NASM | MASM |
|---|---|---|
| bufの64bit絶対アドレスをraxに入れる | mov rax, buf |
mov rax, buf or mov rax, [buf]
|
| bufの下位32bitアドレスにある値をraxに入れる | mov rax, [buf] |
無い? |
| bufへのRIP相対アドレスにある値をraxに入れる | mov rax, [rel buf] |
mov rax, qword ptr [buf] or mov rax, qword ptr buf
|
A5
- Xbyak :
vaddps(zmm0, zmm1, ptr_b[rax]) - MASM :
vaddps zmm0, zmm1, dword bcst [rax] - NASM :
vaddps zmm0, zmm1, [rax]{1to16} - GAS :
vaddps (%rax){1to16}, %zmm1, %zmm0
1個の要素をレジスタ全体にブロードキャストするにはIntelの表記では{1toX}を使います。
今回はfloat(32bit)をzmmレジスタ(512bit)にブロードキャストするのでX = 512/32=16となります。GASとNASMは{1to16}という属性が付与されています。
しかし、本来ニーモニックとレジスタからXの値は一意に決まります。そこでXbyakではptr_bを採用し、自動的に設定されるようにしました(bはbroadcastから)。
MASMも恐らく同様の考えによりbcstを使います。ただしMASMは元のデータサイズ(今回はdword)を指定する必要があります。
s_xbyakではptr_bから{1toX}を求めるために、Intel SDMからニーモニックのパターンを全て抜き出してテーブルを作って対応しました。なかなか面倒でした。問題5
A6
- Xbyak :
vcvtps2dq(zmm0 | T_rd_sae, zmm1)またはvcvtps2dq(zmm0, zmm1 | T_rd_sae) - MASM :
vcvtps2dq zmm0, zmm1{rd-sae} - NASM :
vcvtps2dq zmm0, zmm1, {rd-sae} - GAS :
vcvtps2dq {rd-sae}, %zmm1, %zmm0
Intel XEDによる逆アセンブラはvcvtps2dq zmm0{rd-sae}, zmm1を出力しました。アセンブラによって{rd-sae}の指定位置が異なります。
Xbyakでは場所に悩まないように、どちらのレジスタにつけてもOKにしています。
MASMはIntelの表記に似てレジスタと{rd-sae}の間にコンマはないのですが、NASMは何故かコンマが必要です。{1toX}はコンマが要らないので不思議ですね。
GASは{rd-sae}はレジスタじゃないのにそれを含めて逆順にしているので違和感があります。問題6
A7
- Xbyak:
vrndscaleps(zmm0|T_sae, zmm1, 0)またはvrndscaleps(zmm0, zmm1|T_sae, 0) - MASM :
vrndscaleps zmm0, zmm1, 0{sae} - NASM :
vrndscaleps zmm0, zmm1, {sae}, 0 - GAS :
vrndscaleps $0, {sae}, %zmm1, %zmm0
前問の類題で即値を取る場合です。Intel XEDによる逆アセンブラはvrndscaleps zmm0{sae}, zmm1, 0を出力しました。やはりアセンブラごとに{sae}の場所が違います。NASMやGASは{sae}が端にあるというわけではないのですね。問題7
A8
- Xbyak :
vcvtpd2dq(xmm0, ptr[rax])またはvcvtpd2dq(xmm0, xword[rax]) - MASM :
vcvtpd2dq xmm0, xmmword ptr [rax] - NASM :
vcvtpd2dq xmm0, oword [rax] - GAS :
vcvtpd2dqx (%rax), %xmm0
vcvtpd2dq(xmm, mem)はm128とm256によって挙動が変わります。ニーモニックとレジスタだけでメモリのサイズを特定できない例外パターンの一つです。そのためメモリサイズを指定する必要があります。
Xbyakではptrを使うとxwordとして扱います。m256にしたい場合はywordを使います。
NASMはxwordではなくowordを指定しなければなりません。ywordやzwordはそのままなのでxwordにしてほしかったところです。GASはニーモニックにxをつけます(m256ならy、m512ならz)。以前objdumpのバグを見つけて報告したことがあります。
サイズ指定子一覧
| asm | 8 | 16 | 32 | 64 | 128 | 256 | 512 |
|---|---|---|---|---|---|---|---|
| Xbyak | byte | word | dword | qword | xword | yword | zword |
| MASM | byte ptr | word ptr | dword ptr | qword ptr | xmmword ptr | ymmword ptr | zmmword ptr |
| NASM | byte | word | dword | qword | oword | yword | zword |
| GAS | b | w | l | q | x | y | z |
GASの32bitがdではなくlなのに注意。lだとlongで64bitかと勘違いしそうです。問題8
A9
- Xbyak :
vcvtpd2dq(xmm0, yword_b[rax]) - MASM : 未対応
- NASM :
vcvtpd2dq xmm0, [rax]{1to4} - GAS :
vcvtpd2dq (%rax){1to4}, %xmm0またはvcvtpd2dqy (%rax){1to4}, %xmm0
Xbyakではywordとしてブロードキャストするためにyword_bを使います(ここではptr_bを使うとxword_bとなります)。
GASやNASMでは{1to4}があるのでm256とわかるためにサイズを指定する必要はありません。
MASMはvcvtpd2dq xmm0, qword bcst ymmword ptr [rax]と書いてもm128のブロードキャスとして扱われたのでバグ報告しました。そのうち改善されると思います。問題9
A10
- Xbyak :
vpdpbusd(xmm0, xmm1, xmm2, VexEncoding) - MASM :
vex vpdpbusd xmm0, xmm1, xmm2 - NASM : 未対応?
- GAS :
{vex} vpdpbusd %xmm2, %xmm1, %xmm0
デフォルトでは{evex}やEvexEncodingを指定したのと同じです。
NASMでは{evex}は指定できてAVX-512としてエンコードされましたが、{vex}はエラーになりました。現時点で未対応なのかもしれません(マニュアルには{vex2}や{vec3}という表記があったのですがどれもエラー)。問題10
まとめ
s_xbyakのAVX-512関係の実装のために他のアセンブラでの記法を調べたのですが、改めて構文に細かな違いがあるのに気付きました。
とりあえずs_xbyakでこれらの差異は吸収できたので、今後(私は)開発が多少楽になるでしょう。
バグや未対応な機能もちらほらありますね。回答に書いた以外ではMASMでAVX-512 FP16周りのvcvttsh2usiなどで{sae}を指定できないバグを見つけたりもしました。
Discussion