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, |
![]() |
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应用程序。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步