C/calling-conversion-ia16

16ビットDOS(x86)用Cコンパイラで使われる呼び出し規約(calling conversion)についてのメモになるかと思われます。 正確性についてはぶっちゃけ怪しいので詳しい人教えてください…

__cdeclや__pascalなどのcalling conversionはMicrosoft以外のコンパイラにも実装されているが、特に32ビット超の値(struct/union)や浮動小数点数を戻り値にした場合、コンパイラメーカーごとに実装に差異があるので、素直にポインタ経由でやり取りするほうが無難。 __fastcall用の関数をアセンブラで書くつもりなら、事前にコンパイラの-Sオプションで呼び出し元の引数のレジスタ割り当てを確認しておいたほうがいいと思う…

Microsoft C++

たぶん Microsoft C/C++ 7.0 や Visual C++ 1.xあたりの話。

アンダースコアがふたつつく修飾子 __cdecl, __pascal, __fastcall は MS-C7 以上でサポートされている。MS-C6 およびそれ以前はアンダースコアひとつの_cdecl, _pascal、アンダースコアなしの cdecl, pascalがサポートされているが、C7以上での使用は推奨されていない。

_fastcall はMS-C6以上でサポート(少なくともMS-C 5.1ではサポートされていないそうです)。 アンダースコアなしの fastcallは予約語ではない。


保存が必要なレジスタ

SI,DI,BP,DS,SS そしてflags中のDFを保存しなければならない(関数内でCLD/STD命令などを使用してDFを変更する場合はflagsの保存と復元が必要となる)。 MS-Cの場合、関数内で暗黙のメモリコピーを行う際にDFを設定せずにmovsw命令が生成されるので、DF=0であることが暗黙の前提になっている気がする…。

関数の戻り値

基本的に8ビット→AL、16ビット→AX、32ビット→DX:AXとなる。 32ビット以下のstructやunionの値返しもレジスタとなる。 32ビットを超える値や浮動小数点値の場合はcalling conversionによって異なる。


__cdecl

オプション無指定時のデフォルト。

  • 引数を右から左の順でスタックに積む。
  • floatは2ワード(4バイト)、doubleは4ワード(8バイト)、long doubleは5ワード(10バイト)のようだ。
  • 引数として積んだ分のスタックは呼び出し側が戻す。
  • シンボル名の先頭にアンダースコア _ がつく。
  • 32ビット超の値(浮動小数点以外)を返す場合、戻り値を保存する領域を関数側でメモリ内に確保しておき、そのポインタをAXもしくはDX:AXに返す。
  • floatとdoubleの戻り値は結果をグローバル変数 __fac に保存し、そのポインタをAXもしくはDX:AXに返す。__facの実体はランタイムライブラリ内で宣言されている(QWORD)ので関数側での確保は不要。
  • long doubleの戻り値はFPUの先頭スタックに返される。結果をメモリに保存せず、当然ポインタも返さない。

__pascal, __fortran
  • 引数を左から右の順でスタックに積む。可変長引数には対応しない。
  • 引数(と戻り値の保存域へのポインタ)として積んだ分のスタックは関数側が戻す。
  • シンボル名がすべて大文字化される。
  • 戻り値が32ビット超、もしくは浮動小数点数の場合、引数をスタックに積んだ後、戻り値を保存するための領域へのnearポインタをスタック上に1ワード積む。戻り値としてその領域へのポインタをDX:AXに返す。(MS-Cの場合、この領域は呼び出し元がスタック上に確保する。常にnearポインタで、関数側ではBXレジスタ経由で領域にアクセスするが、戻り値のDXにはSSレジスタの値が代入されるため、メモリモデルに関わらずDS==SSが前提になっているものと思われる)

__fastcall
  • 整数値もしくはnearポインタの引数を、なるべくレジスタ(AX,DX,BX)渡しにする。
  • 基本は左の引数からAX(AL)→DX(DL)→BX(BL)の順:f(int r_ax, int r_dx, int r_bx)
  • ただし、最初に出てきたnearポインタには優先的にBXが割り当てられる:f(void __near *r_bx, int r_ax, int r_dx)
  • 32ビット整数値にはDX:AXが優先的に割り当てられる。ただし、すでにAXが割り当て済の場合はすべてスタックに積まれる:f(long r_dxax, int r_bx)、f(int r_ax, long stack0, int r_dx, int r_bx)
  • 値渡しのstruct、union、farポインタ、浮動小数点数はすべてスタックに積まれる。
  • レジスタ割り当てされなかった引数はスタックに左から右の順で積む。可変長引数には対応しない。
  • 引数(と戻り値の保存域へのポインタ)として積んだ分のスタックは関数側が戻す。
  • シンボル名の先頭に @ がつく。
  • 戻り値が32ビット超の場合、戻り値を保存するための領域へのnearポインタをスタック上に1ワード積み、その後に引数をスタックに積む。戻り値としてその領域へのポインタをDX:AXに返す。(MS-Cの場合、この領域は呼び出し元が_BSSセグメント内に確保する。常にnearポインタで、関数側ではBXレジスタ経由で領域にアクセスするが、戻り値のDXにはSSレジスタの値が代入されるため、メモリモデルに関わらずDS==SSが前提になっているものと思われる)
  • 浮動小数点値の戻り値はFPUの先頭スタックに返される。結果をメモリに保存せず、当然ポインタも返さない。

ちなみに32ビットx86の場合、16ビットとレジスタ割り当てがまったく違う点に注意(参考:https://learn.microsoft.com/cpp/cpp/fastcall

Turbo C/C++, Borland C++

Turbo C 1.0の時点でcdecl, pascalの修飾子がサポートされている(fortranは未サポート)。 先頭にアンダースコアがひとつつく_cdecl, _pascalはBorland C++ 2.0から、ふたつつく__cdecl, __pascalはBorland C++ 3.1からサポートされている(3.0のサポート状況は未確認)。

_fastcall, __fastcallはBorland C++ 2.0時点では未対応。3.1からサポートされている(3.0のサポート状況は未確認)。 アンダースコアなしのfastcallは予約語ではない。

浮動小数点数や32ビット超のstruct/unionを戻り値にした場合の扱いがMicrosoftと異なる点に注意。

cdecl

オプション無指定時のデフォルト。

  • 戻り値が32ビット超の場合、引数をスタックに積んだ後、戻り値を保存するための領域へのfarポインタをスタック上に2ワード積む。戻り値としてその領域へのポインタをDX:AXに返す。保存領域は呼び出し側が_BSSセグメント上に確保する。(Microsoftの_cdeclと異なり、戻り値の保存領域へのfarポインタをスタックに積むため、va_arg系の処理が未対応の可能性がある…)
  • 浮動小数点値の戻り値はFPUの先頭スタックに返される。結果をメモリに保存せず、当然ポインタも返さない。

pascal
  • 戻り値が32ビット超の場合、戻り値を保存するための領域へのfarポインタをスタック上に2ワード積み、その後に引数をスタックに積む(ポインタサイズと積む順番がMicrosoftの_pascalと異なる)。戻り値として保存領域へのポインタをDX:AXに返す。保存領域は呼び出し側が_BSSセグメント上に確保する。
  • 浮動小数点値の戻り値はFPUの先頭スタックに返される。結果をメモリに保存せず、当然ポインタも返さない。

__fastcall
  • 戻り値が32ビット超の場合、戻り値を保存するための領域へのfarポインタをスタック上に2ワード積み、その後に引数をスタックに積む。戻り値としてその領域へのポインタをDX:AXに返す。保存領域は呼び出し側が_BSSセグメント上に確保する。
  • 浮動小数点値の戻り値はFPUの先頭スタックに返される。結果をメモリに保存せず、当然ポインタも返さない。

Watcom C++ (OpenWatcom)

OpenWatcom 1.x、2.0pre版あたりの話。 商用時代のWatcom C/C++ 11.0も含まれるかも。

C/C++ユーザーズガイドによると、16ビットコンパイラでは__cdecl, __pascal, __watcallがサポートされている。 ユーザーズガイドに記述がないが __fastcallにも対応している。また、32ビットコンパイラにサポート記述がある __syscall, __stdcallも一応受け付ける。

アンダースコアのないcdecl, pascal, アンダースコア一つの _cdecl, _pascal, _fastcall, _syscall, _stdcallも使用可能(アンダースコア一つの _watcallはサポートされない)。

Microsoft/Borlandとは異なり、long doubleは64ビットとなる。

16ビット向けコンパイラでも引数、戻り値として64ビット整数が使える。 32ビット整数値とはレジスタ割り当てがまったく異なる点に注意。

関数の戻り値

基本的に8ビット→AL、16ビット→AX、32ビット→DX:AX、64ビット(整数)→AX:BX:CX:DXとなる。64ビット整数はAXが最上位ワード、DXが最下位ワードになる。

32ビット超のstruct/unionや浮動小数点数を戻り値にした場合の扱いはどちらかというとMicrosoftに近い。タイニー/スモール/ミディアムモデルでは戻り値用に確保した領域にDSでアクセスし、コンパクト/ラージ/ヒュージモデルではSSでアクセスするコードが生成されている。


cdecl
  • 戻り値が32ビット超の場合、戻り値を保存するための領域へのnearポインタをスタック上に1ワード積み、その後に引数をスタックに積む。戻り値としてその領域へのnearポインタをAXに返す(DXにセグメントを設定しない)。 保存領域は呼び出し側がDGROUPに属するセグメント上に確保する(struct/unionは_BSS、浮動小数点数はなぜかCONST内に確保されるようだ)。

(以下、書きかけ)