Thunk 技術指在程式運行時動態構造可執行機器碼——這原本是編譯器的工作。據《深度探索 C++ 物件模型》考證,「Thunk」一詞源自 Donald Knuth 姓氏倒寫,這位《計算機程式設計藝術》作者被譽為「編程聖經」撰寫者。
傳統 Thunk 寫法需手工查閱指令集,將機器碼填入字節陣列,再以函式指標跳轉執行。例如:
unsigned char code[9];
*((DWORD*)&code[0]) = 0x042444FF; // inc dword ptr [esp+4]
code[4] = 0xe9; // JMP
*((DWORD*)&code[5]) = (DWORD)&foo - (DWORD)&code[0] - 9;
void (*pf)(int) = (void(*)(int))&code[0];
pf(6); // 輸出 "In foo, a = 7"
此法極其繁瑣、易錯,且難以維護。
既然編譯器擅長將組語轉為機器碼,為何不直接寫內嵌組語,再由程式在運行時提取代碼段地址、替換動態數值?
關鍵難點在於跳轉偏移量等動態值在編譯期未知。解決方案:在組語中用特殊佔位符(如 0xFFFFFFFF)預留位置,運行時掃描替換為真實地址。
void ThunkTemplate(DWORD& addr1, DWORD& addr2) {
int flag = 0;
DWORD x1, x2;
if(flag) { // 永不執行,僅供編譯器生成代碼
__asm {
thunk_begin:
mov ecx, -1 // 佔位:this 指針
mov eax, -2 // 佔位:目標函式地址
jmp eax
thunk_end:
}
}
__asm {
mov x1, offset thunk_begin
mov x2, offset thunk_end
}
addr1 = x1; addr2 = x2;
}
void ReplaceCodeBuf(BYTE* code, int len, DWORD oldVal, DWORD newVal) {
for(int i=0; i<len-4; ++i)
if(*((DWORD*)&code[i]) == oldVal) {
*((DWORD*)&code[i]) = newVal;
return;
}
}
DWORD addr1, addr2;
ThunkTemplate(addr1, addr2);
memcpy(m_thunk, (void*)addr1, addr2-addr1);
ReplaceCodeBuf(m_thunk, addr2-addr1, -1, (DWORD)this); // 填入 this
ReplaceCodeBuf(m_thunk, addr2-addr1, -2, (DWORD)&CTimer::CallBack); // 填入函式地址
Windows 定時器回調 TimerProc 無額外參數傳遞 this,導致類成員函式無法直接作為回調。Thunk 技術完美破解:
this 指針(置入 ECX 或堆疊)文中完整展示了支援 __thiscall 與 __stdcall 兩種呼叫慣例的 Thunk 模板代碼,並封裝為 CTimer 類,實現「純成員函式作為回調」的優雅封裝。
本文提出的「編譯器生成模板 + 運行時修補」方案,徹底消除手工機器碼的痛苦,且保留靈活性。即使偏好傳統字節陣列寫法,也可先用此法生成代碼再導出數值。源碼附後,歡迎研究改進。