近期接觸了一些逆向工程的技術,仔細的了解了 C 語言決定入口點的機制,在此作整理。所謂入口點(Entry Point)指的是程式啟動後第一個被執行指令的位址。對一般的 programmar 而言,程式的入口點就是 main
函式,在 Windows 環境下有可能為WinMain
,這都是最基本的常識。然而事實上 main
並不是這個程式第一個被執行的函式。
這時我們需要先對入口點做更嚴謹的定義。首先,一個程式是如何啟動的?方法有很多,常見的有:在檔案總管中雙按執行檔、在命令提示字元中輸入程式名稱、在程式中呼叫 CreateProcess
啟動另一個程式,其實這三種方法最終都是透過呼叫 CreateProcess
來啟動程式,而 CreateProcess
中,系統會先將環境建立好,之後在跳轉到程式的入口點。
入口點要如何得知?入口點其實就紀錄在執行檔中。那執行檔的入口點是如何決定? 這就是連結器(Linker)的工作了。以下我就針對兩種最常見的 linker 進行說明。
第一個例子為 Visual C++ 的 linker,預設會根據 /SUBSYSTEM
這個 flag 來選擇進入點,若指定了 /SUBSYSTEM:CONSOLE
,linker會選擇 mainCRTStartup
作為進入點,若指定了 /SUBSYSTEM:WINDOWS
,linker會選擇 WinMainCRTStartup
作為進入點。若要使用非預設的入口點,則需指定 /ENTRY
這個 flag。
由此可知 VC 的 linker 選擇進入點的順位如下:
/ENTRY
指定的函式/SUBSYSTEM
的值- 若沒有指定
/SUBSYSTEM
,則看main
與WinMain
使否有定義,若main
有定義則使用mainCRTStartup
,若WinMain
有定義則使用WinMainCRTStartup
。
第二個例子為 gcc 的 linker,這邊直接列選擇入口點的順位:
-e
選項所指定的位址- linker script 中
ENTRY()
所指定的位址 - 若
start
符號有定義,則使用start
所指定的位址 - 若有 .text 這個 section,則指定
.text
區段第一個byte的位址為入口點 - 使用位址 0 作為入口點
以上兩個例子參考了這兩個說明文件:
那實際上該如何驗證? gcc 為了支援多個平台因此原始碼比較複雜,因此這邊選擇對 Visual C++ 的 CRT 原始碼做研究。
VC 的CRT的原始碼就放在VC的安裝資料夾中,我的電腦上的路徑為:C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\crt\src
其中我們感興趣的程式碼都位於 vcruntime這個資料夾中,其中有三個檔案為我們的目標:
- exe_common.inl =>實作初始化 C runtime的程式碼
- vcstartup_internal.h =>
mainCRTStartup
與WinMainCRTStartup
的宣告 - exe_main.cpp =>
mainCRTStartup
的實作(定義) - exe_winmain.cpp =>
WinMainStartup
的實作(定義)
exe_main.cpp 與 exe_winmain.cpp 都非常簡短,直接列出來:
1 | //exe_main.cpp |
1 | //exe_winmain.cpp |
exe_common.inl 中實作了CRT的初始化,其實不管入口點為何,CRT初始化都是使用同一段程式碼,差別只在最後呼叫的是 main 還是 WinMain 而已。CRT的初始化又是另一個題目了,往後有機會再研究。
研究CRT原始碼讓人相當過癮,以往使用 OllyDbg 時,往往對main之前的那一大段程式碼不知其所以然,現在配合原始碼一對照,可以清清楚楚地知道程式每一步在做的是什麼,往後也許會寫一篇關於CRT啟動程式碼的文章。