関数を呼び出すということ
前の資料では、WINAPI というのは __stdcall の別名であり、これは呼び出し規約であると説明しました。
そして 「呼び出し規約」 を理解するためには 「関数を呼び出す」ということがどのようなものか理解すると良いです。
尚、ここでは x86 アーキテクチャを想定しています。
結局、関数を呼び出すってどういうこと?
あるプログラム A から、あるプログラム B を呼び出す時に x86 ではスタックというメモリ領域を使用します。
ここでは、A は Windows の Win32 サブシステム、B はここで作った自前のプログラムとします。
A から B の WinMain を呼び出すときのことを考えます。
この時に、A はスタックにパラメータを積み上げます。 B を実行するときに、必要に応じてスタックに積み上げられたパラメータを参照します。
B の処理が終わったら、制御は A へと戻ります。
この時に、スタックに積み上げられたパラメータはもう不要なので、クリーンアップします。
具体例: 関数 Foo を呼び出す例
言葉だけではわかりにくいと思うので、図でもう一度説明します。
ここでは、Foo という名前の関数を、引数 param1、param2, param3 の三つで呼び出すことを考えましょう。
C 言語のコードでは、
Foo ( param1, param2, param3);
というような状況です。
Foo を呼び出す部分では、引数 param1, param2, param3 をスタックに積み上げています。
スタックに積み上げる命令は PUSH です。 引数を param3, param2, param1 という順番で、スタックに PUSH しています。 param3, param2, param1 という順番で積み上げるので、 スタックは図の中の右下側のようになります。
そうしてスタックを形成してから、CALL Foo という命令で Foo 関数に入ります。
図1. __stdcall 呼び出し時のコードとスタック
Foo 関数の実行中は、スタックに積み上げられたパラメータを必要に応じて参照します。
Foo 関数を実行した後、RET で呼び出し元に戻ります。 戻るときに、RET N という命令を発行して、スタックを N だけ元に戻します。
つまり、これでスタックは関数の呼び出しを準備する前の状態に戻ります。
いかがでしょうか?パラメータはこのようにして、関数に渡されるのです。
普段、あなたが書いた C 言語のコードから、あなたが書いた関数を呼ぶ場合は、 何も指定しなくても、デフォルトの呼び出し規約に従って、プログラムが構築されるため、 わざわざ呼び出し規約を指定しなくても問題はありません。
しかし、あなたのプログラムから、Windows の API を呼ぶ場合は、 その API が作られている形式に合わせて呼び出さないといけません。
結局、__stdcall という呼び出し規約ではどうなるの?
さて、いよいよ __stdcall という呼び出し規約について説明します。
__stdcall という呼び出し規約では、プロトタイプ宣言の右側のパラメータから順番にスタックに積みます。
具体的に言うと A (呼び出し元) は nCmdShow → lpCmdLine → hPrevInstance → hInstance という 順番でパラメータを積み上げます。
int WINAPI WinMain ( hInstance, hPrevInstance, lpCmdLine, nCmdShow )
← 右から左へスタックに積む
そして、B が処理を終えたら B (呼び出された側) がスタックをクリーンアップします。
スタックにデータを積み上げていくと、スタックポインタはアドレスの小さいほうに「伸びて」いきます。 スタックをクリーンアップするときは、伸びたスタックを元に戻すことになるので、「スタックを巻き戻す」などと表現する場合があります。 この言い方を使うと、「B がスタックをクリーンアップする」は「B がスタックを巻き戻す」と言い換えられます。
他にも __stdcall で定義していることはありますが、主にこの二つが __stdcall 呼び出し規約で決められていることです。
もう一度言うと、__stdcall は
- 「引数の積み上げ順序は右から左」
- 「スタックの巻き戻しをするのは呼び出された側」
という呼び出し規約です。
他の呼び出し規約ではどうなの?
呼び出し規約は __stdcall の他にもあります。
代表格は __cdecl です。これは C 言語で標準に使用されている呼び出し規約です。
こちらは、「引数の積み上げ順序は右から左」であり __stdcall の場合と同じですが、スタックを巻き戻すのは異なり、「スタックを巻き戻すのは呼び出し側」です。
C 言語の標準ライブラリでは可変個引数の関数をサポートしています。例えば printf などの関数がそうです。 可変個引数の関数はそれが作られた時点で、実行時にいくつのパラメータがスタックに積み上げられるのかわかりません。 ある場所からは、8バイト分のパラメータが積み上げられるかもしれませんし、 ある場所からは100バイトのパラメータが積み上げられるかもしれません。その関数にどのくらいの量の パラメータを渡すか、それを知っているのは呼び出し側だけなのです。 このために、C 言語の標準ではスタックを巻き戻すのは呼び出し側の仕事になっています。
Win32 は WinMain を __stdcall のお作法に従って呼び出す!
以上を踏まえもう一度 WinMain に話を戻すと結局こういうことになります。
WinMain は OS の Win32 サブシステムから呼び出されます。
Win32 サブシステムは、この WinMain 関数が __stdcall 規約に従っていると想定しています。ですから、Win32 は、WinMain を呼び出す時に、 スタックにパラメータを nCmdShow → lpCmdLine → hPrevInstance → hInstance の順番で積み上げます。
そして、Win32 は WinMain から制御が戻ってきたときにスタックを巻き戻しません。WinMain がスタックを巻き戻します。
ちなみに蛇足ですが、もし誤って WinMain を __cdecl として作ってしまうとどうなるでしょうか。
WinMain は処理を終了するときに、__cdecl の規約に従いスタックをクリーンアップしません。
また Win32 は __stdcall に従いますから、WinMain から制御が戻ってきたときに、やはりスタックをクリーンアップしません。 スタック上にクリーンアップされないデータが積み上げられたままとなってしまいます。
運がよければ呼び出し規約を間違えても、一見問題なく処理が終了するかもしれません。 しかしそれは単に運が良かった、というだけです。
以上、説明がやや長くなってしまいましたが、いかがでしたでしょうか?
ここでは WinMain は __stdcall 呼び出し規約にしたがうこと、 及び __stdcall の意味について説明しました。