WinMain是如何被调用的

0 前言

在C/C++语言设计中,main函数作为应用程序的入口,若不定义main函数将在链接时报错。

基于C语言的Win32程序设计中,WinMain函数作为Win32应用程序的入口,若不定义WinMain函数将在链接时报错。

同样的C语言,同一个编译器,为什么会出现这样的情况?

1 WinMain函数

1.1 WinMain函数原型

Win32应用程序的入口函数为WinMain,函数原型在WinBase.h文件中:

int WINAPI WinMain (

    _In_ HINSTANCE hInstance,
    _In_opt_ HINSTANCE hPrevInstance,
    _In_ LPSTR lpCmdLine,
    _In_ int nShowCmd
);

1.2 WinMain函数原型中的符号

符号 描述 其它
int 返回值 程序返回值,0表示正常,非0表示异常。程序非正常退出时,操作系统可能弹框提示非正常关闭。
WINAPI 函数调用约定

WINAPI宏展开:

#define WINAPI      __stdcall

WinMain 函数名  
HINSTANCE hInstance 当前应用程序实例句柄

HINSTANCE宏展开:

DECLARE_HANDLE(HINSTANCE);

#define DECLARE_HANDLE(name) struct name##__{int unused;}; typedef struct name##__ *name

HINSTANCE hPrevInstance 指向前一个实例的句柄 没有意义。 它用于 16 位Windows,但现在始终为零
LPSTR lpCmdLine 命令行参数 可以是命令提示符程序后的参数,快捷方式参数,创建进程函数传入的参数
int nShowCmd 窗口显示控制参数 窗口显示方式:隐藏、正常、最大化、最小化

2 程序构建原理

2.1 PE(Portable Executable)文件

PE文件的全称是Portable Executable,意为可移植的可执行的文件,常见的EXE、DLL、OCX、SYS、COM都是PE文件,PE文件是微软Windows操作系统上的程序文件(可能是间接被执行,如DLL)。

--百度百科

意思是微软Windows操作系统提供了一个种接口,让你的指令在微软Windows操作系统中执行,这种接口是PE文件。

PE文件规定了可执行程序中数据和指令的存储位置,其中AddressOfEntryPoint存放程序的入口。

比如一个可执行程序(.exe)中可能包含很多函数,运行程序时,操作系统解析PE文件格式中的AddressOfEntryPoint值,并从AddressOfEntryPoint指向的函数地址开始执行。

2.2 链接工具

编译工具将代码转换成目标程序,链接工具将多目标程序生成可执行程序。

可执行程序的PE文件约定程序的入口地址放入AddressOfEntryPoint变量,目标程序有很多函数,到底把哪个函数的地址放在入口地址呢?

链接工具可以指定程序的入口函数,如VC编译器,可以通过/entry参数指定入口函数,默认入口函数为/entry:mainCRTStartup。没有指定/entry参数时,IDE根据当前项目特征指定入口函数,控制台程序默认为/entry:mainCRTStartup,窗口程序默认为/entry:WinMainCRTStartup。

默认的入口函数/entry:mainCRTStartup、/entry:WinMainCRTStartup,安装的开发工具时,相关库提供了实现。

链接后生成的PE文件的AddressOfEntryPoint存储着入口函数的地址。

2.3 编译工具

编译工具将代码转换成目标程序,链接工具在目标程序中检查依赖。找不到依赖则报错,找到所有依赖则生成PE文件。

C语言编写控制台程序,编译时,若未设置/entry参数,默认入口函数mainCRTStartup间接调用了main,链接时,如果没有定义main函数则报错。

C语言编写Win32窗口程序,编译时,若未设置/entry参数,默认入口函数WinMainCRTStartup间接调用了WinMain,链接时,如果没有定义WinMain函数则报错。

2.4 主函数

C语言编写控制台程序,默认入口函数为main函数,C语言编写Win32窗口程序时,默认入口函数为WinMain函数。

由于程序的入口函数可以通过链接工具/entry参数指定,因此,可以通过该参数修改程序入口函数。

3 程序构建实例

3.1 主函数缺失(现象)

C语言编写控制台程序,不写main函数,链接报错:

1>MSVCRTD.lib(exe_main.obj) : error LNK2019: 无法解析的外部符号 main,函数 "int __cdecl invoke_main(void)" (?invoke_main@@YAHXZ) 中引用了该符号


C语言编写Win32窗口程序,不写WinMain函数,链接报错:

1>MSVCRTD.lib(exe_winmain.obj) : error LNK2019: 无法解析的外部符号 WinMain,函数 "int __cdecl invoke_main(void)" (?invoke_main@@YAHXZ) 中引用了该符号

3.2 LIB文件(推理)

没有主函数实现的链接错误信息中的MSVCRTD.lib(exe_main.obj) MSVCRTD.lib(exe_winmain.obj) 是什么意思?

从MSVCRTD.lib文件名可以看出文件类型为.lib,lib文件可以是静态链接库,也可以是导入库。

lib静态链接库包含函数声明和实现,链接时将依赖的块打包到编译后的可执行程序;lib导入库包含DLL的输出信息,链接时,将依赖信息写入编译后的可执行文件。lib静态链接库和lib导出库链接生成可执行程序后,可执行程序执行时,不在使用编译时使用的lib文件。

可以通过lib命令查看lib文件是静态链接库还是导出库。使用命令lib /LIST MSVCRTD.lib,查看命令输出得知MSVCRTD.lib时一个静态链接库:

 通过输出过滤可以发现输出列表中包含exe_main.obj和exe_winmain.obj文件。

C:\Program Files\Microsoft Visual Studio\2022\Enterprise>lib /list "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Tools\MSVC\14.30.30705\lib\x86\msvcrtd.lib"  |findstr main.obj

d:\a01\_work\10\s\Intermediate\vctools\msvcrt.nativeproj_607447030\objd\x86\dll_dllmain.obj
d:\a01\_work\10\s\Intermediate\vctools\msvcrt.nativeproj_607447030\objd\x86\exe_main.obj
d:\a01\_work\10\s\Intermediate\vctools\msvcrt.nativeproj_607447030\objd\x86\exe_winmain.obj
d:\a01\_work\10\s\Intermediate\vctools\msvcrt.nativeproj_607447030\objd\x86\exe_wmain.obj
d:\a01\_work\10\s\Intermediate\vctools\msvcrt.nativeproj_607447030\objd\x86\exe_wwinmain.obj

由此得知,exe_main.obj和exe_winmain.obj是MSVCRTD.lib静态链接库中的一部分。

3.3 链接(本质)

链接工具在链接时,由于指定了程序入口函数,如mainCRTStartup或WinMainCRTStartup,意味着在程序中会调用入口函数,那么就需要检查入口函数的依赖。

控制台程序默认入口为mainCRTStartup,mainCRTStartup函数在exe_main.obj中,mainCRTStartup函数间接调用了main函数:

mainCRTStartup()->__scrt_common_main()->__scrt_common_main_seh()->invoke_main()->main()。

 如果没有main函数实现,链接工具链接检查时,找不到依赖的main函数,链接报错。


Win32窗口程序默认入口为WinMainCRTStartup,WinMainCRTStartup函数在exe_winmain.obj中,WinMainCRTStartup函数间接调用了WinMain函数:

WinMainCRTStartup()->__scrt_common_main()->__scrt_common_main_seh()->invoke_main()->WinMain()。

如果没有WinMain函数实现,链接工具链接检查时,找不到依赖的main函数,链接报错。

4 总结

4.1 程序入口

控制台程序默认入口为mainCRTStartup,Win32窗口程序默认入口为WinMainCRTStartup,也可以通过编译工具设置为自定义函数。

4.2 main调用序列

main调用可以通过exe_main.cpp,exe_common.inl文件查看

mainCRTStartup()->__scrt_common_main()->__scrt_common_main_seh()->invoke_main()->main()。

4.3 WinMain调用序列

WinMain调用可以通过exe_winmain.cpp,exe_common.inl文件查看

WinMainCRTStartup()->__scrt_common_main()->__scrt_common_main_seh()->invoke_main()->WinMain()。

4.4 主函数链接原理

根据上面的构建原理得知,主函数不是编译、链接工具完成编译链接后,去搜寻是不是存在主函数。

主函数的检查和一般函数依赖检查一样,利用的是链接工具的依赖检查。

4.5 为什么是WinMain不是main?

不管是main还是WinMain都是编程语言的约定,约定内容包括返回值,调用约定,函数名,函数参数。

main和WinMain参数都包含命令行参数。

对Win32应用程序来说,主函数大概率会用到应用程序实例,即HINSTANCE hInstance参数。

为了给Win32应用程序开发提供便捷,制定了更适合Win32应用程序开发的接口约定WinMain。

应用程序实例也可以通过GetModuleHandle获取,也就是说,用main或者自定义的函数作为主函数也可以编写Win32应用程序。

posted @   锅烟煤  阅读(1408)  评论(4编辑  收藏  举报
点击右上角即可分享
微信分享提示