Help us understand the problem. What is going on with this article?

Delphi と Windows の呼び出し規約

はじめに

Delphi の Windows ターゲットプラットフォームでの呼び出し規約を調べてみました。

呼び出し規約 (Calling Conventions)

Delphi (Win32) で使える呼び出し規約は次の通りです。デフォルトは register 呼び出し規約です。

指令 パラメータの
順序 (方向)
クリーンアップの
担当
レジスタ経由の
パラメータ渡し
register 左から右 ルーチン
pascal 左から右 ルーチン ×
cdecl 右から左 呼び出し側 ×
stdcall 右から左 ルーチン ×
safecall 右から左 ルーチン ×

16bit Delphi (Delphi 1) には pascalcdecl の他に次のような指令がありましたが、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 コンソールアプリケーションを用意します。

CallingConversionTest.dpr
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 ウィンドウ | 逆アセンブル] で出す事もできます。
image.png
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 からの抜粋です。

WINDOWS.H(Windows3.0)
...
#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);
...

WINDOWS.H(Windows3.1)
...
#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);
...
WINDEF.H(Windows95)
#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 用に次のようなコンソールアプリケーションを用意します。

File1.cpp
#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 があちこちに出てくるのはこれが理由です。
image.png

■ 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:


  1. Windows 3.0 では "API" キーワード (それより前の Windows では未確認) が far pascal で定義されていました。 

ht_deko
とある熊本の障害復旧(トラブルシューター)
https://ht-deko.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away