MSVCが/Od
で吐き出すアセンブリの話です。
最適化をかけると呼出し自体が消滅(inline)したり、余分なバックアップがなくなったりします。
x86では__cdecl
、__stdcall
などいろいろな呼び出し規約がありましたが、x64では__fastcall
に一本化(もはや区別する意味が無い)されました。
レジスタ
引数
引数は先頭4つまでがレジスタ渡しになりうる。この4つというのは整数、浮動小数合わせたもの。
0番目引数は、整数ならrcx
浮動小数ならxmm0
。
1番目引数は、整数ならrbx
浮動小数ならxmm1
。
2番目引数は、整数ならr8
浮動小数ならxmm2
。
3番目引数は、整数ならr9
浮動小数ならxmm3
。
例えばf(int,int,float,float)
のようなときはf(rxc,rbx,xmm2,xmm3)
となる。
f(rxc,rbx,xmm0,xmm1)
ではない点に注意。
また、f(int,int,int,int,float,float)
では4,5番目は浮動小数レジスタが余っているが引数に利用されずスタック渡しとなる。
引数
戻り値は、整数ならrax
、浮動小数ならxmm0
。
整数の場合は引数で用いるレジスタとは別だが、浮動小数では戻り値と0番目引数が同じくxmm0
を用いる。排他ではなく呼び出し直前と直後で解釈が変わるということ。
バックアップ
下記レジスタの状態をバックアップするのは呼び出された側の役目。(そもそも使わない場合はバックアップもスキップされるので注意)
それ以外のレジスタは呼び出され側で書き換え自由なので、ある意味呼び出し側で(必要に応じて)バックアップが必要ということになる。
R12~R15
(汎用整数)、RBX,RDI,RSI
(一応用途はあるけど実質汎用整数)、
XMM6~XMM15
(汎用浮動小数)、
RBP
(基本的にベースポインタ?)
スタック
引数にスタックを使わない場合でも、呼び出し前に40Bを自由に使える領域として確保しておく必要があります。
40Bの内訳は、アラインメント8+レジスタ引数バックアップ用8*4で40とのこと。
アラインメントが揃っているなら8は不要ですが、スタックのアラインメントは16Bで関数呼び出し時に戻りアドレスのバックアップで8B積まれているので、アライメントが崩れています。
http://herumi.in.coocan.jp/prog/x64.html
sub rsp 40
実際にMSVCが吐くアセンブリでは、スタックの確保の回数はなるべく少なくするようになっているようで、関数の初めにローカル変数や引数用の分とまとめて確保(rsp
を減算)しているようです。
また、呼び出され側で引数を受け取った場合はとりあえず、呼び出し側が確保してくれた引数用スタックの32Bに放り込んでいます。(引数有関数では冒頭は以下のような決まり文句になっています)
f:
mov DWORD PTR [rsp+32], r9d ; 3番目整数引数
mov DWORD PTR [rsp+24], r8d ; 2番目整数引数
mov DWORD PTR [rsp+16], edx ; 1番目整数引数
mov DWORD PTR [rsp+8], ecx ; 0番目整数引数
// push ?? ; レジスタのバックアップ
// push ??
sub rsp, ?? ; スタックの確保
これを一見すると、スタック確保前にスタック使っているように見えるのですが、movしている先のスタックは呼び出し元の関数のスタック領域です。(rspより下なので確保済み領域ですね)
アドレス | 内容 | 確保した関数 | 使う関数 |
---|---|---|---|
0x00 | ローカル変数 | Callee | Callee |
0x08 | レジスタのバックアップ | Callee(push) | Callee |
0x10 | ReturnAddress | Caller(jmp) | Callee |
0x18 | アラインメント | Caller | - |
0x20 | 5番目引数 | Caller | Callee |
0x28 | 4番目引数 | Caller | Callee |
0x30 | 3番目引数バックアップ用 | Caller | Callee |
0x38 | 2番目引数バックアップ用 | Caller | Callee |
0x40 | 1番目引数バックアップ用 | Caller | Callee |
0x48 | 0番目引数バックアップ用 | Caller | Callee |
0x50 | ローカル変数 | Caller | Caller |
ベースポインタとRBP
rbp
はベースポインタということで、上の表で言うならCalleeに居る時、rbp=0xXX38
と期待されると思うのですが、実際に吐かれるアセンブリではその限りではないようです。
というのもスタックの確保は基本的に関数の先頭でまとめてsub rsp
で済ませてしまうため、ローカル変数や引数の基準点はrsp
からでも計算できますし、それでなくとも基準点さえ固定であれば何もReturnAddressが入っているアドレスを基準にする必要もありません。
なので微妙な位置にrbp
をセットすることもあるようです。特に、rsp
は先の関数呼び出しのために確保してあげる40B+αも含めたものになっているので、そこから40B+αを足した分(量的には引いた分)をrbp
にセットしているようです。
おまけ
これ便利