x64 アセンブリ言語プログラミング

x64(x86-64)について


Windowsでの呼び出し既約
Software Conventionsを参考にしつつメモを.
Windowsでのレジスタ
汎用レジスタが倍増したので引数の数が少ないときはスタック経由ではなく基本的にレジスタ経由になりました. また32bitのときにあった__fastcall, __stdcallなど(cf. VC++では)は全て無視されるようになりました. C++のメンバ関数の呼び出し方法についてはC++ではを参照.

Cの関数のレジスタの使い方は次の通りです.

レジスタの用途
レジスタ 用途 呼び出された側(callee)での保存の必要性
rax 戻り値 不要
rcx 1番目の整数型引数 不要
rdx 2番目の整数型引数 不要
r8 3番目の整数型引数 不要
r9 4番目の整数型引数 不要
r10, r11 - 不要
r12~r15, rdi, rsi, rbx, rbp, rsp - 変更するなら必要*
xm0 1番目の浮動小数型引数 / 戻り値 不要
xm1 2番目の浮動小数型引数 不要
xm2 3番目の浮動小数型引数 不要
xm3 4番目の浮動小数型引数 不要
xm4, xm5 - 不要
xm6~xm15 - 変更するなら必要*

Windowsでのスタック
スタックは常に16byteアラインメントされています. ただし関数呼び出し直後は戻りアドレス(8byte)がpushされているため, 8(mod 16)となっています. 関数内から別の関数を呼び出すときはアライメントを揃える必要があり, 引数4個のスタック分(32byte)を呼び出し元で確保する必要があります. 確保された領域は呼び出された側で自由に使えます.
    push rbx // アライメントをそろえる(add rsp, 32とくっつけてもよい)
    // 1変数関数int func(int x)を呼び出す
    sub  rsp, 32
    // rcxに値設定
    call func
    // raxを使う
    add  rsp, 32
    pop rbx
    // func()内部
proc func:
    mov  [rsp + 8], ecx ; ecxを保存してみる
    ...

今までのx86に慣れているとこれはなかなかびっくりです.

FPU/3D Now!非サポート?
WikipediaのXpAMD64には
64ビットアプリケーションではx87命令・MMX命令及び3DNow!命令をサポートしない(x87レジスタをコンテキストスイッチの際にセーブしない)
なんてあって驚いた. ほんまかいなと64bit Xp Professionalで試してみた(ソース:test_mmx.cpp).

スレッドを起動してmm0にそれぞれのスレッド上で異なる値を設定して値をポーリング読み出しする. このプログラムを複数異なる引数で起動して読み出された値を確認する. もしコンテキストスイッチでMMXレジスタが保存されていないなら値は同じものになるはず.

試してみると正しく異なる値になった. これを見る限りでは上記「MMX/3D Now!非サポート」は間違っていて「MMX/3D Now!サポート」が正しそう.
一つ可能性としては32bit Xp上のVMware上の64bit Xpで試したのでホストOS側でたまたま保存してしまってるという可能性もあるが(どなたか検証してみて), Legacy Floating-Point Supportにも

The MMX and floating-point stack registers (MM0-MM7/ST0-ST7) are preserved across context switches

と明記されていた(カーネル内部では使えないとある). 最初非サポートにしようとしたが仕様が変わったというところだろうか.
英語版Wikipediaのx86-64にはサポートすると書いてあった…. 日本語版が古いままなだけか.

gccでの呼び出し既約
x64 System V Application Binary Interface AMD64 Architecture Processor Supplementを参考にしつつメモを.
gccでのレジスタ

Cの関数呼び出しのレジスタの使い方は次の通りです. C++のメンバ関数の呼び出し方法についてはC++ではを参照.

レジスタの用途
レジスタ 用途 呼び出された側(callee)での保存の必要性
rax 戻り値 / 利用したSSEレジスタの数 不要
rdi 1番目の整数型引数 不要
rsi 2番目の整数型引数 不要
rdx 3番目の整数型引数 不要
rcx 4番目の整数型引数 不要
r8 5番目の整数型引数 不要
r9 6番目の整数型引数 不要
r10, r11 - 不要
r12~r15, rbx, rbp, rsp - 変更するなら必要*
xm0~xm1 1番目の浮動小数型引数 / 戻り値 不要
xm2~xm7 n番目の浮動小数型引数 不要
xm8~xm15 テンポラリ 不要

gccでのスタック
スタックは常に16byteアラインメントされています. ただし関数呼び出し直後は戻りアドレス(8byte)がpushされているため, 8(mod 16)となっています. Windowsと同様, 関数内から別の関数を呼び出すときはアライメントを揃える必要があります.
callerとcallee
ある場所から関数func()をcallした方を呼び出し元(caller), 呼び出された関数func()を呼び出し先(callee)といいます. 上記表でcalleeでの保存の必要性が「不要」というのは, func()の中でそのレジスタを使ったときに呼び出されたときの状態に戻す必要が無いという意味です. 「変更するなら必要」は字義通り変更するなら関数を抜けるときに元の状態に戻す必要があります.
逆に, callerからすると「不要」とあるレジスタは関数呼び出しをしたあと未初期化状態として扱う必要があり, 「変更するなら必要」とあるレジスタは関数呼び出しの前後で値は変わっていないものとして扱えます.
 +---caller---+    +--callee--+
 |    ...     |    |          |
 | mov rax, 3 |    |  (func)  |
 | call func -+--->+   ...    |
 |    ...     <----+-- ret    |
 |    ...     |    +----------+
 +------------+     (rbpなどをいじったら元に戻す)
    ↑
 (rbpなどは変わらないと思っていい)
FPUレジスタ
doubleやfloatなどの浮動小数点数は関数の引数, 戻り値ともにFPUレジスタではなくSSEレジスタに割り当てられます. 下記のfunc1はVC, gccともに同一のコードになります. 簡潔でいいですね.
double func1(double x, double y)
{
    return x + y;
}
proc func1:
  addsd xmm0, xmm1
  ret
SSEレジスタ
SSEレジスタを現す__m128dや__m128はVC, gccともに関数の戻り値として使われるとSSEレジスタ渡し(xmm0)となります. 関数の引数として使われるとVCとgccで異なります. VCではポインタ渡しになります. なんでレジスタ渡しじゃないのか理解に苦しみますが下記コードはどちらも同じasm出力となります.
__m128 func3(__m128 x, __m128 y)
{
    return _mm_xor_ps(x, y);
}

__m128 func4(const __m128& x, const __m128& y)
{
    return _mm_xor_ps(x, y);
}

64bit Windows用Intel Cコンパイラではrcx経由ではなくxmm0に直接渡されるようです.

proc func3:
  movaps xmm0, xmmword ptr [rcx] ; 1番目の値へのポインタ経由
  xorps  xmm0, xmmword ptr [rdx] ; 2番目の値へのポインタ経由
  ret
(追記) VC2013で導入されたベクトル呼び出し規約 __vectorcallも参照.

gccでは関数の引数はレジスタに割り当てられます. 上記func3は下記のようにすっきりします. __m128へのconst参照はできないようでfunc4はエラーとなります.

proc func3:
  xorps xmm0, xmm1
  ret
混在パターン
整数とFPUが混じった関数の場合酷いことになります. 引数の番号がVCでは通し番号ですが, gccではそれぞれ別に番号が振られます. VCとgccで両方動作させるにはかなり気をつかう必要があります. 具体的にみてみましょう.
double func5(int a, double x, int b, double y)
{
    return a + x + b + y;
}

VCの場合, 通し番号なのでaは1番目, xは2番目, bは3番目, yは4番目となります. aは1番目のintなのでecx, xは2番目のdoubleなのでxmm0ではなく, xmm1です. bは3番目のintなのでedxではなくr8d, yは4番目のdoubleなのでxmm3です.

VCのfunc5のレジスタ割り当て
int a(1番目) double x(2番目) int b(3番目) double y(4番目)
整数だけだった場合 ecx edx r8d r9d
FPUだけだった場合 xmm0 xmm1 xmm2 xmm3
割り当てられるレジスタ ecx xmm1 r8d xmm3

  movd     xmm0, ecx   ; a
  cvtdq2pd xmm0, xmm0  ; (double)a
  addsd    xmm0, xmm1  ; a += x
  movd     xmm1, r8d   ; b
  cvtdq2pd xmm1, xmm1  ; (double)b
  addsd    xmm0, xmm1  ; a += b
  addsd    xmm0, xmm3  ; a += y
  // return a + x + b + y
  ret

gccの場合, 整数とFPUで別々なのでaは整数の1番目, xはFPUの1番目, bは整数の2番目, yはFPUの2番目となります. つまりaはedi, bはxmm0, bはesi, yはxmm1となります.

gccのfunc5のレジスタ割り当て
int a(1番目のint) double x(1番目のdouble) int b(2番目のint) double y(2番目のdouble)
整数だけだった場合 edi esi edx ecx
FPUだけだった場合 xmm0 xmm1 xmm2 xmm3
割り当てられるレジスタ edi xmm0 esi xmm1

  movapd   xmm2, xmm0  ; x
  cvtsi2sd xmm0, edi   ; (double)a
  addsd    xmm0, xmm2  ; a += x
  cvtsi2sd xmm2, esi   ; (double)b
  addsd    xmm0, xmm2  ; a += b
  addsd    xmm0, xmm1  ; a += y
  // return a + x + b + y
  ret

どっちでも動くコードを書きたい方, とりあえず泣いてください.


御意見は光成滋生<herumi@nifty.com>までお願いします