呼出規約をアセンブリコードで確認

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バイトで、引数のサイズの合計と同じサイズですね。なるほど。

補足

アセンブリコードをファイルとして出力させるには、プロジェクトのオプションで「C/C++ - 出力ファイル - アセンブリの出力」を適当に設定してやるとよいです。すると、***.objファイルと同じ所に***.asmファイルができます。
g++なら、-Sオプションを付けてコンパイル