エントリーポイントを WinMain と違う名前に変えてみた

WinMain とは何か?」では、WinMain という名前が、 ウィンドウズプログラムの既定のエントリーポイントであると説明しました。

ここでは試しに、WinMain と違う名前、例えば MyFunc という名前にしたらどうなるか、みてみましょう。

ちなみに、この記事は単なる好奇心のためのものです。通常は WinMain という名前を残すべきです。

リンカオプションまでチェックしないとどこからプログラムが始まるかわからないのでは、面倒くさくて仕方がありません。

さて、次のコードを helloworld.cpp として保存します。

#include <windows.h>

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

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

        return 0;
}

MyFunc という名前の関数が実装されているだけです。

これで次の makefile でビルドしてみましょう。リンカオプションで /ENTRY は指定していません。

TARGETNAME=helloworld
OUTDIR=.\chk
LINK=link.exe

ALL : "$(OUTDIR)\$(TARGETNAME).exe"

"$(OUTDIR)" :
    @if not exist "$(OUTDIR)\\" mkdir "$(OUTDIR)"

CPPFLAGS=\
        /nologo\
        /W4\
        /Fo"$(OUTDIR)\\"\
        /Fd"$(OUTDIR)\\"\
        /c\
        /Zi\
        /DUNICODE\
        /D_UNICODE\

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

LINKOBJS= \
        "$(OUTDIR)\$(TARGETNAME).obj"

"$(OUTDIR)\$(TARGETNAME).exe" : "$(OUTDIR)" $(LINKOBJS)
    @$(LINK) $(LINKFLAGS) $(LINKOBJS)

.cpp{$(OUTDIR)}.obj:
    @$(CPP) $(CPPFLAGS) $<

ビルドを試みると、次のようにエラーになります。... で省略してます。

> nmake

Microsoft (R) Program Maintenance Utility Version 14.10.25019.0
Copyright (C) Microsoft Corporation.  All rights reserved.

...
LIBCMT.lib(exe_winmain.obj) : error LNK2019: unresolved external symbol WinMain 
referenced in function "int __cdecl __scrt_common_main_seh(void)" (?__scrt_common_main_seh@@YAHXZ)
.\chk\helloworld.exe : fatal error LNK1120: 1 unresolved externals
...
Stop.

要は WinMain が無いぞ、と怒られています。

そこで 次のようにリンカオプションを書き換えて、ビルドし直します。これで無事にビルドできるはずです。

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

以上を実行すると、確かに MyFunc に実装されたポップアップが表示されるので、動作確認はできるはずです。

念のためエントリーポイントを確認するために dumpbin で、EXE ファイルの内容を確認してみましょう。

> dumpbin /HEADERS helloworld.exe

ヘッダーに次のようにエントリーポイントが確認できます。

 HEADER VALUES
             20B magic # (PE32+)
           14.10 linker version
            1200 size of code
             E00 size of initialized data
               0 size of uninitialized data
            1005 entry point (0000000140001005) @ILT+0(?MyFunc@@YAHPEAUHINSTANCE__@@0PEADH@Z)
            1000 base of code
       140000000 image base (0000000140000000 to 0000000140006FFF)

エントリーポイント (entry point) として、?MyFunc@@YAHPEAUHINSTANCE__@@0PEADH@Z と書いてますが、 これは undname SDK ツールを使うと次のような内容であることがわかります。

> undname ?MyFunc@@YAHPEAUHINSTANCE__@@0PEADH@Z
Microsoft (R) C++ Name Undecorator
Copyright (C) Microsoft Corporation. All rights reserved.

Undecoration of :- "?MyFunc@@YAHPEAUHINSTANCE__@@0PEADH@Z"
is :- "int __cdecl MyFunc(struct HINSTANCE__ * __ptr64,struct HINSTANCE__ * __ptr64,char * __ptr64,int)"

これは確かに MyFunc ですね。

ちなみに、__cdecl になっていますが、これは x64 ビルドだからです。x64 では基本的にパラメータがレジスター渡しなので、__cdecl で問題ありません。

x86 用のビルドで dumpbin をみると、ヘッダは次のようになります。

             10B magic # (PE32)
           14.10 linker version
            1200 size of code
             800 size of initialized data
               0 size of uninitialized data
            1005 entry point (00401005) @ILT+0(?MyFunc@@YGHPAUHINSTANCE__@@0PADH@Z)
            1000 base of code

エントリーポイントを undname すると、

> undname ?MyFunc@@YGHPAUHINSTANCE__@@0PADH@Z
Microsoft (R) C++ Name Undecorator
Copyright (C) Microsoft Corporation. All rights reserved.

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

このように __stdcall であることがわかります。

ついでに言えば、上でわざわざ undname して名前を取得していますが、これは C++ としてコンパイルしているからです。

C++ では引数違いのことなる関数 (オブジェクト指向でいうところの多態性) を実装可能とするために、 コンパイラでは引数の違う関数の名前を変えて扱います。これによってリンカが正しい関数を見つけられるようになります。

これを名前修飾 (Name mangling) といいます。

上記ソースコードを helloworld.c として拡張子を変えて C 言語として扱う、 もしくは *.cpp のままでもコンパイラオプション /TC をつけてコンパイルすれば、 次のように名前の修飾が行われません。

             20B magic # (PE32+)
           14.10 linker version
            1200 size of code
            1000 size of initialized data
               0 size of uninitialized data
            1005 entry point (0000000140001005) @ILT+0(MyFunc)
            1000 base of code

ネームマングリングは COM のようなバイナリーのレイアウトが大事になるような状況で微妙に問題になったりすることがあるので、 ちょっと覚えておくといいかもしれません。