原文:
※原文は2001/9月に書かれ、VC6 + Win2kをベースに記述されています。日付や環境は古いですが、内容自体は2010/2月時点でのMSDNの呼び出し規約に関する説明と同じです。
※なお記事中のアセンブラコードについては、原文のものをそのまま載せています。こちらについては、2010年現在も同じ保証がありません。また記事中のコードはデバッグモードでコンパイルしたもので、最適化は施されていないようです。
WindowsでのC++プログラミングの学習中に、関数の宣言で使われる "__cdecl", "__stdcall", "__fastcall", "WINAPI"という文字列を見かけたことはないだろうか?もしかしたらMSDNで検索し、それが「呼び出し規約(calling convention)」を指定するものということを調べた人も居るかも知れない。この記事では、Windows上でのVisualC++コンパイラが使う呼び出し規約について解説する。なお、この記事で解説する呼び出し規約はあくまでもMicrosoftのもので、それ以外でのコンパイラやアーキテクチャでのサポート状況は保証しない。移植性に注意する場合は、これらの規約は使えないかも知れない点に注意して欲しい。
呼び出し規約とは何か?呼び出し規約とは引数がどのように関数に渡され、戻り値がどのように戻されるかを定義している。また呼び出し規約に応じて関数名がどのように修飾されるかも定義している。C/C++プログラムを書く時に、本当に呼び出し規約の知識が必要かというと、必須とは言えない。ただし呼び出し規約の知識は、デバッグを大いに助けてくれるだろう。またアセンブラで書かれたコードとC/C++をリンクする時は必須となる。
なお、この記事を読むに当たり基本的なアセンブラプログラミングの知識が必要だろう。
さて、どの呼び出し規約を使うにせよ、以下の処理は共通して行われる。
呼び出し規約の例として("thiscall"を除く)、次の関数を題材に使う。
int sumExample (int a, int b) { return a + b; }
この関数を呼ぶ側は次のコードを想定する。
int c = sum (2, 3);
__cdecl, __stdcall, __fastcall の呼び出し規約の解説では、このコードを"C++ではなく"、Cソースとしてコンパイルしている。この記事で紹介する関数名の修飾ルールはCの宣言に適用される。C++の場合のシンボル名の修飾については本記事では取り扱わない。
この呼び出し規約はC/C++プログラムのデフォルトとして使われる。コンパイラオプションで指定したい場合は"/Gd"になる。もしプロジェクトのデフォルトが他の呼び出し規約に設定されていても、関数宣言で __cdecl を指定することが出来る。
int __cdecl sumExample (int a, int b);
__cdecl呼び出し規約の主な特徴は次の通り:
__cdeclで呼び出す側のアセンブラコードを見てみる:
; // push arguments to the stack, from right to left push 3 push 2 ; // call the function call _sumExample ; // cleanup the stack by adding the size of the arguments to ESP register add esp,8 ; // copy the return value from EAX to a local variable (int c) mov dword ptr [c],eax
呼ばれる関数側のアセンブラコードを見てみる:
; // function prolog ; 直前のベースポインタ(フレームポインタ)をスタック上に退避 push ebp ; フレームポインタをESPで更新 mov ebp,esp ; SUBでESPを直接減算し、ローカル変数 ; (C言語でいうauto変数)の領域を確保 sub esp,0C0h ; レジスタの値をスタック上に退避 push ebx push esi push edi ; コンパイラがstack保護の為に生成した ; マジックナンバーのfilling lea edi,[ebp-0C0h] mov ecx,30h ; repによるループ回数 mov eax,0CCCCCCCCh ; 0xCC が4バイト分 ; EAXの内容をEDIの指すアドレスを先頭にコピーしていく。 ; EAXは0xCCが4バイトで、ループ回数は0x30なので、 ; 0x30 x 4バイト = 0xC0 バイト分 0xCC がコピーされる。 ; 0xC0というのはローカル変数で確保したサイズと一致する。 ; これにより、ローカル変数領域を0xCCで初期化したことになる。 rep stos dword ptr [edi] ; // return a + b; mov eax,dword ptr [a] add eax,dword ptr [b] ; // function epilog ; レジスタの復帰 pop edi pop esi pop ebx ; ESPを巻き戻すことでローカル変数領域を解放したことになる mov esp,ebp ; ベースポインタの復元 pop ebp ; 呼び出し元に戻る ret
※"rep"命令は呼び出し規約とは無関係で、MSVCのコンパイラが生成したデバッグ用のコードのようです。元記事の方では、コメント欄に「スタックオーバーフロー用のコードじゃない?」「いや、デバッガで値を確認しやすくするように初期化するデバッグコードだ」という投稿があります。
この呼び出し規約は主にWin32API関数で使われる。WINAPIは__stdcallとしてdefineされたマクロである。
#define WINAPI __stdcall
この呼び出し規約を使用するには、"__stdcall"を明示的に関数宣言で指定する。
int __stdcall sumExample (int a, int b);
コンパイラオプションで "/Gz" を指定することで、明示的に呼び出し規約が指定されていない関数に対して __stdcall 呼び出し規約を適用することが出来る。
__stdcall呼び出し規約の主な特徴は次の通り:
呼び出し側のアセンブラコード例:
; // push arguments to the stack, from right to left push 3 push 2 ; // call the function call _sumExample@8 ; // copy the return value from EAX to a local variable (int c) mov dword ptr [c],eax
関数側のアセンブラコード例:
; // function prolog goes here (the same code as in the __cdecl example) ; // return a + b; mov eax,dword ptr [a] add eax,dword ptr [b] ; // function epilog goes here (the same code as in the __cdecl example) ; // cleanup the stack and return ; オペランド付のnear returnの場合、 ; オペランドで指定されたバイト数分、 ; 戻った後ESPを加算する = スタックを縮める。 ; これにより呼ぶ側(caller)で確保した引数領域を ; スタック上から解放する。 ret 8
スタッククリーンアップが関数側で行われる為、__stdcall呼び出し規約は__cdeclよりは若干サイズが小さくなる。一方、printf()のような可変長引数を使う関数は__stdcall呼び出し規約を使えない。可変長引数の場合、引数をいくつstackにpushしたのかは関数を呼ぶ側(caller)だけが知っている為、スタッククリーンアップも呼ぶ側(caller)で行う。可変長引数を使う関数は__cdecl呼び出し規約を使う必要がある。
__fastcallは、引数を可能であればレジスタ経由で渡す呼び出し規約である。レジスタ渡しはスタック経由よりは若干速くなる。
この呼び出し規約を使用するには、"__fastcall"を明示的に関数宣言で指定する。
int __fastcall sumExample (int a, int b);
コンパイラオプションで "/Gr" を指定することで、明示的に呼び出し規約が指定されていない関数に対して __fastcall 呼び出し規約を適用することが出来る。
__fastcall呼び出し規約の主な特徴は次の通り:
※将来のバージョンでは、レジスタ渡しに使うレジスタが変更される可能性がある。
呼び出し側のアセンブラコード例:
; // put the arguments in the registers EDX and ECX mov edx,3 mov ecx,2 ; // call the function call @fastcallSum@8 ; // copy the return value from EAX to a local variable (int c) mov dword ptr [c],eax
関数側のアセンブラコード例:
; // function prolog push ebp mov ebp,esp sub esp,0D8h push ebx push esi push edi push ecx lea edi,[ebp-0D8h] mov ecx,36h mov eax,0CCCCCCCCh rep stos dword ptr [edi] pop ecx mov dword ptr [ebp-14h],edx mov dword ptr [ebp-8],ecx ; // return a + b; mov eax,dword ptr [a] add eax,dword ptr [b] ;// function epilog pop edi pop esi pop ebx mov esp,ebp pop ebp ret
thiscallはC++クラスのメンバ関数を呼ぶ時のデフォルトの呼び出し規約である。ただし引数が可変のメンバ関数を呼ぶ時を除く。
thiscall呼び出し規約の主な特徴は次の通り:
thiscallのサンプルは、他と異なり"C++"としてコンパイルする必要がある。題材として次のようなメンバ関数を用意した。
struct CSum { int sum ( int a, int b) {return a+b;} };
呼び出し側のアセンブラコード例:
push 3 push 2 lea ecx,[sumObj] call ?sum@CSum@@QAEHHH@Z ; CSum::sum , C++ method name mangling mov dword ptr [s4],eax
関数側のアセンブラコード例:
push ebp mov ebp,esp sub esp,0CCh push ebx push esi push edi push ecx lea edi,[ebp-0CCh] mov ecx,33h mov eax,0CCCCCCCCh rep stos dword ptr [edi] pop ecx ; "this"の取得 mov dword ptr [ebp-8],ecx ; a + b mov eax,dword ptr [a] add eax,dword ptr [b] pop edi pop esi pop ebx mov esp,ebp pop ebp ; オペランド付のRET命令により、 ; ESPの調整して引数領域をスタックから解放する。 ret 8
引数が可変なメソッドを呼んだ場合は、__cdeclが使われ、"this"はスタックの最後にpushされる。
"Conclusion", "License", "About the Author"の抄訳は省略、原文を参照のこと。