理解 C++ 編譯器如何將函數呼叫轉換為組合語言指令,對實現反射、動態呼叫、Hook、序列化等底層機制至關重要。本文基於 x86 語彙 (VC6/VC7),系統梳理各類參數類型、呼叫慣例與類成員函數的堆疊/暫存器佈局。
__cdecl (呼叫端清理堆疊,參數右至左入堆)this 指標,VC 編譯器透過 ECX 傳遞 (__thiscall)EAX,浮點數放 ST(0),大型結構體由呼叫端配置空間並傳遞隱含指標void SetValue(LONG val); // 呼叫端:push val → call SetValue
// 函數內:mov eax, [ebp+8] 取得 val
LONG GetValue(LONG val); // 同樣入堆,返回值放 EAX
// 呼叫端:push val → call → mov result, eax
void SetValue(std::string val); // 呼叫端:在堆疊建構暫時物件 → call
// 函數內:lea eax, [ebp+8] 取得物件位址
std::string GetValue(...); // 呼叫端:配置返回值空間 → lea eax, [ret] → push eax → call
// 函數內:以 eax 為目標建構返回物件
void SetValue(std::string& val); // 呼叫端:lea eax, val → push eax → call
// 函數內:mov eax, [ebp+8] 取得參考位址
std::string GetValue(std::string& val); // 參考同樣傳址,返回值另行處理
thiscall)關鍵差異:ECX = this 指標,參數仍右至左入堆。
void X::SetValue(LONG val); // 呼叫端:push val → mov ecx, this → call
// 函數內:ecx=this, val=[ebp+8]
LONG X::GetValue(LONG val); // 同上,返回值放 EAX
void X::SetValue(std::string val); // 呼叫端:堆疊建構暫時物件 → mov ecx, this → call
// 函數內:ecx=this, lea eax, [ebp+8] 取暫時物件
std::string X::GetValue(...); // 呼叫端:配置返回空間 → lea eax, ret → push eax
// → mov ecx, this → call
void X::SetValue(std::string& val); // 呼叫端:lea eax, val → push eax → mov ecx, this → call
// 函數內:ecx=this, mov eax, [ebp+8]
std::string X::GetValue(std::string& val); // 參考+返回值雙重隱含指標
// 呼叫端:lea eax, val → push eax
// → lea eax, ret → push eax
// → mov ecx, this → call
利用上述規律,可構建繞過編譯器參數檢查的動態呼叫器:
// 初始化映射工廠
InitializeMappingFactory();
// 標準呼叫
IMVOKE_METHOD(NULL, &MessageBox, NULL, "hello", "title", MB_OK);
// 甚至可傳遞額外參數 (編譯器不檢查,執行期由被呼叫端忽略)
IMVOKE_METHOD(NULL, &MessageBox, NULL, "hello", "title", MB_OK, "extra1", "extra2");
實現核心:手工構造堆疊幀、設定 ECX、跳轉目標位址,需精確對應上述慣例。
RCX, RDX, R8, R9XMM0~XMM3__cdecl/__stdcall 之分)this 仍放 RCX (第一參數位置)掌握呼叫慣例細節,是從「使用庫」進階到「構建基礎設施」的關鍵門檻,也是逆向工程、熱修補、語言互操作的核心能力。