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應用程序。