Share to: share facebook share twitter share wa share telegram print page

呼出規約

呼出規約(よびだしきやく)ないし呼出慣例(よびだしかんれい)(: calling convention)は、コンピュータ命令セットアーキテクチャごとに取り決められるABIの一部で、サブルーチンが呼出される際に従わねばならない制限などの標準である。名前修飾について、データを渡す「実引数」、戻るべきアドレスである「リターンアドレス」、データを戻す「返戻値」などを、スタックなどに対してどのように格納するのか、また各レジスタを、呼び出し側とサブルーチンのどちらの側が保存するか、等といった取決めの集まりである。言語が同じでも、分割コンパイルされリンカでリンクされる相互のプロシージャ間では、呼出し呼出されるならば同一の呼出規約に従っていなければならない。一方で、違う言語の間でも、同一の呼出規約を経由して相互にプロシージャを呼出すこともできる。

cdecl

インテルx86ベースのシステム上のC/C++では cdecl 呼出規約が使われることが多い。cdeclでは関数への引数は右から左の順でスタックに積まれる。関数の戻り値は EAX(x86のレジスタの一つ)に格納される。呼び出された側の関数ではEAX, ECX, EDXのレジスタの元の値を保存することなく使用してよい。呼び出し側の関数では必要ならば呼び出す前にそれらのレジスタをスタック上などに保存する。スタックポインタの処理は呼び出し側で行う。

例えば、以下のCプログラムの関数呼び出しは、

int function(int, int, int);
int a, b, c, x;
...
x = function(a, b, c);

以下のような機械語を生成する(MASMにおけるx86アセンブリ言語で記述する)。

push c
push b
push a
call function
add esp, 12 ;スタック上の引数を除去
mov x, eax

Cソースコードにてa, b, cの順で記述された引数は、逆の順序c, b, aでスタックに積まれ、call命令でリターンアドレスをスタックに積んだ上でサブルーチンにジャンプする。戻った後に呼び出し側がスタック上の引数データを除去する。

cdecl は通常 x86 Cコンパイラのデフォルトであるが、他のコンパイラも(Delphiを含むPascalコンパイラ等)、cdecl に呼出規約を変更するオプションを持っている。手動で操作するには、例えば、

void _cdecl function(params);

とする。_cdeclはプロトタイプ宣言部ないし関数宣言部に書く必要がある。

OS/2上の Virtual Pascal での例を挙げると、

function MyWndProc(h : HWND; m : ULONG; mp1 : MPARAM; mp2 : MPARAM) : MRESULT; CDECL;

のようにCDECL指令を付ける。このコンパイラは通常下記の Pascal 呼出規約を用いるが、この関数はOS/2のウィンドウシステム (Presentation Manager) から呼ばれるため、システム側に合わせてcdecl呼出規約に変更する必要がある。

Pascal

Pascal 呼出規約はcdeclの逆である。引数は左から右への順序でスタックに積まれ、スタック上の引数データを除去するのは呼ばれた側(サブルーチン側)である。Cと異なり、引数の個数と型がサブルーチン宣言時点で完全に固定であるため、スタック上の引数データのバイト数はサブルーチン内部で判明している。引数データの除去は実際にはサブルーチンからのリターン時のスタックポインタの調整で行われ、x86では"RET n"の一命令で実行可能である。サブルーチンのコードサイズはわずかながら増えることが多いが、呼び出し側でスタックを処理するコードが不要になるため全体としてはわずかながらコードのサイズが小さくなることが多い。本来呼出規約はプログラミング言語とは独立した概念だが、プログラミング言語の影響を受ける例である。

Lisa及び初期のMacintoshのシステム、アプリケーションはPascalで記述されたため、以前のMacintosh Toolboxを利用するにはPascal呼出規約を用いる必要があった。

呼出規約と言語仕様

言語仕様と呼出規約は独立した概念だが、上記の例とは逆に、呼出規約が実装レベルで言語仕様に影響を与える場合がある。fooとbarがどちらも関数であるとし、foobar(foo, bar) のように複数の引数をとる関数呼び出しがあるとする。Pascal 呼出規約では引数を左から右に格納するので、fooを先に、barを後に評価するのが自然で効率が高い。cdeclでは逆にbarを先にfooを後に評価すると自然である。これらの呼出規約に沿って「自然に」コンパイラを実装すると、言語仕様を実装レベルで決定してしまうことになる。

例えば次のPascalコードを考える。

var i : Integer;
...
function foo : Boolean;
begin
    foo := i > 0;
    i := 1
end;

function bar : Boolean;
begin
    bar := i > 0;
    i := -1
end;

function foobar(f, b : Boolean) : Boolean;
begin
    foobar := f < b
end;

begin
   i := 1;
   write(foobar(foo, bar))
end

関数呼び出し foobar(foo, bar) を行う際引数のfooとbarのどちらを先に評価するかで、結果が変わる。fooを先に評価すると変数 i はfoo呼び出し後にも1であるので、fooは真、barも真、foobar(foo, bar)は偽となる。barを先に評価すると、barは真だが、呼び出し後に i は-1になるため、fooは偽となり、foobar(foo, bar)は真となる。なお、標準Pascalでは引数を評価する順序を規定しておらず、結果が引数の評価順に依存するプログラムは良くない例だとされる。この例は良くない例である。

Register (fastcall)

レジスタ呼出規約または fastcall、レジスタ渡し が意味するものは、歴史的な事情からコンパイラ依存であるが、総じて次のようなものである。プロセッサのレジスタのビット幅と個数に合わせて、最初のいくつかの引数を(スタックではなく)レジスタに格納し、サブルーチンに渡す。残りの引数は cdecl と同様に右から左の順にスタックに積まれる[1]。戻り値はAL, AX, EAXレジスタに格納する。関数の引数の個数が可変でない場合は、スタックからの引数データの除去は(Pascal 呼出規約のように)サブルーチン側で行うのが通例である。しかしながら、ほとんどのランタイムライブラリサブルーチンはわずかな個数の引数しか取らないため、スタックにデータを積む必要は(従ってスタックを清掃する必要も)全くない。メモリよりレジスタの方が読み書きが速いため、レジスタ渡しは効率が高い。

レジスタ渡しの例

  • マイクロソフト(以下MS) __fastcall[1][2] 呼出規約(別名 __msfastcall)では、最初の2つの引数をECXおよびEDXに格納する。
  • ボーランド __fastcall[3] 呼出規約では、最初の3つの引数をEAX, EDX, ECXに格納する。
  • Watcom __fastcall 呼出規約では、最初の4つの引数をEAX, EDX, EBX, ECXに格納する。

Watcom C/C++ コンパイラでは #pragma aux[4] コンパイラ指令を用いて、プログラマが自前の呼出規約を指定することができる。取扱説明書によると、「"Very few users are likely to need this method, but if it is needed, it can be a lifesaver"(引用部分につき訳さない。ほとんど不要だが、いざという時には必要なものである、程度の意)」だそうである。

stdcall

MS stdcall [5]Windows APIで利用されている。引数は右から左に渡される。呼び出された側の関数ではEAX, ECX, EDXのレジスタの元の値を保存することなく使用してよい。呼び出し側の関数では必要ならば呼び出す前にそれらのレジスタをスタック上などに保存する。返り値はEAXに格納する。cdeclと異なり、スタックの清掃はサブルーチン側で行う(Pascal 呼出規約と同様)。ただし引数リストが可変長の場合は呼び出し側がスタックの後始末を行う。


safecall

WindowsでのDelphiは、safecallという呼出規約を用いてCOMのエラー処理をカプセル化している。COM/OLE の要求に沿って、例外が呼び出し側に漏れ出すことがなく、戻り値HRESULTに報告される。Delphiのコードからsafecallを行うと、自動的にHRESULTがチェックされ、必要なら例外が発生する。

thiscall

この規約はC++のメンバ関数を呼ぶのに用いられる。二つの主たる版があり、コンパイラ及び、関数が可変長の引数リストを用いるかによっていずれかが用いられる。

GCC (GNUコンパイラコレクション)では、thiscall はほとんど cdecl と同様である。呼び出し側がスタックを清掃し、引数は右から左の順で渡される。相違点は全ての引数を渡した後、最後に this ポインタがスタックに積まれることである。これは Windows で可変引数を用いるときの thiscall と類似している。

Windowsの場合は、関数が可変引数を取らない場合、引数は右から左に渡され、thisポインタはECXレジスタに格納される。スタックを清掃するのは呼ばれた側である。

thiscallがキーワードではないため、thiscallを明示的に指定することはできないが、IDAのような逆アセンブラではそれを指定する必要がある。そのため、__thiscall__というキーワードが用意されている。

clrcall

未稿

vectorcall

浮動小数点型、__m128型、およびそれらのうち同一の型を最大4つメンバーに持つ複合型はXMM0:XMM5に、__m256型およびその複合型はYMM0:YMM5を用いる。収まらなかった分は整数型としてポインタ渡しされる。返り値はEAX、EDX:EAX、XMM0:XMM3、YMM0:YMM3のいずれかに格納される。 それ以外はマイクロソフト fastcallに準ずる。

Intel ABI

インテルアプリケーションバイナリインタフェース (ABI) はほとんどのコンパイラが採用している呼出規約である。呼び出された側の関数ではEAX, ECX, EDXのレジスタの元の値を保存することなく使用してよい。呼び出し側の関数では必要ならば呼び出す前にそれらのレジスタをスタック上などに保存する。

x86-64

マイクロソフト x64呼出規約

x64呼出規約はx86-64(AMD64) / EM64Tで追加されたレジスタ空間を有益に利用する。RCX, RDX, R8, R9レジスタは整数型とポインタ型の引数に、XMM0, XMM1, XMM2, XMM3は浮動小数点型引数に用いられる。残りの引数はスタックに置かれる。__m128型はXMMレジスタを使わずスタックに置かれ整数型としてポインタ渡しされる。呼び出された側の関数ではRAX, RCX, RDX, R8, R9, R10, R11, XMM0:XMM5のレジスタの元の値を保存することなく使用してよい。呼び出し側の関数では必要ならば呼ぶ出す前にそれらのレジスタをスタック上などに保存する。レジスタが足りなくなれば、スタックが用いられる。返り値はRAXかXMM0に格納される。スタックポインタの処理は呼び出し側で行う。

vectorcall

浮動小数点型、__m128型、およびそれらのうち同一の型を最大4つメンバーに持つ複合型はXMM0:XMM5に、__m256型およびその複合型はYMM0:YMM5を用いる。収まらなかった分は整数型としてポインタ渡しされる。返り値はRAX、XMM0:XMM3、YMM0:YMM3のいずれかに格納される。 それ以外はマイクロソフト x64呼出規約に準ずる。

System V AMD64 ABI 呼出規約

LinuxBSDmacOSなどマイクロソフトのオペレーティングシステム (OS) 以外で利用されている。

  • 整数・ポインタ引数 - RDI, RSI, RDX, RCX, R8, R9
  • 浮動小数点引数 - XMM0, XMM1, XMM2, XMM3, XMM4, XMM5, XMM6, XMM7
  • 戻り値 - RAX
  • システムコールでは RCX の代わりに R10 を使用
  • レジスタだけでは引数の数が不足する場合はスタックを利用。

標準サブルーチンプロローグ/エピローグ

サブルーチンに制御が渡った点で、標準的には次のような処理を行う:

_function:
    push ebp       ;ベースポインタを保存
    mov ebp, esp   ;現在のスタックフレームを指すようベースポインタを変更
    sub esp, x     ;局所変数(Cでいう自動変数)の大きさの分スタックポインタを減らす

この結果、EBPはスタック上の引数の頭を指し、局所変数を格納する領域をスタック上に確保することができる。元のEBPの値はスタックに保存されている。このように局所変数は引数と同様にサブルーチン呼び出し毎にスタック上に確保されるので、再帰呼び出しが可能になる。

このサブルーチンから抜け出す際は、次のシーケンスを実行する:

    mov esp, ebp   ;局所変数を除去
    pop ebp        ;ベースポインタを復帰
    ret            ;サブルーチンから戻る

これはcdeclの例であって、Pascal 呼出規約では次のように引数データをサブルーチン側が掃除する。

    ret n          ;nは引数データのバイト数

次のC関数は

int _cdecl MyFunction(int i) { 
    int k;
    return i + k;
}

次のアセンブラコードと同等である。

    ;エントリシーケンス
    push ebp
    mov ebp, esp
    sub esp, 4     ;整数型変数kの領域に相当

    ;関数のコード
    mov eax, [ebp + 8] 
                   ;引数iを加算器(アキュムレータ)に移動
    add eax, [ebp - 4]
                   ;kをiに加える
                   ;結果はEAX(加算器)に残る

    ;終了シーケンス
    mov esp, ebp
    pop ebp
    ret

脚注・出典

  1. ^ ボーランド __fastcall 呼出規約では、残りの引数は Pascal 呼出規約 と同様にからの順にスタックに積まれる。変数修飾子 - RAD Studio XE2”. Delphi® XE2 および C++Builder® XE2 オンライン ヘルプ. Embarcadero Technologies, Inc. (2012年4月30日). 2013年1月22日閲覧。

関連項目

外部リンク

Kembali kehalaman sebelumnya