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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM