首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> VC >> 關於VC++

C++ 成員函數指標取址與直接呼叫:突破類型系統限制的實用技巧

     編輯:關於VC++

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(&TT::foo) 皆編譯錯誤 C2440。

方法一:Union 類型懲罰 (跨編譯器相容)


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);

  • ✅ 支援 VC6、VC8、GCC、Clang
  • ✅ 可取得 private/protected 成員函數位址 (Union 不檢查存取權限)
  • ⚠️ 依賴 sizeof(To) == sizeof(From),單繼承非虛函數通常滿足

方法二:內嵌組語 (僅 VC8+/x86)


#define GetMemberFuncAddr_VC8(addr, func) \
    __asm { mov eax, offset func } \
    __asm { mov addr, eax }
  • ✅ 直接取得位址,無類型轉換
  • ✅ 可取得 private 成員 (組語區塊繞過存取檢查)
  • ❌ VC6 編譯失敗 (不支援 offset 成員函數)
  • ❌ x64 禁用內嵌組語

呼叫約定與 this 指標傳遞

慣例this 傳遞參數壓棧順序VC 預設
__thiscallECX 暫存器右至左✅ 成員函數預設
__stdcall最後壓棧 (最左參數)右至左WinAPI
__cdecl最後壓棧右至左C 函數

⚠️ VC6 保留 __thiscall 關鍵字但不可用 (編譯錯誤:reserved for future use)。

直接呼叫成員函數位址

__thiscall 型 (VC 預設)


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");      // 像普通函數呼叫

__stdcall/__cdecl 型


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

通用呼叫封裝:CallMemberFunc


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=其他)

限制與風險

  • ❌ **假設所有參數佔 4 位元組棧空間** — double (8 bytes)、struct、float (產生 FPU 指令) 均不支援。
  • ❌ **無參數型別檢查**,錯誤參數導致棧損毀、崩潰。
  • ❌ **x64 完全不適用** (呼叫約定、暫存器、棧佈局全不同)。
  • ✅ 僅適合「參數皆為整數/指標/CHAR/SHORT/INT/LONG」的簡單成員函數。

繼承與虛函數行為分析

單繼承非虛函數


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 的虛函數表動態分派,**完美保持多態語意**。

多重繼承

  • 成員函數指標大小可能變為 8/16/24 位元組 (含 this 調整位移、虛基類別偏移等)。
  • Union/組語取址僅取得前 4 位元組,**資訊遺失導致呼叫錯誤**。
  • 需手工處理 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 反組譯分析、多重繼承失效示範,請下載附件。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved