x64(x86-64)について
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 | - | 変更するなら必要* |
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に慣れているとこれはなかなかびっくりです.
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にはサポートすると書いてあった…. 日本語版が古いままなだけか.
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 | テンポラリ | 不要 |
+---caller---+ +--callee--+
| ... | | |
| mov rax, 3 | | (func) |
| call func -+--->+ ... |
| ... <----+-- ret |
| ... | +----------+
+------------+ (rbpなどをいじったら元に戻す)
↑
(rbpなどは変わらないと思っていい)
double func1(double x, double y)
{
return x + y;
}
proc func1: addsd xmm0, xmm1 ret
__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
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です.
| 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となります.
| 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
どっちでも動くコードを書きたい方, とりあえず泣いてください.