はじめに
Delphi の Windows ターゲットプラットフォームでの呼び出し規約を調べてみました。
呼び出し規約 (Calling Conventions)
Delphi (Win32) で使える呼び出し規約は次の通りです。デフォルトは register 呼び出し規約です。
指令 | パラメータの 順序 (方向) |
クリーンアップの 担当 |
レジスタ経由の パラメータ渡し |
---|---|---|---|
register | 左から右 | ルーチン | ○ |
pascal | 左から右 | ルーチン | × |
cdecl | 右から左 | 呼び出し側 | × |
stdcall | 右から左 | ルーチン | × |
safecall | 右から左 | ルーチン | × |
16bit Delphi (Delphi 1) には pascal と cdecl の他に次のような指令がありましたが、32bit / 64bit コンパイラでは無視されます。
指令 | 意味 |
---|---|
near | near 呼び出しモデルを指定する。プログラムで宣言されたルーチンや、ユニットの implementation セクションで宣言されたルーチンは暗黙的に near となる。far 呼び出しモデルよりもやや高速に動作する。 |
far | far 呼び出しモデルを指定し、他のモジュール (ユニット) からの呼び出しを可能とする。ユニットの interface セクションで宣言されたルーチンは暗黙的に far となる。手続き型 (関数ポインタ) を使うルーチンは far 呼び出しモデルでなくてはならない。 |
export | DLL でエクスポートされるルーチンやコールバックルーチンに指定する必要がある。far 呼び出しモデルを強制する。 |
少し話は逸れますが、Delphi 1 には interrupt 指令もありました。これは割り込み処理用なのですが、Delphi 1 で実際に使えたかどうかは確認していません。次のような 割り込み手続き
で利用されました。
procedure IntHandler(Flags, CS, IP, AX, BX, CX, DX, SI, DI, DS, ES, BP: word); interrupt;
begin
...
end;
interrupt 指令は Delphi 2 以降でエラーになります。
呼び出し規約の違いを確認する
呼び出し規約の違いを確認してみましょう。まずは次のような Windows 32 bit コンソールアプリケーションを用意します。
program CallingConversionTest;
{$APPTYPE CONSOLE}
function Calc(a: Integer; b: Integer; c: Integer): Integer;
begin
Result := a + b + c;
end;
var
v: Integer;
begin
v := Calc(1, 2, 3);
Writeln(v);
end.
v := Calc(1, 2, 3);
の行にブレークポイントを置き、デバッグ実行します。
ブレークポイントで止まったら 〔Ctrl〕+〔Alt〕+〔D〕
を押して逆アセンブルリストを出します。メインメニューから [表示 | デバッグ | CPU ウィンドウ | 逆アセンブル] で出す事もできます。
See also:
■ register 呼び出し規約
function Calc(a: Integer; b: Integer; c: Integer): Integer; register;
ルーチンの末尾に register を指定するか何も指定しなければ register 呼び出し です。
CallingConversionTest.dpr.14: v := Calc(1, 2, 3);
0040B100 B903000000 mov ecx,$00000003
0040B105 BA02000000 mov edx,$00000002
0040B10A B801000000 mov eax,$00000001
0040B10F E8C4ECFFFF call Calc
0040B114 A388054100 mov [$00410588],eax
引数が ecx, edx, eax レジスタに格納されています。パラメータが 4 個以上だとそれらは左から順にスタックに積まれます。
CallingConversionTest.dpr.14: v := Calc(1, 2, 3, 4, 5);
0040B100 6A04 push $04
0040B102 6A05 push $05
0040B104 B903000000 mov ecx,$00000003
0040B109 BA02000000 mov edx,$00000002
0040B10E B801000000 mov eax,$00000001
0040B113 E8C0ECFFFF call Calc
0040B118 A388054100 mov [$00410588],eax
パラメータ | レジスタ/ スタック |
---|---|
1 | eax |
2 | edx |
3 | ecx |
4 | スタック #1 |
5 | スタック #2 |
■ pascal 呼び出し規約
function Calc(a: Integer; b: Integer; c: Integer): Integer; pascal;
下位互換性のために残されている呼び出し規約です。
CallingConversionTest.dpr.14: v := Calc(1, 2, 3);
0040B100 6A01 push $01
0040B102 6A02 push $02
0040B104 6A03 push $03
0040B106 E8CDECFFFF call Calc
0040B10B A388054100 mov [$00410588],eax
パラメータ | レジスタ/ スタック |
---|---|
1 | スタック #1 |
2 | スタック #2 |
3 | スタック #3 |
引数が左から順にスタックに積まれます。ドキュメントに
register 呼び出し規約と pascal 呼び出し規約の場合、評価順序は定義されていません。
と書いてありますが、ここで言う 評価順序
はスタックに積まれる順序 (方向) の事ではありません。
16bit の Windows API (Win16 API) は (far) pascal 呼び出し規約 でした。次のコードは Windows 3.x の WINDOWS.H
と Windows 95 の WINDEF.H
からの抜粋です。
...
#define FAR far
#define NEAR near
#define PASCAL pascal
#define API far pascal
#define CALLBACK far pascal
...
BOOL API PostMessage(HWND, WORD, WORD, WORD);
LONG API SendMessage(HWND, WORD, WORD, WORD);
...
...
#define FAR _far
#define NEAR _near
#define PASCAL _pascal
#define CDECL _cdecl
#define WINAPI _far _pascal
#define CALLBACK _far _pascal
...
typedef UINT WPARAM;
typedef LONG LPARAM;
typedef LONG LRESULT;
...
BOOL WINAPI PostMessage(HWND, UINT, WPARAM, LPARAM);
LRESULT WINAPI SendMessage(HWND, UINT, WPARAM, LPARAM);
...
#if (_MSC_VER >= 800) || defined(_STDCALL_SUPPORTED)
#define CALLBACK __stdcall
#define WINAPI __stdcall
#define WINAPIV __cdecl
#define APIENTRY WINAPI
#define APIPRIVATE __stdcall
#define PASCAL __stdcall
#else
#define CALLBACK
#define WINAPI
#define WINAPIV
#define APIENTRY WINAPI
#define APIPRIVATE
#define PASCAL pascal
#endif
古い MacOS のライブラリ (68K / PowerPC) も pascal 呼び出し規約 でした。次の文は Apple の『Building and Managing Programs in MPW (Second Edition)』からの引用です。
Remove the pascal Keyword
In the PowerPC runtime environment, Pascal and C calling conventions are identical. If you declare a function with the pascal keyword, the PowerPC compiler uses the same calling convention as if you had declared it without the pascal keyword. In PowerPC compilers, function parameters are pushed onto the stack from left to right.
■ cdecl 呼び出し規約
function Calc(a: Integer; b: Integer; c: Integer): Integer; cdecl;
C / C++ のデフォルトの呼び出し規約です。Windows を除く、多くの OS のライブラリはこの cdecl 呼び出し規約 です。引数は右から順にスタックに積まれます。
CallingConversionTest.dpr.14: v := Calc(1, 2, 3);
0040B100 6A03 push $03
0040B102 6A02 push $02
0040B104 6A01 push $01
0040B106 E8CDECFFFF call Calc
0040B10B 83C40C add esp,$0c
0040B10E A388054100 mov [$00410588],eax
パラメータ | レジスタ/ スタック |
---|---|
1 | スタック #3 |
2 | スタック #2 |
3 | スタック #1 |
cdecl 呼び出し規約 ではスタックポインタ (ESP) の後始末を呼び出し側で行う必要があります。パラメータが 5 つになるとこのようなコードになります。
CallingConversionTest.dpr.14: v := Calc(1, 2, 3, 4, 5);
0040B100 6A05 push $05
0040B102 6A04 push $04
0040B104 6A03 push $03
0040B106 6A02 push $02
0040B108 6A01 push $01
0040B10A E8C9ECFFFF call Calc
0040B10F 83C414 add esp,$14
0040B112 A388054100 mov [$00410588],eax
■ stdcall 呼び出し規約
function Calc(a: Integer; b: Integer; c: Integer): Integer; stdcall;
32bit の Windows API (Win32 API) の殆どがこの stdcall 呼び出し規約 です。つまり Windows NT 3.1 から Windows API は stdcall 呼び出し規約になった事になります。
言語を問わず、DLL で外部公開するルーチンは stdcall にしておけばトラブルも少ないと思います。
CallingConversionTest.dpr.14: v := Calc(1, 2, 3);
0040B100 6A03 push $03
0040B102 6A02 push $02
0040B104 6A01 push $01
0040B106 E8CDECFFFF call Calc
0040B10B A388054100 mov [$00410588],eax
pascal 呼び出し規約の逆で、引数は右から順にスタックに積まれます。
パラメータ | レジスタ/ スタック |
---|---|
1 | スタック #3 |
2 | スタック #2 |
3 | スタック #1 |
winapi キーワードを用いると、ターゲットプラットフォームのデフォルトの呼び出し規約となるため、32bit Windows 環境では stdcall 呼び出し規約と同等になります。
■ safecall 呼び出し規約
function Calc(a: Integer; b: Integer; c: Integer): Integer; safecall;
例外ファイアウォールを持つ呼び出し規約です。COM で使われます。stdcall 呼び出し規約 とほぼ同じです。
CallingConversionTest.dpr.14: v := Calc(1, 2, 3);
0040B100 8D45EC lea eax,[ebp-$14]
0040B103 50 push eax
0040B104 6A03 push $03
0040B106 6A02 push $02
0040B108 6A01 push $01
0040B10A E8C1EDFFFF call Calc
0040B10F E8D4DDFFFF call @CheckAutoResult
0040B114 8B45EC mov eax,[ebp-$14]
0040B117 A388054100 mov [$00410588],eax
上記コードはテストとしては適当ではありません。吐かれるアセンブリコードがどういうものになるかの検証である事に留意してください。
パラメータ | レジスタ/ スタック |
---|---|
1 | スタック #3 |
2 | スタック #2 |
3 | スタック #1 |
safecall 呼び出し の関数は、
function AddSymbol(ASymbol: OleVariant): WordBool; safecall;
暗黙的に HRESULT 型の結果 (戻り値) を持っています。
function AddSymbol(ASymbol: OleVariant; out RetValue: WordBool): HRESULT; stdcall;
See also:
C++Builder との互換性
C++Builder を使って C++ との互換性を確認してみます。C++Builder 用に次のようなコンソールアプリケーションを用意します。
#pragma hdrstop
#pragma argsused
#ifdef _WIN32
#include <tchar.h>
#else
typedef char _TCHAR;
#define _tmain main
#endif
#include <stdio.h>
int Calc(int a, int b, int c, int d, int e) {
return a + b + c + d + e;
}
int _tmain(int argc, _TCHAR* argv[])
{
int v = Calc(1, 2, 3, 4, 5);
printf("%d\n", v);
return 0;
}
これを C++Builder の古いコンパイラ (BCC32) でコンパイルします。Clang ベースの BCC32C/X ではなく BCC32 を使うのは、吐かれるアセンブリコードが Delphi のものに似てシンプルだからです。
int v = Calc(1, 2, 3, 4, 5);
の行にブレークポイントを置き、デバッグ実行します。ブレークポイントで止まったら、Delphi の時と同じように 〔Ctrl〕+〔Alt〕+〔D〕
を押して逆アセンブルリストを出します。
■ cdecl 呼び出し規約 (cdecl、_cdecl、__cdecl)
int __cdecl Calc(int a, int b, int c, int d, int e)
何も指定しなかった場合や __cdecl
を指定した場合には、cdecl 呼び出し規約 でした。
File1.cpp.19: int v = Calc(1, 2, 3, 4, 5);
00401214 6A05 push $05
00401216 6A04 push $04
00401218 6A03 push $03
0040121A 6A02 push $02
0040121C 6A01 push $01
0040121E E8D9FFFFFF call Calc(int,int,int,int,int)
00401223 83C414 add esp,$14
00401226 8945FC mov [ebp-$04],eax
■ pascal 呼び出し規約 (pascal、_pascal、__pascal)
int __pascal Calc(int a, int b, int c, int d, int e)
__pascal
を指定した場合には、pascal 呼び出し規約 でした。
File1.cpp.19: int v = Calc(1, 2, 3, 4, 5);
00401218 6A01 push $01
0040121A 6A02 push $02
0040121C 6A03 push $03
0040121E 6A04 push $04
00401220 6A05 push $05
00401222 E8D5FFFFFF call Calc(int,int,int,int,int)
00401227 8945FC mov [ebp-$04],eax
Win16 API は (far) pascal 呼び出し、Win32 API は stdcall 呼び出しだったので、他の C/C++ コンパイラでは便宜上次のような定義をしている事があります。
#define PASCAL __stdcall
Win32 用の定義だけを見て**「pascal 呼び出し規約って stdcall 呼び出し規約と同じなんだな!」**と思ってはいけません。
Win16 | Win32 | |
---|---|---|
Borland の pascal 呼び出し規約 | スタック (左->右) (pascal) |
スタック (左->右) (pascal) |
他メーカー の pascal 呼び出し規約 | スタック (左->右) (pascal) |
スタック (右->左) (stdcall) |
「互換性をどう取ったか」によって実装が異なります。Pascal 系の言語を自社ラインナップに持っていれば、Borland のような対応が自然だったかと思います。
■ stdcall 呼び出し規約 (_stdcall、__stdcall)
int __stdcall Calc(int a, int b, int c, int d, int e)
__stdcall
を指定した場合には、stdcall 呼び出し規約 でした。
File1.cpp.19: int v = Calc(1, 2, 3, 4, 5);
00401218 6A05 push $05
0040121A 6A04 push $04
0040121C 6A03 push $03
0040121E 6A02 push $02
00401220 6A01 push $01
00401222 E8D5FFFFFF call Calc(int,int,int,int,int)
00401227 8945FC mov [ebp-$04],eax
■ Borland fastcall 呼び出し規約 (_fastcall、__fastcall)
int __fastcall Calc(int a, int b, int c, int d, int e)
__fastcall
を指定した場合には、register 呼び出し規約 でした。
File1.cpp.20: printf("%d\n", v);
00401224 6A04 push $04
00401226 6A05 push $05
00401228 B903000000 mov ecx,$00000003
0040122D BA02000000 mov edx,$00000002
00401232 B801000000 mov eax,$00000001
00401237 E8C0FFFFFF call Calc(int,int,int,int,int)
0040123C 8945FC mov [ebp-$04],eax
C++Builder で VCL / Firemonkey アプリケーションを作ると __fastcall があちこちに出てくるのはこれが理由です。
■ Microsoft fastcall 呼び出し規約 (_msfastcall、__msfastcall)
int __msfastcall Calc(int a, int b, int c, int d, int e)
__msfastcall
を指定した場合には、Microsoft 互換の fastcall 呼び出し規約 となりました。この呼び出し規約は Delphi には存在しません。
File1.cpp.19: int v = Calc(1, 2, 3, 4, 5);
00401224 6A05 push $05
00401226 6A04 push $04
00401228 6A03 push $03
0040122A BA02000000 mov edx,$00000002
0040122F B901000000 mov ecx,$00000001
00401234 E8C3FFFFFF call Calc(int,int,int,int,int)
00401239 8945FC mov [ebp-$04],eax
引数が edx, ecx レジスタに格納されています。パラメータが 3 個以上だとそれらは右から順にスタックに積まれます。
パラメータ | レジスタ/ スタック |
---|---|
1 | ecx |
2 | edx |
3 | スタック #3 |
4 | スタック #2 |
5 | スタック #1 |
BCC32 のコンパイラオプションには _fastcall
を Microsoft の fastcall 呼び出し規約 とみなすオプションがあります。
64bit Windows の場合
Windows 64bit プラットフォームの場合、Delphi では safecall 以外の指令を無視し、呼び出しに Microsoft x64 呼び出し規約 が使われます。
CallingConversionTest.dpr.13: v := Calc(1, 2, 3, 4, 5);
000000000040EA4F B901000000 mov ecx,$00000001
000000000040EA54 BA02000000 mov edx,$00000002
000000000040EA59 41B803000000 mov r8d,$00000003
000000000040EA5F 41B904000000 mov r9d,$00000004
000000000040EA65 C744242005000000 mov [rsp+$20],$00000005
000000000040EA6D E86EFFFFFF call Calc
000000000040EA72 890568A70000 mov [rel $0000a768],eax
Microsoft x64 呼び出し規約は 64bit 版の (ms)fastcall 呼び出し規約とでも言うべきもので、最初の 4 つはレジスタに格納され、残りは右から順にスタックに積まれます (実際にはもう少し複雑です)。
これにより、pascal 呼び出し規約も次のようになりました。
Win16 | Win32 | Win64 | |
---|---|---|---|
Embarcadero の pascal 呼び出し規約 | pascal | pascal (32) | Microsoft x64 |
他メーカー の pascal 呼び出し規約 | pascal | stdcall | Microsoft x64 |
WINAPI キーワード | far pascal 1 | stdcall | Microsoft x64 |
やろうと思えば スタック (左->右)
な 64bit の pascal 呼び出し規約も実装できたと思いますが、Win32 の時点でも pascal 呼び出し規約は (ほぼ) 使われていなかったので、もはやそうするメリットはなかったのだろうと思われます。
safecall 呼び出し規約もレジスタとスタックの使い方は同じなのですが、HRESULT があるのでちょっと違うアセンブリコードが吐かれます。
CallingConversionTest.dpr.13: v := Calc(1, 2, 3, 4, 5);
000000000040EABF B901000000 mov ecx,$00000001
000000000040EAC4 BA02000000 mov edx,$00000002
000000000040EAC9 41B803000000 mov r8d,$00000003
000000000040EACF 41B904000000 mov r9d,$00000004
000000000040EAD5 C744242005000000 mov [rsp+$20],$00000005
000000000040EADD 488D4538 lea rax,[rbp+$38]
000000000040EAE1 4889442428 mov [rsp+$28],rax
000000000040EAE6 E835FFFFFF call Calc
000000000040EAEB 89C1 mov ecx,eax
000000000040EAED E88EE1FFFF call @CheckAutoResult
000000000040EAF2 8B4538 mov eax,[rbp+$38]
000000000040EAF5 8905EDA60000 mov [rel $0000a6ed],eax
こちらもテストとしては適当でありません。吐かれるアセンブリコードがどういうものになるかの検証である事に留意してください。
おわりに
16bit / 32bit Windows プログラミングでは、呼び出し規約に注意する必要があります。
64bit Windows プログラミングではあまり気にする必要はなさそうですが、32bit / 64bit 共用のコードを書く際には呼び出し規約に注意しないといけませんね。
See also:
- 呼び出し規約 (DocWiki)
- 言語混在時の呼び出し規約 (DocWiki)
- 呼出規約 (Wikipedia)
- Windowsにおける呼出規約の歴史 (Owl's perspective)
- x64 での呼び出し規則 (docs.microsoft.com)
- x86 calling conventions (Wikipedia: en)
-
Windows 3.0 では "API" キーワード (それより前の Windows では未確認) が
far pascal
で定義されていました。 ↩