1. 程式人生 > >Windows C++ 程式的入口點

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函式

main

3.新增端點,啟動除錯,當程式停在main函式中的斷點時,shift+F11 跳出main函式,如圖所示

invoke_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

實際輸出目錄和程式名讀者自行修改

dumpbin_headers

大家注意看截圖紅色部分 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 選項,連結器則會根據選項引數選擇實際的入口點

  1. /SUBSYSTEM:CONSOLE 實際入口: mainCRTStartup (or wmainCRTStartup) 內部呼叫: main (or wmain)
  2. /SUBSYSTEM:WINDOWS 實際入口: WinMainCRTStartup (or wWinMainCRTStartup) 內部呼叫 : WinMain (or wWinMain) 呼叫約定: __stdcall

連結時如果沒有使用 /SUBSYSTEM 選項,連結器會根據現有的過程(函式)選擇實際的入口點

exe_common.inl 通過巨集來控制 invoke_main 函式的版本

例如:

invoke_main不同版本

如果定義了main,則編譯器會定義 _SCRT_STARTUP_MAIN 這個巨集,就會編譯 呼叫main函式的invoke_main版本

  1. 如果程式定義了 main 過程,則連結器會使用 mainCRTStartup (or wmainCRTStartup) 作為程式的實際的入口點
  2. 如果程式定義了 WinMain 過程,則連結器會使用 WinMainCRTStartup (or wWinMainCRTStartup) 作為程式的實際的入口點
  3. 如果程式同時定義了 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)

附上一張連結器選擇入口點的流程圖

入口點選擇

 

最後,感謝大家看到這裡,雖然也不一定有人會看