Windows API 常需回呼函數(__stdcall),但 C++ 成員函數隱含 this 指標(__thiscall),無法直接作為回呼。Thunk 技術可動態生成機器碼,將 this 載入 ECX 後跳轉至成員函數,橋接兩種呼叫慣例。
__thiscall 將 this 放入 ECX,參數右至左入棧,呼叫者清棧;__stdcall 參數同樣右至左入棧,但被呼叫者清棧。兩者棧平衡一致,僅差 this 傳遞方式。Thunk 只需兩條指令:
mov ecx, pThis
jmp member_func_address
#pragma pack(push, 1)
struct MemFunToStdCallThunk {
BYTE m_mov; // 0xB9 (mov ecx, imm32)
DWORD m_this; // this 指標
BYTE m_jmp; // 0xE9 (jmp rel32)
DWORD m_relproc; // 相對跳轉位移
BOOL Init(DWORD_PTR proc, void* pThis) {
m_mov = 0xB9;
m_this = PtrToUlong(pThis);
m_jmp = 0xE9;
m_relproc = DWORD((INT_PTR)proc - ((INT_PTR)this + sizeof(MemFunToStdCallThunk)));
FlushInstructionCache(GetCurrentProcess(), this, sizeof(*this));
return TRUE;
}
void* GetCodeAddress() { return this; }
};
#pragma pack(pop)
class CTestClass {
int m_nBase;
MemFunToStdCallThunk m_thunk;
void memFun(int m, int n) { /* ... */ }
public:
CTestClass() : m_nBase(10) {}
void Test() {
m_thunk.Init(UnionCastType<DWORD_PTR>(&CTestClass::memFun), this);
auto fun = (StdCallFun)m_thunk.GetCodeAddress();
fun(1, 3); // 觸發回呼
}
};
利用 union 進行型別懲罰,將成員函數指標轉為 DWORD_PTR:
template <typename TDst, typename TSrc>
TDst UnionCastType(TSrc src) {
union { TDst uDst; TSrc uSrc; } uMedia;
uMedia.uSrc = src;
return uMedia.uDst;
}
調用流程:參數入棧 → 跳入 Thunk → ECX 載入 this → 跳轉成員函數執行。需注意資料執行保護(DEP)環境下需確保記憶體可執行,FlushInstructionCache 確保指令快取一致性。