Windows C++ 程式的入口點
第一個問題,什麼是入口點?
- 對於開發者來說,程式的入口點就是程式執行的時候第一個執行的函式。
對於C++程式,常見的入口點有:
1.main
2.WinMain
3.DllMain
- 對於作業系統來說,程式的入口點就是把程式裝載到記憶體後,第一條命令開始的地方。
作業系統(Windows)如何確定入口點呢?
首先,Windows下所有可執行程式都是PE格式,PE其中一個組成部分 可選頭 ,對應的資料結構:IMAGE_OPTIONAL_HEADER
在可選頭中,有一個成員 AddressOfEntryPoint,該成員就表示程式的入口點。
關於PE檔案格式,讀者可以自行查閱資料。
在這裡,我放出兩個連結,可以快速瞭解PE的入口點
深入理解 Win32 PE 檔案格式:https://blog.csdn.net/chenlycly/article/details/53378946
_IMAGE_OPTIONAL_HEADER structure:https://docs.microsoft.com/zh-cn/windows/desktop/api/winnt/ns-winnt-_image_optional_header
第二個問題,IMAGE_OPTIONAL_HEADER.AddressOfEntryPoint指向的地址就是main/WinMain/DllMain的函式地址嗎?
可能這麼問,一些讀者可能不好理解。
首先,對於Windows來說,IMAGE_OPTIONAL_HEADER.AddressOfEntryPoint 其實就是程式的實際入口點
知道這一點之後,問題就可以換成這樣 main/WinMain/DllMain 是程式的實際入口點嗎?
答:當然不是,或者更嚴謹一點,大部分情況下不是
證明
1.使用vs建立新專案,專案型別選擇空專案
2.新增.cpp檔案,並新增main函式
3.新增端點,啟動除錯,當程式停在main函式中的斷點時,shift+F11 跳出main函式,如圖所示
可以看到,偵錯程式進入了一個 exe_common.inl 的檔案,該檔案中的62行有一個函式 invoke_main(),函式名也很好理解,呼叫main,函式體也和函式名一樣,在64行呼叫了main函式,也就是我們自己寫的main函式。
由此可以證明,main函式並不是程式的實際入口點。
那麼,invoke_main() 是實際的入口點嗎?當然也不是。
main不是實際入口點,invoke_main也不是實際的入口點,那麼哪個函式才是真正的入口點呢?它又是如何一步一步呼叫到我們寫的main的呢?接下來繼續分析。
至於一開始說的,大部分情況下入口點不是main/WinMain/DllMain,可能有些細心的讀者會問,那剩下的小部分呢?別急,接下來會全部分析到。
查詢真正的入口點
首先,先準備好需要的工具
1.dumpbin.exe 這個工具用作檢視PE檔案,如果讀者電腦上沒有這個工具,可自行下載
2.vscode 用來打卡c執行時庫的程式碼,當然,讀者也可以使用別的文字檢視工具
開始
1.使用cmd進入我們剛剛新建的專案的輸出目錄,然後使用dumpbin檢視編譯後的程式
例如:
cd D:\Test\Debug
dumpbin /headers CPlusPlus.exe
實際輸出目錄和程式名讀者自行修改
大家注意看截圖紅色部分 OPTIONAL HEADER VALUES 的第6項,這個成員就是上面提到的
IMAGE_OPTIONAL_HEADER.AddressOfEntryPoint ,也就是 程式的 實際入口點
因為我們的輸出目錄帶有.PDB檔案,dumpbin把這個 11041 這個地址對應的函式名打印出來了
mainCRTStartup,也就是說,我們剛剛那個程式實際的入口函式是這個,知道了實際入口函式名,接下來就好辦了
大家是否還記得 exe_common.inl 這個檔案,這個檔案是 invoke_main 函式所在的檔案,我們找到這個目錄
X:\XX\Microsoft Visual Studio 14.0\VC\crt\src\vcruntime
然後通過vscode開啟整個目錄
全域性搜尋 mainCRTStartup
搜尋結果有很多,我們不用關心 對於這個函式的呼叫和宣告,我們只需要找到它的定義
它的定義位於檔案 exe_main.cpp
找到了實際的入口函式,就可以通過程式碼檢視函式呼叫關係,一步一步往下,最後就可以看到invoke_main和main了
WinMain和DllMain也是相同的道理,把main函式改為WinMain/DllMain,編譯連結後檢視真正的入口函式,然後搜尋查詢入口函式的定義,這裡就不一一列舉了
main:
mainCRTStartup(exe_main.cpp)->__scrt_common_main(exe_common.inl)->__scrt_common_main_seh(exe_common.inl)->invoke_main(exe_common.inl)->main
WinMain:
WinMainCRTStartup(exe_main.cpp)->__scrt_common_main(exe_common.inl)->__scrt_common_main_seh(exe_common.inl)->invoke_main(exe_common.inl)->WinMain
DllMain:
DllMainCRTStartup(dll.dllmain.cpp)->dllmain_dispatch(dll.dllmain.cpp)->DllMain
連結器對於入口點的選擇
讀者朋友們可能會納悶,為什麼我們定義main函式和定義WinMain函式,程式的實際入口不一樣,連結器是怎麼選擇入口點的,以下是我做了一些實驗,總結出來的,供大家參考。不一定完全正確,歡迎大神指出問題
連結時,如果使用 /ENTRY 選項,則會使用 /ENTRY 選項指定的入口點,這一點就解釋了上面所說的小部分情況,如果我們使用/ENTRY指定入口點為 main/WinMain/DllMain ,那麼程式的實際入口點就是main/WinMain/DllMain
如果沒有使用 /ENTRY 選項(一般情況下):
對於 EXE, 連結時如果使用 /SUBSYSTEM 選項,連結器則會根據選項引數選擇實際的入口點
- /SUBSYSTEM:CONSOLE 實際入口: mainCRTStartup (or wmainCRTStartup) 內部呼叫: main (or wmain)
- /SUBSYSTEM:WINDOWS 實際入口: WinMainCRTStartup (or wWinMainCRTStartup) 內部呼叫 : WinMain (or wWinMain) 呼叫約定: __stdcall
連結時如果沒有使用 /SUBSYSTEM 選項,連結器會根據現有的過程(函式)選擇實際的入口點
exe_common.inl 通過巨集來控制 invoke_main 函式的版本
例如:
如果定義了main,則編譯器會定義 _SCRT_STARTUP_MAIN 這個巨集,就會編譯 呼叫main函式的invoke_main版本
- 如果程式定義了 main 過程,則連結器會使用 mainCRTStartup (or wmainCRTStartup) 作為程式的實際的入口點
- 如果程式定義了 WinMain 過程,則連結器會使用 WinMainCRTStartup (or wWinMainCRTStartup) 作為程式的實際的入口點
- 如果程式同時定義了 main 過程和 WinMain 過程,連結器會優先使用WinMainCRTStartup (or wWinMainCRTStartup) 作為程式的實際的入口點
如果程式既沒有定義 main 過程,也沒有定義 WinMain 過程,則會連結失敗,提示 需要定義入口點
對於 DLL , 連結器選擇的實際的入口點是 _DllMainCRTStartup 內部呼叫 DllMain
如果開發者沒有定義 DllMain,系統則會事先編譯 Microsoft Visual Studio 14.0\VC\crt\src\vcruntime\dll_dllmain_stub.cpp
並連結
參考: https://msdn.microsoft.com/zh-cn/library/windows/apps/xaml/f9t8842e(v=vs.85)/html
綜上,連結器預設(未使用/ENTRY選項)選擇的入口點有三個
mainCRTStartup,WinMainCRTStartup,_DllMainCRTStartup
但是開發者的程式碼中一般不會有這三個函式的實現,這時候就需要藉助編譯器自帶的執行時庫 : MSVCRTD.lib
MSVCRTD.lib是Debug版本,其對應的Release版本是MSVCRT.lib,沒有最後面的D(ebug)
這個庫中實現編譯好了這三個函式
要用到這個庫,當然就需要連結這個庫,但是我們檢視剛剛建立的專案屬性,並沒有新增這個庫
沒有新增這個庫,那怎麼連結這個庫呢?
其實,對於.cpp檔案,編譯器在編譯時,會自動加上這個庫
證明:
1.設定彙編檔案輸出
2.重新編譯後,在專案的Debug目錄下找到彙編檔案 xxx.asm,並開啟
如圖,彙編程式碼中,引入了這個庫 MSVCRTD(忽略了字尾.lib)
附上一張連結器選擇入口點的流程圖
最後,感謝大家看到這裡,雖然也不一定有人會看