C++ 成員函數指標 (T (Class::*)(Args...)) 受嚴格類型系統保護:不可轉換為 void*、DWORD 或普通函數指標,呼叫語法繁瑣 ((obj.*ptr)(args)、(pObj->*ptr)(args))。但底層開發 (Hook、回呼、序列化、動態載入) 常需取得成員函數位址並像普通函數般呼叫。本文詳述兩種取址方法、呼叫約定處理、虛函數/繼承/多重繼承行為分析,並提供通用呼叫封裝 CallMemberFunc。
class TT { public: void foo(int x) { printf("%d\n", x); } };
using FUNC = void (TT::*)(int);
FUNC p = &TT::foo;
TT obj; (obj.*p)(5); // 物件呼叫
TT* pObj = &obj; (pObj->*p)(6); // 指標呼叫
嘗試 (DWORD)&TT::foo 或 reinterpret_cast 皆編譯錯誤 C2440。
template
void GetMemberFuncAddr_VC6(To& addr, From f) {
union { From _f; To _t; } u;
u._f = f;
addr = u._t;
}
用法:DWORD ptr; GetMemberFuncAddr_VC6(ptr, &TT::foo);
sizeof(To) == sizeof(From),單繼承非虛函數通常滿足
#define GetMemberFuncAddr_VC8(addr, func) \
__asm { mov eax, offset func } \
__asm { mov addr, eax }
offset 成員函數)| 慣例 | this 傳遞 | 參數壓棧順序 | VC 預設 |
|---|---|---|---|
__thiscall | ECX 暫存器 | 右至左 | ✅ 成員函數預設 |
__stdcall | 最後壓棧 (最左參數) | 右至左 | WinAPI |
__cdecl | 最後壓棧 | 右至左 | C 函數 |
⚠️ VC6 保留 __thiscall 關鍵字但不可用 (編譯錯誤:reserved for future use)。
using FUNC_STD = void (__stdcall*)(int, char, const char*); // 普通函數指標型別
TT obj; obj.m_a = 123;
DWORD ptr; GetMemberFuncAddr_VC6(ptr, &TT::foo);
FUNC_STD fn = (FUNC_STD)ptr;
DWORD thisPtr = (DWORD)&obj;
__asm mov ecx, thisPtr; // 準備 this
fn(5, 'a', "xyz"); // 像普通函數呼叫
using FUNC_STD = void (__stdcall*)(void* thisPtr, int, char, const char*); // 額外第一參數
TT obj; obj.m_a = 123;
DWORD ptr; GetMemberFuncAddr_VC6(ptr, &TT::foo_stdcall);
FUNC_STD fn = (FUNC_STD)ptr;
fn(&obj, 5, 'a', "xyz"); // 手動傳 this
DWORD CallMemberFunc(int callFlag, DWORD funcAddr, void* pThis, int argCount, ...)
{
DWORD ret = 0;
if (argCount > 0) {
__asm {
mov ecx, argCount
mov edx, ecx
shl edx, 2 // edx = argCount * 4
add edx, 0x14 // 偏移至第一個可變參數 (ebp+0x14)
nextArg:
push dword ptr [ebp+edx]
sub edx, 4
dec ecx
jnz nextArg
}
}
if (callFlag == 0) { // __thiscall
__asm mov ecx, pThis;
} else { // __stdcall/__cdecl
__asm push pThis;
}
__asm {
call funcAddr
mov ret, eax
}
return ret;
}
用法:CallMemberFunc(0, ptr1, &obj, 3, 5, 'a', "xyz"); (0=__thiscall, 1=其他)
double (8 bytes)、struct、float (產生 FPU 指令) 均不支援。
class Base { public: void foo() { puts("Base::foo"); } };
class Derived : public Base { public: void bar() { puts("Derived::bar"); } };
// &Derived::foo 編譯器自動替換為 &Base::foo,行為正常
class Base { virtual void vfoo() { puts("Base::vfoo"); } };
class Derived : public Base { void vfoo() override { puts("Derived::vfoo"); } };
DWORD pBase, pDerived;
GetMemberFuncAddr(pBase, &Base::vfoo); // 取得 vcall thunk 位址
GetMemberFuncAddr(pDerived, &Derived::vfoo); // 取得另一 vcall thunk
Base b; Derived d;
Call(pBase, &b); // 輸出 Base::vfoo
Call(pDerived, &b); // 輸出 Base::vfoo (thunk 解析 this->vptr[0])
Call(pBase, &d); // 輸出 Derived::vfoo (多態生效!)
Call(pDerived, &d); // 輸出 Derived::vfoo
原理:編譯器不回傳真實函數位址,而是回傳 vcall thunk (匯編 mov eax,[ecx]; jmp [eax] 或 jmp [eax+4]),執行時依 this 的虛函數表動態分派,**完美保持多態語意**。
this 調整位移、虛基類別偏移等)。this 指標調整 (加減位移)、虛基類別查找,**無通用簡單方案**。| 場景 | 建議方案 |
|---|---|
| 單繼承、非虛/虛函數、參數簡單 | Union 取址 + CallMemberFunc / 手工組語準備 this |
| 需支援 double/struct 參數 | 定義匹配的普通函數指標型別,強轉後手動準備 this |
| 多重繼承、虛基類別 | **避免直接取址呼叫**,改用 std::function/std::bind/lambda 封裝 |
| x64 / 現代 C++ (C++11+) | 使用 std::mem_fn、std::bind、std::function、Lambda 捕獲 this |
成員函數指標的底層操作屬於「黑魔法」,除非極度必要 (舊代碼維護、無標頭檔的二進制 Hook、極致效能熱路徑),**強烈建議使用標準 C++ 可呼叫物件抽象層**。完整原始碼含 Union/組語取址、CallMemberFunc、虛函數 thunk 反組譯分析、多重繼承失效示範,請下載附件。