home ホーム search 検索 -  login ログイン  | reload edit datainfo version cmd icon diff delete  | help ヘルプ

抄訳メモ/Calling Conventions Demystified

抄訳メモ/Calling Conventions Demystified

抄訳メモ / Calling Conventions Demystified
id: 581 所有者: msakamoto-sf    作成日: 2010-02-07 16:23:03
カテゴリ: Assembler C言語 

原文:

※原文は2001/9月に書かれ、VC6 + Win2kをベースに記述されています。日付や環境は古いですが、内容自体は2010/2月時点でのMSDNの呼び出し規約に関する説明と同じです。
※なお記事中のアセンブラコードについては、原文のものをそのまま載せています。こちらについては、2010年現在も同じ保証がありません。また記事中のコードはデバッグモードでコンパイルしたもので、最適化は施されていないようです。


イントロダクション

WindowsでのC++プログラミングの学習中に、関数の宣言で使われる "__cdecl", "__stdcall", "__fastcall", "WINAPI"という文字列を見かけたことはないだろうか?もしかしたらMSDNで検索し、それが「呼び出し規約(calling convention)」を指定するものということを調べた人も居るかも知れない。この記事では、Windows上でのVisualC++コンパイラが使う呼び出し規約について解説する。なお、この記事で解説する呼び出し規約はあくまでもMicrosoftのもので、それ以外でのコンパイラやアーキテクチャでのサポート状況は保証しない。移植性に注意する場合は、これらの規約は使えないかも知れない点に注意して欲しい。

呼び出し規約とは何か?呼び出し規約とは引数がどのように関数に渡され、戻り値がどのように戻されるかを定義している。また呼び出し規約に応じて関数名がどのように修飾されるかも定義している。C/C++プログラムを書く時に、本当に呼び出し規約の知識が必要かというと、必須とは言えない。ただし呼び出し規約の知識は、デバッグを大いに助けてくれるだろう。またアセンブラで書かれたコードとC/C++をリンクする時は必須となる。

なお、この記事を読むに当たり基本的なアセンブラプログラミングの知識が必要だろう。

さて、どの呼び出し規約を使うにせよ、以下の処理は共通して行われる。

  1. 全ての引数は4バイトに丸められ(もちろんWin32上での話)、適切なメモリ上に配置される。一般的にはスタック上に配置され、呼び出し規約によってはレジスタに格納される場合もある。
  2. 関数のアドレスにジャンプする。
  3. 関数の中では、ESI, EDI, EBX, EBPレジスタがスタック上に保存される。この処理を行うコードを"function prolog"と呼び、コンパイラにより生成される。
  4. 関数の本体コードが実行され、戻り値はEAXレジスタにセットされる。
  5. ESI, EDI, EBX, EBPレジスタ値がスタックから復元される。この処理は"function epilog"と呼ばれ、殆どの場合にコンパイラが生成する。
  6. 関数に渡した引数がスタックから削除される。この処理を「スタッククリーンアップ(stack cleanup)」と呼び、呼び出し規約に応じ関数を呼ぶ側(caller)または呼ばれる側(calee)のどちらかで実行される。

呼び出し規約の例として("thiscall"を除く)、次の関数を題材に使う。

int sumExample (int a, int b)
{
    return a + b;
}

この関数を呼ぶ側は次のコードを想定する。

int c = sum (2, 3);

__cdecl, __stdcall, __fastcall の呼び出し規約の解説では、このコードを"C++ではなく"、Cソースとしてコンパイルしている。この記事で紹介する関数名の修飾ルールはCの宣言に適用される。C++の場合のシンボル名の修飾については本記事では取り扱わない。

C calling convention (__cdecl)

この呼び出し規約はC/C++プログラムのデフォルトとして使われる。コンパイラオプションで指定したい場合は"/Gd"になる。もしプロジェクトのデフォルトが他の呼び出し規約に設定されていても、関数宣言で __cdecl を指定することが出来る。

int __cdecl sumExample (int a, int b);

__cdecl呼び出し規約の主な特徴は次の通り:

  1. 引数は右から左にスタックにpushされる。(評価順序とは無関係)
  2. スタッククリーンアップは呼ぶ側(caller)が行う。
  3. 関数名はアンダースコア1つが接頭辞になって修飾される。

__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のコンパイラが生成したデバッグ用のコードのようです。元記事の方では、コメント欄に「スタックオーバーフロー用のコードじゃない?」「いや、デバッガで値を確認しやすくするように初期化するデバッグコードだ」という投稿があります。

Standard calling convention (__stdcall)

この呼び出し規約は主にWin32API関数で使われる。WINAPIは__stdcallとしてdefineされたマクロである。

#define WINAPI __stdcall

この呼び出し規約を使用するには、"__stdcall"を明示的に関数宣言で指定する。

int __stdcall sumExample (int a, int b);

コンパイラオプションで "/Gz" を指定することで、明示的に呼び出し規約が指定されていない関数に対して __stdcall 呼び出し規約を適用することが出来る。

__stdcall呼び出し規約の主な特徴は次の通り:

  1. 引数は右から左にスタックにpushされる。(評価順序とは無関係)
  2. スタッククリーンアップは呼ばれる側(callee)が行う。
  3. 関数名の修飾はアンダースコア1つが接頭辞に、"@"に続けて引数のスタック上のサイズが接尾辞になる。

呼び出し側のアセンブラコード例:

; // 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呼び出し規約を使う必要がある。

Fast calling convention (__fastcall)

__fastcallは、引数を可能であればレジスタ経由で渡す呼び出し規約である。レジスタ渡しはスタック経由よりは若干速くなる。
この呼び出し規約を使用するには、"__fastcall"を明示的に関数宣言で指定する。

int __fastcall sumExample (int a, int b);

コンパイラオプションで "/Gr" を指定することで、明示的に呼び出し規約が指定されていない関数に対して __fastcall 呼び出し規約を適用することが出来る。

__fastcall呼び出し規約の主な特徴は次の通り:

  1. 32ビットに収まる最初の二つの引数はECX, EDXレジスタで渡される。残りは右から左の順でスタックにpushされる。(評価順序とは無関係)+ スタッククリーンアップは呼ばれる側(callee)が行う。
  2. 関数名の修飾は"@"1つが接頭辞に、"@"に続けて引数のサイズが接尾辞になる。引数のサイズは、レジスタで渡される分も含める。

※将来のバージョンでは、レジスタ渡しに使うレジスタが変更される可能性がある。

呼び出し側のアセンブラコード例:

; // 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

thiscallはC++クラスのメンバ関数を呼ぶ時のデフォルトの呼び出し規約である。ただし引数が可変のメンバ関数を呼ぶ時を除く。

thiscall呼び出し規約の主な特徴は次の通り:

  1. 引数は右から左にスタックにpushされる。"this"はECXレジスタで渡される。
  2. スタッククリーンアップは呼ばれる側(callee)が行う。

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"の抄訳は省略、原文を参照のこと。



プレーンテキスト形式でダウンロード
現在のバージョン : 1
更新者: msakamoto-sf
更新日: 2010-02-07 16:30:41
md5:8ae74dae422fbaddbf2988609c77eece
sha1:74434fa16af0cf13f86ce2d166382ccd93acd1b0
コメント
コメントを投稿するにはログインして下さい。