プログラムはどこから始まるの? ~ WinMain とは?

前節では単純な Windows プログラムをビルドしました。 今回はその説明です。まだビルドしていない人はぜひ自分の手で、ビルドしてみてください。

今回はこのコードを解説します。


#include <windows.h>

int WINAPI WinMain ( 
	HINSTANCE hInstance, 
	HINSTANCE hPrevInstance, 
	LPSTR lpCmdLine, 
	int nCmdShow ) {

	MessageBox( 
		NULL, 
		TEXT("Hello, world!"), 
		TEXT("Hello"), 
		MB_OK | MB_ICONINFORMATION );

	return 0;
}

A さん: 「あれ?これ C 言語のプログラムですよね?」
先生: 「そうだよ。」
A さん: 「C 言語のプログラムって main 関数がないと動きませんよね?このプログラム、これで動くんですか?」
先生: 「ははは。さっき、動くところをみたじゃないか」
A さん: 「あ、そうですね。ポップアップが出てきましたよね。でも、main がないのに・・・」

C 言語には慣れているはずの A さんも、見慣れない書き方に面食らってしまったようです。

では、このコードを見ていきましょう。これのコードには そもそも main すらありません。 これで動くのでしょうか。 C 言語で書いたコードは main から始まるのではなかったでしょうか。 それから山ほどあるキーワード WINAPI、HINSTANCE、 LPSTR、MessageBox、TEXT、...。 これらを一つ一つ見ていきましょう。

Windows プログラムは WinMain から開始

結論からひとことで言うと、 リンカオプションでサブシステム (/SUBSYSTEM) を WINDOWS に指定すると、 デフォルトのエントリポイントは WinMain になります

これはどういうことでしょうか。

リンカオプションでサブシステムを指定する、ということは簡単に言うと 「どんな環境用のコードを生成するか指定する」 ということです。 Windows 向けのコードなら WINDOWS という値を、 ウィンドウを持たないキャラクタベースのプログラムなら CONSOLE という値を、 携帯端末用のモバイル Windows 用なら WINDOWSCE という値を、それぞれ指定します。

サブシステムとして WINDOWS が指定されていると、リンカは WinMain という名前の関数を探して、それをプログラムのエントリポイントに設定します。 「エントリポイントに設定する」ということは、OS がプログラムを実行するために EXE ファイルをメモリにロード (読み込む) した後、どのアドレスからプログラムを実行するか指定する、 ということです。実は EXE ファイルのヘッダ部分にエントリポイントのアドレスを書き込む場所があり、 リンカはその情報を埋めるために WinMain という関数のアドレスを探しているのです。

ちなみにリンカオプション /ENTRY で、エントリポイントなる関数の名前を変更することが出来ます。 例えば、WinMain という名前が嫌なら違う名前に変えても構いません。

例えば、Foo というでたらめの名前をエントリポイントにしたいなら次のようにもできます。


#include <windows.h>

int WINAPI Foo ( 
	HINSTANCE hInstance, 

... 省略...

リンカオプションを次のように変えます。


LINK32_FLAGS=\
	user32.lib\
	/nologo\
	/subsystem:windows\
	/pdb:"$(OUTDIR)\$(TARGETNAME).pdb"\
	/out:"$(OUTDIR)\$(TARGETNAME).exe"\
	/DEBUG\
	/ENTRY:Foo

これを nmake でビルドすると、helloworld.exe が正常にビルドできるはずです。

念のため dumpbin コマンドで、出来上がった helloworld.exe のヘッダ情報に含まれる、 エントリポイントの情報を抽出すると、次のようになります。

> dumpbin /HEADERS .\chk\helloworld.exe | find "entry point"
            1005 entry point (00401005) @ILT+0(?Foo@@YGHPAUHINSTANCE__@@0PADH@Z)
> undname ?Foo@@YGHPAUHINSTANCE__@@0PADH@Z
Microsoft (R) C++ Name Undecorator
Copyright (C) Microsoft Corporation. All rights reserved.

Undecoration of :- "?Foo@@YGHPAUHINSTANCE__@@0PADH@Z"
is :- "int __stdcall Foo(struct HINSTANCE__ *,struct HINSTANCE__ *,char *,int)"

上記のように確かに Foo がエントリポイントに設定されていることがわかります。

上記で使った undname コマンドは、コンパイラが内部的に修飾・変更した名前を、元のシンボルに戻すコマンドです。 C++ コンパイラでは、ソースコード上で同じ名前の関数 (メソッド) が異なるパラメータを引数にとる (多態性) 必要があるため、 パラメータの情報でメソッド名が修飾されているのです。

結局 WinMain とは、Windows プログラムを作成する際、リンカがエントリポイントとして探す既定の名前だったのです。 この名前を変更する理由は通常ありませんから、「Windows プログラムは WinMain からスタートする」と覚えても問題ないでしょう。

但し、C ランタイム関数をを利用する場合にはそれが実行される前に CRT の環境設定用コードが実行されます。このページの下の補足を見てください。

尚、実はサブシステムとして CONSOLE を指定したときの、デフォルトのエントリポイントが、 main だったのです。このために、キャラクタベースのコンソールプログラムでは、main からプログラムが実行されていたのでした。

補足: 標準 C ライブラリや C# の場合

printf や alloc などの、いわゆる標準的な C 言語ライブラリを利用する場合や C# などのマネージドコードを実行するときに、実は上記の記述とはやや異なる動作をします。

C 言語の標準的なライブラリを利用する場合には、それを利用するために背後でライブラリの初期化等を行わなければなりません。 このため、Windows API だけでなく C ライブラリを使う場合、C ライブラリの初期化を行うコードを、 本当のエントリポイントとして設定します。コンソールプログラムの場合は _mainCRTStartup、 Windows プログラムの場合は _WinMainCRTStartup という関数です。

つまりコンソールプログラムの場合、実際には次のような順序で実行されます。

[OS の Win32 サブシステム] → [_mainCRTStartup] → [main 関数]

C のライブラリを利用するための、初期化や例外ハンドラの設定などが _mainCRTStartup 内部で行われ、 _mainCRTStartup の中から main 関数が呼び出されます。

これはコンソールプログラムとしてビルドするときだけではなく、たとえターゲットを WINDOWS にしても、 C の標準ライブラリを使用してプログラムを書く限り、上記のような流れとなります。 すなわち、次のようになる場合もあるということです。

[OS の Win32 サブシステム] → [_WinMainCRTStartup] → [WinMain 関数]

このような仕組みによって、C ライブラリが利用できるのです。

また、C# や VB.NET 等の CLR (Common Language Runtime) 上のプログラム (いわゆるマネージドコード) の場合は、 ユーザーが書いたカスタムプログラムを実行する前に、CLR 自体の準備が必要です。 マネージド・コードを実行するときは次のように実行されます。

[OS の Win32 サブシステム] → [CLR の初期化] → [ユーザー定義の C# のエントリポイントメソッド]
(Win32 サブシステムから見たエントリポイントは _CorExeMain という名前の関数です)

以上でみたように、Win32 サブシステムからダイレクトに WinMain を呼び出すときが最も低コストのプログラムスタートアップなのです。

ここまでお読みいただき、誠にありがとうございます。SNS 等でこの記事をシェアしていただけますと、大変励みになります。どうぞよろしくお願いします。

© 2024 Web/DB プログラミング徹底解説