Windows 64bitコードでの関数呼び出し
Microsoftが定めるWindows OS上の64bitコードが使用する関数の呼び出し方法(ABI:Application Binary Interface)では、関数の引数のうち最初の4個がレジスタに割当てられます。5個目以降はスタックに割当てられます。戻り値はRAXレジスタです。
Parameter Passing
引数の位置 |
整数*1 |
浮動小数点数 |
第1引数 |
RCX |
XMM0 |
第2引数 |
RDX |
XMM1 |
第3引数 |
R8 |
XMM2 |
第4引数 |
R9 |
XMM3 |
実際にどのようなコードが生成されるかを、Visual Studio 2015 Community EditionのVC++で試してみました。
サンプルコードの記述およびコンパイルにおけるメモ
- C++コードではシンボル(関数名)がマングリングされるので、extern "C"で関数を宣言します。
- Releaseビルドをします。
- ごく単純なサンプルコードは最適化でインライン展開され、また無効コードとして削除されるといった影響があるので、コンパイルオプションに/Ob1(コンパイラ判断ではインライン展開しない)を追加します。
- 引数を無理やりにでも使うコードを記述します。
- プロジェクトのプロパティで[C/C++] > [出力ファイル]でアセンブリの出力を指定します(アセンブリコード、コンピューター語コード、ソースコード(/FAcs))。
引数が1個(整数)の関数
int oneArg(int a)
{
int value = a + 1;
return value;
}
ビルドすると、コンパイルされたオブジェクトファイル(*.obj)と同じディレクトリに、アセンブリコードのファイル(*.cod)が生成されます。
第1引数は整数なのでRCXレジスタが、戻り値はRAXレジスタが使われるコードが生成されると期待して生成されたアセンブリコードを見てみましょう。
_TEXT SEGMENT
a$ = 8
oneArg PROC
00000 8d 41 01 lea eax, DWORD PTR [rcx+1]
00003 c3 ret 0
oneArg ENDP
_TEXT ENDS
EAXレジスタは、戻り値を入れるRAXレジスタの下位32bitなので、戻り値型であるint型整数(32bit)が格納される場所で間違いなさそうです。
RCXレジスタは、第1引数を入れるレジスタなのでこれも間違いなさそうです。
ソースコードでは、第1引数の値に1を加算していますが、[rcs+1]で表現されているように見えます。
さて、ここでLEA命令が生成されています。LEA命令を調べると、アドレス計算の命令ですが、転じて整数の計算に使われています。ここでのコードは、RCXレジスタの値に1を加算した値をEAXレジスタに入れるものです。
引数が1個の関数を呼び出すコード
int arg = std::stoi(str);
int ret1 = oneArg(arg);
生成されたアセンブリコードは次です。
000b7 8b cf mov ecx, edi
000b9 e8 00 00 00 00 call oneArg
000be 44 8b d0 mov r10d, eax
なお、EDIレジスタには前段の処理で、oneArg関数の引数として渡す値が入っています。
第1引数RCXレジスタの下位32bitであるECXレジスタに値を入れたあと関数呼び出しを行います。
戻り値であるEAXレジスタの値をR10レジスタの下位32bitに入れています。
push, popがなくすっきりしたコードになっています。
引数が4個(整数)の関数
int fourArgs(int a, int b, int c, int d)
{
int alfa = a + b;
int bravo = c * d;
int ret = alfa + bravo;
return ret;
}||<
第1引数aがRCX、第2引数bがRDX、第3引数cがR8、第4引数dがR9の各レジスタに入れて渡され、戻り値はRAXレジスタが使われるコードが生成されると期待されます。生成されたアセンブリコードを見てみましょう。
>|asm|
fourArgs PROC ; COMDAT
; 5 : int alfa = a + b;
; 6 : int bravo = c * d;
00000 45 0f af c1 imul r8d, r9d
; 7 : int ret = alfa + bravo;
00004 42 8d 04 01 lea eax, DWORD PTR [rcx+r8]
00008 03 c2 add eax, edx
; 8 : return ret;
; 9 : }
0000a c3 ret 0
fourArgs ENDP
最初に第3引数cと第4引数dの掛け算のコードが生成されています。レジスタR8DはR8レジスタの下位32bitで、R9DはR9レジスタの下位32bitです。
imul命令でオペランドのレジスタ2つの掛け算が実行され、結果はR8レジスタに上書きされます。
lea命令で、第1引数aと、imul命令結果(c * d)が加算され、EAXレジスタに格納されます。
続くadd命令で、EAXレジスタ( a + c * d)と第2引数bが加算され、EAXレジスタに格納され、関数からリターンします。
引数が4個の関数を呼び出すコード
int ret2 = fourArgs(ret1, ret1 * 2, ret1 * 3, ret1 * 4);
生成されたアセンブリコードは次です。
000c1 44 8d 0c 85 00
00 00 00 lea r9d, DWORD PTR [rax*4]
000c9 44 8d 04 40 lea r8d, DWORD PTR [rax+rax*2]
000cd 8d 14 00 lea edx, DWORD PTR [rax+rax]
000d0 8b c8 mov ecx, eax
000d2 e8 00 00 00 00 call fourArgs
000d7 8b d8 mov ebx, eax
fourArgs関数の引数1〜4に渡すの値を、lea命令で計算しています。計算結果をそれぞれ引数2(RDXの下位32bitであるEDXレジスタ)、引数3(R8の下位32bitであるR8Dレジスタ)、引数4(R9の下位32bitであるR9Dレジスタ)、引数1(RCXの下位32bitであるECXレジスタ)に入れて、callで関数を呼び出しています。
ここまで関数の引数には、スタックは使われずレジスタで受け渡しされています。
引数が5個(整数、浮動小数点数混在)の関数
int fiveArgs(int a, double b, char* c, int d, int e)
{
int v1 = a + d + e;
double v2 = v1 / b;
double v3 = v2 + *c;
int ret = oneArg((int)v3);
return ret;
}
第1引数aがRCX、第2引数bがXMM1、第3引数cがR8、第4引数dがR9の各レジスタに入れて渡され、第5引数eはスタックに積まれて渡され、戻り値はRAXレジスタが使われるコードが生成されると期待されます。生成されたアセンブリコードを見てみましょう。
fiveArgs PROC
00000 42 8d 04 09 lea eax, DWORD PTR [rcx+r9]
00004 03 44 24 28 add eax, DWORD PTR e$[rsp]
00008 66 0f 6e d0 movd xmm2, eax
0000c 41 0f be 00 movsx eax, BYTE PTR [r8]
00010 f3 0f e6 d2 cvtdq2pd xmm2, xmm2
00014 66 0f 6e c0 movd xmm0, eax
00018 f2 0f 5e d1 divsd xmm2, xmm1
0001c f3 0f e6 c0 cvtdq2pd xmm0, xmm0
00020 f2 0f 58 d0 addsd xmm2, xmm0
00024 f2 0f 2c ca cvttsd2si ecx, xmm2
00028 e9 00 00 00 00 jmp oneArg
fiveArgs ENDP
ちょっと複雑になってきました。
最初の計算では、第1引数aがRCX、第4引数dがR9、第5引数はスタックにあるのですが、pop命令ではなく、スタックポインタRSPにオフセットe$を加えた場所から値を取ってきて加算しています。
5個目の引数がスタックから取られていることが分かりました。
以降は省略します。
引数が4個の関数を呼び出すコード
15 : int ret3 = fiveArgs(argc, arg, (char*)argv, arg, arg)
000dd f3 0f e6 c9 cvtdq2pd xmm1, xmm1
000e1 89 74 24 20 mov DWORD PTR [rsp+32], esi
000e5 44 8b ce mov r9d, esi
000e8 4c 8b c7 mov r8, rdi
000eb 8b cd mov ecx, ebp
000ed e8 00 00 00 00 call fiveArgs
第1引数にRCX(ECX)、第2引数にXMM1、第3引数にR8、第4引数にR9(R9D)、第5引数はスタックに値を入れています。
スタックには、pushではなく、スタックポインタ(RSP)からのオフセットにmov命令で値を入れています。