C++入口不是main?知乎上打起來了

知乎上居然有人為了C++的入口函式到底是什麼打了起來!

C++入口不是main?知乎上打起來了

至於打得有多激烈我就不知道了,我們來關注這個問題本身。

你說main函式是入口,那main是被誰呼叫的呢?

他說mainCRTStartup是入口,那mainCRTStartup又是被誰呼叫的呢?

從程序建立說起

一切的一切,讓我們從建立程序開始說起。

程序建立完成後,接著會建立主執行緒,這是程序中第一個開始執行程式碼的執行緒。

主執行緒建立後,就得到了時間片,開始參與系統的執行緒排程,那麼程式從哪裡開始執行呢?

在Windows平臺,C++程式碼編譯後的可執行檔案叫PE檔案。

PE檔案中有一個叫

OEP

的術語便是指的程式入口點。所謂入口點顧名思義就是主執行緒最開始執行的地方,許多病毒加殼技術其中一點就是對這個OEP進行處理。

現在,我們來使用工具PEID來看一個程式(VC8。0編譯)的OEP如圖:

C++入口不是main?知乎上打起來了

0x00011078乃是RVA(相對虛擬地址),要看在程序地址空間中真正的起始地址,還得加上PE檔案的對映基址,預設為0x00400000,不過,可以透過編譯器選項進行調整。不知道也沒關係,將程式放入OllyDbg,在記憶體對映中可以看到程式的對映基址:

C++入口不是main?知乎上打起來了

由圖看到對映基址是0x00400000。

那麼由前面所述,程式執行的第一條指令應該位於0x00400000 + 0x00011078 = 0x00411078。

沒錯,就是這樣。

切換到OllyDbg的主視窗,我們發現了,程式確實初始停在了這裡,並且這裡是一條jmp指令。

C++入口不是main?知乎上打起來了

我們到Jmp的目的地0x00411800去看看那裡是什麼東東?

C++入口不是main?知乎上打起來了

這是什麼東西?先賣個關子,總之,這裡是程式進來之後真正做的第一件事。

main函式被誰呼叫?

換個思路,我們開啟VS2008寫一個簡單的程式,程式做什麼並不重要,我們要看它的啟動原理。

C++入口不是main?知乎上打起來了

注意看呼叫堆疊視窗,因為我是使用UNICODE編碼環境,故_tmain()就是wmain(),如果是ANSI編碼就是最開始學程式時的main()函數了。

以前寫程式就想過一個問題,我們寫的所有函式都會被我們自己直接或間接呼叫,但有一個函式例外,那就是main()函式。我們寫了它但從不會去呼叫它,事實上也不可能去呼叫它。

從呼叫堆疊看到,我們的wmain函式是被_tmainCRTStartup函式呼叫的,這是個什麼東西?再往前推是wmainCRTStartup呼叫的_tmainCRTStartup。這兩個函式是做什麼的,他們之間有什麼關係?

雙擊呼叫堆疊裡的項即可轉到對應的原始碼,我們可以發現,這兩個函式是在crtexe。c檔案中實現的。閱讀原始碼可以發現,有四個啟動函式分別是:

mainCRTStartup() ANSI + 控制檯程式

wmainCRTStartup() UNICODE + 控制檯程式

WinMainCRTStartup() ANSI + GUI程式

wWinMainCRTStartup() UNICODE + GUI程式

這一點在《windows核心程式設計》中也有提到。不過我們可以更進一步一窺它們的實現程式碼:

C++入口不是main?知乎上打起來了

就這麼簡單,先呼叫了__security_init_cookie(),然後是我們前面看到的_tmainCRTStartup()。

第一個函式是做什麼的呢?這個是微軟在VS2003後引入的防止緩衝區溢位攻擊的技術。簡單的說就是在呼叫函式的時候在棧裡安裝一個隨機的cookie值,這一cookie值在記憶體的一個地方有備份,函式呼叫完成後需要檢測這個cookie和備份的一不一致,以此來判斷有沒有棧溢位發生。那麼,這個函式就是來初始化這個備份區域的資料的。

然後第二個函式呼叫_initterm()進行全域性變數、物件初始化。之後,我們可以看到才是真正呼叫了我們的main()/wmain()/WinMain()/wWinMain()的地方。繞了一大圈,回答了開始的疑問了。

C++入口不是main?知乎上打起來了

這兩個函式是編譯器在生成可執行檔案的時候給我們連結進來的。

至此,我們來看看第一個函式wmainCRTStartup的彙編程式碼。如圖:

C++入口不是main?知乎上打起來了

請注意和我們前面使用OllyDbg除錯時的圖對比:

C++入口不是main?知乎上打起來了

發現沒有?一樣的!我們之前留的那個問題的答案想必已經出來了:

程式一進來從OEP處執行了jmp指令,這條指令轉向了wmainCRTStartup開始了程式真正的起點!

結論

編譯生成的exe檔案,雙擊執行後,建立新程序的地址空間,然後主執行緒開始執行。

程式一進來透過jmp指令來到前面列出的四個啟動函式,它們再呼叫最終的啟動器_tmainCRTStartup。

這個啟動器幹了幾件大事,分別是,使用GetStartupInfo獲取程序啟動資訊,然後使用_inititem初始化全域性變數和物件,最後呼叫我們main、wmain、WinMain、wWinMain進入我們的程式。。。

所以,從程式語言的角度來說,

main函式就是入口函式,這一點毋庸置疑

。至於mainCRTStartup,則是VC++這個編譯器額外增加的包含C/C++執行時庫初始化操作在內的封裝函式,可以算可執行檔案的入口函式。

說明:這裡談到的是使用VC2008編譯器生成的exe檔案形態(不同的VC版本可能情況有所不同),至於Linux上的ELF檔案,情況則更不一樣。

最後給大家留一個思考題:

程序建立後,又是從哪裡進入到OEP的呢?

前面我們說了,OEP是程式執行的入口,是一切的起點。那在進入入口之前,程序又在幹什麼?

這個問題有點類似於:在宇宙大爆炸之前,世界是怎麼樣的?

歡迎大家留言交流,告訴我你的想法!