呼出規約をアセンブリコードで確認
Windowsプログラミングで関数に付けるWINAPIとかAPIENTRYとかのキーワード、いったい何なのかと調べると呼出規約というやつで、関数呼び出し時の引数の渡し方やスタックの掃除のしかたがこれによって変わるらしい。
言葉の説明だけ見ても実感がわかないので、アセンブリコードで見てみました。
出力
__int64 __cdecl f(int a, __int64 b){ // __cdecl と __stdcall のどちらかを使う return a + b; } int main(){ f(1, 2); return 0; }
これを、VCの「逆アセンブル」ウィンドウ(デバッグ時に出せる)で見ると、次のようになりました。
プロジェクトのオプションで「C/C++ - コード生成 - 基本ランタイム チェック」を「既定値」に設定しておくと、エラーチェックのコードが減って見やすくなります。(シンタクスハイライト、どれを指定したらいいかわからない…)→【追記】*asmとか試してみたものの行番号部分がうまく色分けされないのでハイライト無しにしておきます
__cdeclの場合
__int64 __cdecl f(int a, __int64 b){ 00411260 push ebp 00411261 mov ebp,esp 00411263 sub esp,40h 00411266 push ebx 00411267 push esi 00411268 push edi return a + b; 00411269 mov eax,dword ptr [a] 0041126C cdq 0041126D add eax,dword ptr [b] 00411270 adc edx,dword ptr [ebp+10h] } 00411273 pop edi 00411274 pop esi 00411275 pop ebx 00411276 mov esp,ebp 00411278 pop ebp 00411279 ret int main(){ 00411280 push ebp 00411281 mov ebp,esp 00411283 sub esp,40h 00411286 push ebx 00411287 push esi 00411288 push edi f(1, 2); 00411289 push 0 0041128B push 2 0041128D push 1 0041128F call f (411064h) 00411294 add esp,0Ch return 0; 00411297 xor eax,eax } 00411299 pop edi 0041129A pop esi 0041129B pop ebx 0041129C mov esp,ebp 0041129E pop ebp 0041129F ret
__stdcallの場合
__int64 __stdcall f(int a, __int64 b){ 00411260 push ebp 00411261 mov ebp,esp 00411263 sub esp,40h 00411266 push ebx 00411267 push esi 00411268 push edi return a + b; 00411269 mov eax,dword ptr [a] 0041126C cdq 0041126D add eax,dword ptr [b] 00411270 adc edx,dword ptr [ebp+10h] } 00411273 pop edi 00411274 pop esi 00411275 pop ebx 00411276 mov esp,ebp 00411278 pop ebp 00411279 ret 0Ch int main(){ 00411290 push ebp 00411291 mov ebp,esp 00411293 sub esp,40h 00411296 push ebx 00411297 push esi 00411298 push edi f(1, 2); 00411299 push 0 0041129B push 2 0041129D push 1 0041129F call f (41100Fh) return 0; 004112A4 xor eax,eax } 004112A6 pop edi 004112A7 pop esi 004112A8 pop ebx 004112A9 mov esp,ebp 004112AB pop ebp 004112AC ret
違いだけ抜き出すと、次のような感じ。
__cdecl
00411276 mov esp,ebp 00411278 pop ebp 00411279 ret f(1, 2); 00411289 push 0 0041128B push 2 0041128D push 1 0041128F call f (411064h) 00411294 add esp,0Ch ;スタックポインタを戻す return 0;
__stdcall
00411276 mov esp,ebp 00411278 pop ebp 00411279 ret 0Ch ;スタックポインタを戻す f(1, 2); 00411299 push 0 0041129B push 2 0041129D push 1 0041129F call f (41100Fh) return 0;
読み取り
__cdeclは関数呼び出しから返ったあとで、呼び出し側でスタックポインタを戻しています(00411294行)。
一方、__stdcallは関数から返るときのret命令でスタックを戻しています(00411279行)。
期待通りの動作が確認できました。
次に、fを呼び出す直前に引数をスタックに積んでいるところ。これは両者に違いはないんですが。
push 0 push 2
で、2つ目の引数である64ビット整数をまず積みます。リトルエンディアンで、スタックは番地の小さい方に伸びていくので、下位バイトである2が後に積まれています。その後で、push 1
によって1つ目の引数が積まれています。呼び出し規約の解説に書かれているとおり、スタックに積む順番は後ろの引数が先です。ここも確認OK。
あと注目するのが、スタックをクリアしている量。0Chつまり12バイトで、引数のサイズの合計と同じサイズですね。なるほど。