原文:
Windows上のC言語プログラミングで、多くの開発者は「呼び出し規約」(calling convention)に注意を払わない。しかしコードの規模が大きくなるに従いソースコードをモジュールに分割する時、特に外部のライブラリやDLLを使う時、呼び出し規約は無視出来なくなる。
この記事では、特にMicrosoftのVisualC++コンパイラ(MSVC)でサポートする呼び出し規約と、呼び出し規約を適切に選択する理由、より大きなソースコードで注意すべきポイントについて検討する。
伝統的なC言語の関数呼び出しは、次の手順で実現される。
/* example of __cdecl */ push arg1 push arg2 push arg3 call function add sp,12 // "pop; pop; pop"と同義
Windows上のMicrosoft社のコンパイラは上のような呼び出し規約だけではなく、後で見るように複数の呼び出し規約をサポートしている。
上のような呼び出し規約は、__cdeclとして知られている。
他の呼び出し規約で有名なのは__stdcallがある。呼び出し側で引数群をスタックにpushするところは同じだが、スタックのクリーンアップは呼ばれる側(calee)で実行される。こちらがWin32API関数群の標準的な規約で、windows.h内でWINAPIマクロとして定義されている。また、"Pascal"呼び出し規約と呼ばれる場合もある。
/* example of __stdcall */ push arg1 push arg2 push arg3 call function // 呼び出し側ではスタックをクリーンアップしない。
スタックのクリーンアップをどちらで行うのかは、僅かな差に見えるが、呼ぶ側・呼ばれる側が同じ規約に従っていないとスタックの内容が破壊されてしまう。
プログラムの実行時に呼び出し規約のミスマッチが発生すると、プログラムのクラッシュなど破壊的な結果につながる。
他にも__fastcallという呼び出し規約を取りあげるが、これについては一般的な場面ではそれほど使わないと考えている。レジスタでの引数渡しは、レジスタの保存と復元が必要で速度上のメリットは得られないと考えている。
引数をスタックにpushするとき、右からpushするか左からpushするかどちらが「正しい」かについては明確になっていない。コンパイラの作者の好みで順番が決まるが、大体の場合はマシンアーキテクチャに依存する。
著者らの意見では、スタックがアドレスの低位方向に伸びていく場合は右から左へpushしていき、アドレスの高位方向に伸びていく場合は左から右へpushしていくように思われる。
プログラマが明示的に指定出来る呼び出し規約と異なり、引数のpushする順番はコンパイラだけが決定し、本記事で検討する呼び出し規約のミスマッチとは関係ない。もし異なるアーキテクチャに移植が容易なコードを書きたい場合は、引数のpush順序に依存するコードは絶対に書いてはならない。
上の箇条書きで示したように、呼び出し規約のミスマッチは致命的である。Microsoftはミスマッチを回避する為のメカニズムを提供しており、うまく動いてくれるのだが、理由を知らない人には何故そうなっているのか全く分からないようになっている。
このメカニズムは、リンカレベルで関数名に呼び出し規約に応じた接頭・接尾辞を修飾したシンボル名に変換する。デフォルトの呼び出し規約は __cdecl だが、"/G?" コンパイラオプションで明示的に変更出来る。
例:
宣言 | シンボル名 |
void __cdecl foo(void); | _foo |
void __cdecl foo(int a); | _foo |
void __cdecl foo(int a, int b); | _foo |
void __stdcall foo(void); | _foo@0 |
void __stdcall foo(int a); | _foo@4 |
void __stdcall foo(int a, int b); | _foo@8 |
void __fastcall foo(void); | @foo@0 |
void __fastcall foo(int a); | @foo@4 |
void __fastcall foo(int a, int b); | @foo@8 |
リンカーレベルでのシンボル名は、Cのソースコードの世界では意識する必要がない。コンパイラ・リンカの内部でシンボル名の処理が行われるので、呼び出し規約が異なる関数がリンクされる危険は回避される。
以下に、わざと未定義の関数をリンクさせることでリンカレベルでシンボル名がどう変換されているのか確認する例を示す。
C> type testfile.c extern void __stdcall func1(int a); extern void __stdcall func2(int a, int b, double d); extern void __cdecl func3(int b); extern void __cdecl func4(int a, int b, double d); int __cdecl main(int argc, char **argv) { func1(1); func2(2, 3, 4.); func3(5); func4(6, 7, 8.0); return 0; } C> cl /nologo testfile.c testfile.c testfile.obj : error LNK2001: unresolved external symbol _func1@4 ... __stdcall testfile.obj : error LNK2001: unresolved external symbol _func2@16 ... __stdcall testfile.obj : error LNK2001: unresolved external symbol _func3 ... __cdecl testfile.obj : error LNK2001: unresolved external symbol _func4 ... __cdecl testfile.exe : fatal error LNK1120: 4 unresolved externals
func2()については、引数が3つなのに4word(4x4=16byte)分の接尾辞がついて"_func2@16"になっている。これは3番目の引数がdoubleのため、2word分占めているためである。またfunc3(), func4()については __cdecl 呼び出し規約なので、引数の数はシンボル名には影響していない。
検出しない。
呼び出し規約によるシンボル名の修飾は、関数プロトタイプより狭い範囲の問題を解決する。
関数のプロトタイプ宣言はC++およびANSI-Cで導入された。以前のK&R-Cでは関数の戻り値だけが関数の宣言に含まれ、引数の数は無視された。C++およびANSI-C以降は引数も含めて関数の宣言とされ、これにより引数が異なる関数呼び出しはコンパイラレベルで警告を発するようになった。
/* somefile.c */ extern int foo(int a); // prototype ... n = foo(1, 2, 3); // mismatch! bad parameter count!
コンパイラはfoo()関数はint型の引数を一つ必要と判断し、引数がそれより少ない or 多い呼び出し、および引数の型が異なる呼び出しについて警告を発する。呼び出し規約に応じたシンボル名の修飾は、この段階では全く関係してこない。
リンカレベルで問題になってくるのは、二つ以上のソースファイルに関数の定義と、関数を呼び出すコードが分離された場合である。
関数を呼ぶ側:
/* in file1.c */ extern int __cdecl foo(int); ... n = foo(1);
関数の定義側:
/* in file2.c */ int __stdcall foo(int a) { .... }
コンパイラはfile1.cとfile2.cを同時にコンパイルすることは無い。そのため、コンパイラレベルでは二つのCソースの間での呼び出し規約のミスマッチを検出することは出来ない。もしシンボル名の修飾が行われず、このままリンクされてしまうとスタックの破壊を引き起こす。上の例で、file1.cで呼び出し規約の宣言を入れ忘れた場合も、コンパイラのデフォルト呼び出し規約である __cdecl が使われ、ミスマッチを引き起こす。
小さなプロジェクトでは、上のような例は明らかに意図的に作り出された状況であるが、大規模なプログラムでは意図せずに頻繁に発生するようになる。また外部のライブラリを使う場合も発生する。ライブラリの提供側が、どの呼び出し規約でコンパイルしたのか・コンパイルオプションの詳細などを伝えないケースは特に注意が必要である。
これらの理由が、リンカレベルで呼び出し規約に応じてシンボル名を修飾する理由である。プロトタイプ宣言により引数の数・型をチェックする機能とは関係ない。
重要:
呼び出し規約によるシンボル名の修飾は、スタック制御を維持するため「だけに」存在する。
殆どのケースでは、プログラム全体でのデフォルト呼び出し規約と特定の関数の呼び出し規約は変わらない。ただし、デフォルトで __cdecl 以外を使っている場合、幾つかの例外が存在する。
extern void __cdecl qsort( void *base, size_t num, size_t width, int (__cdecl *compare )(const void *, const void *) ); .... int __stdcall mycompare(const void *p1, const void *p2) { // compare here } .... qsort(base, n, width, mycompare); // ERROR - mismatch
なおあくまでも「パラメータの呼び出し規約」を合わせるのであって、qsort()それ自体の呼び出し規約は無関係である点に注意する。
例えば下の例はセーフである。
extern void __cdecl qsort( void *base, size_t num, size_t width, int (__stdcall *compare )(const void *, const void *) ); .... int __stdcall mycompare(const void *p1, const void *p2) { // compare here } .... qsort(base, n, width, mycompare);
callback関数を使う局面ではいずれにせよ呼び出し規約に注意する。他にもUNIXの場合、シグナルハンドラの呼び出し規約も注意の対象となるだろう。
ゼロからシステムを構築する場合、Makefileなどで呼び出し規約を調整することは自然な流れである。しかしサードパーティ製のDLLを使うようになると「ねじれ」が生じ始める。特に他の言語でコンパイルされたDLLの場合はなおさらである。
ヘッダーファイルやインポートライブラリが無く、DLLだけしか提供されていない場合はプログラム側で関数ポインタを取り出して呼び出し規約を指定することになり、コンパイラレベルでのサポートは望めない。
例として、バーコードを処理するBARCODE.DLLがあったとして、DLLが提供している関数を二つ、関数名から関数ポインタを取得して呼び出すシーンを考える:
typedef BOOL (__stdcall *INITFUNCTION)(BOOL); typedef int (__stdcall *DRAWFUNCTION)(int x, int y, const char *label); HINSTANCE hInst = LoadLibrary("barcode.dll"); INITFUNCTION pfInit = (INITFUNCTION)GetProcAddress(hInst, "Init"); DRAWFUNCTION pfDraw = (DRAWFUNCTION)GetProcAddress(hInst, "Draw"); (*pfInit)(TRUE); (*pfDraw)(1, 1, "12345"); (*pfDraw)(1, 2, "67890");
typedefで指定している __stdcall は、実際のDLL側と一致させなければならない。もし一致していないとしても、コンパイラ・リンカ共にそれを検出することは出来ず、実行時に判明することになる。
このような場合は、ライブラリの作者が提供しているドキュメントをチェックする以外の有効な代替策は無い。
※かなり意訳になってます。
これまで見てきたように、小さなプログラムでは無視出来るが、システムが大きくなるにつれ、あるいは外部ライブラリを使うようになると呼び出し規約に注意する必要が出てくる。Windows以外の、呼び出し規約の概念が無いプラットフォームへ移植する場合はさらに複雑になる。
外部ライブラリのソースコードがあったとしても、例えばUNIX上で主に構築されたパッケージはMakefileで構築され、一方のWindowsではVisualStudioのプロジェクトファイルを中心としたビルドシステムになっている。このため、一方をもう一方のビルドシステムに組み込むのは簡単ではない。
著者らのお奨めとしては可能であれば "__stdcall" を使うことだが、重要なのは「全ての関数呼び出しで呼び出し規約を一致させること」であり、「どの呼び出し規約を使うか」はあまり重要ではない。全てのソースコードで同じ呼び出し規約を用いる必要はない。特定のライブラリだけ、メインのコード群とは別の呼び出し規約でコンパイルされても良いと考えている。
ここにライブラリを使う時のガイドラインの著者からのガイドラインを示したい:
外部公開するライブラリヘッダでは、全てにおいて呼び出し規約を明示的に指定するべきである。 (コンパイル時のデフォルトに依存しない)
ライブラリヘッダがデフォルトに依存しないのであれば、それを使うモジュールは好きな呼び出し規約でコンパイル出来る。
Win32だけを想定したライブラリであれば、外部公開するライブラリヘッダの全関数に呼び出し規約を明示すれば充分だろう:
/* mylibrary.h */ extern void * __stdcall circalloc(size_t n); extern char * __stdcall circdup(const char *s); extern char * __cdecl circfmt(const char *fmt, ...); extern BOOL __stdcall set_inherit_handle(BOOL bInherit, HANDLE h); extern void __stdcall init_timestamp(void); extern size_t __stdcall sprintf_timestamp(char *obuf); typedef void __stdcall FAILHANDLER(int, const char *, const char *); extern FAILHANDLER * __stdcall set_fail_handler(FAILHANDLER *pHdlr); ...
typedefにも呼び出し規約を指定している点に注意する。FAILHANDLERはcallbackとしてset_fail_handlerに関数ポインタとして指定されるが、typedefに呼び出し規約を指定することで、アプリケーション側でも自動的にFAILHANDLERの呼び出し規約が適用される。
関数定義にまで呼び出し規約を明示する必要は、現実的にはそこまで厳しく必要とされていない。なぜなら、定義側で呼び出し規約を指定していない場合、ヘッダファイル中の宣言に含まれる呼び出し規約が適用されるからだ。逆に、ヘッダファイル中でのプロトタイプ宣言と実際の定義とで呼び出し規約が一致しない状況は避けなければならない。
/* somefile.c */ extern void __stdcall foo1(void); .. void foo1(void) // OK - __stdcall taken from the declaration just seen { ... } extern void __stdcall foo2(void); ... void __cdecl foo2(void) // ERROR - clashes with __stdcall above { ... } extern void foo3(void); // presume __cdecl ... void __stdcall foo3(void) // ERROR - clashes with presumed __cdecl { ... }
あるモジュール内でしか使われない非公開な関数については呼び出し規約を明示する必要はない。関数の利用がコンパイルの単位(=ソースファイル)内で閉じていて、外部から呼ばれないようになっていれば、呼ぶ側・呼ばれる側でミスマッチする心配は無い。
呼び出し規約の指定だが、"__stdcall"などのキーワードがsyntax errorになるようなWin32以外のプラットフォームに移植する場合は少しトリックを使う必要が出てくる。
MSVC以外のプラットフォームにスムーズに対応する為、Cプリプロセッサと移植用のヘッダファイルを使う。
著者らは、この問題に対応する為の定義(およびその他移植性を高める為の多数の定義)を含む"portable.h"というヘッダーファイルを作成している。
#ifndef _WIN32 # define __cdecl /* nothing */ # define __stdcall /* nothing */ # define __fastcall /* nothing */ #endif /* _WIN32 */
プラットフォームや開発環境によっては多少の調整を要する。また、"_WIN32"ではなく"_MSC_VER"を使ったり、"__stdcall"などを直接マクロで無効化するのではなく、WINAPIのようにラッパマクロを定義して切り替えるようにするなど改良の余地はある。
GCCを使う場合は、"__attribute__" 機能で呼び出し規約を指定出来たりするのでそちらを使っても良い。
重要なのは、「ライブラリをコンパイルした時の関数の呼び出し規約」と、「クライアント側がヘッダーファイルで取り込んだ関数宣言の呼び出し規約」、この2つが一致することである。
以上