在 C 語言中實現函數的動態調用(即在運行時根據名稱或地址調用任意函數)是一項進階技術,常用於解釋器、插件系統或腳本引擎。本文參考開源專案 UnderC 的實現,展示如何在 Windows 平台上透過內嵌組合語言處理不同調用慣例(stdcall、cdecl)、參數壓棧、返回值處理以及成員函數調用。
const int DC_STDCALL = 0; // 標準調用
const int DC_CDECL = 1; // C 語言預設調用
const int DC_QWORD = 2; // 64 位返回值(double/__int64)
const int DC_NOWORD = 4;
const int DC_RET_OBJ = 8; // 返回物件
const int DC_RET_VAL = 16 + 8; // 返回值於寄存器
void *gObjectReturnPtr = NULL; // 保留欄位
callfn此函數使用內嵌組合語言(MSVC 風格)手動構建堆棧幀:
thiscall(將 this 指針置於 ECX)void callfn(void* fn, int args[], int argc, void *optr, int flags, void *buff) {
int sz = sizeof(int) * argc;
_asm {
mov ecx, argc
mov ebx, args
// 逆序壓棧
a_loop:
cmp ecx, 0
jz a_out
mov eax, [ebx + 4*ecx]
push eax
dec ecx
jmp a_loop
a_out:
mov ecx, optr // thiscall: this 指針
call fn // 調用目標函數
// cdecl 需調用者清理堆棧
mov ecx, flags
test ecx, DC_CDECL
jz a_over
add esp, sz
a_over:
// 返回值處理...
}
}
Function 與 DllModuleFunction 類別封裝函數地址、調用慣例、this 指針及返回值緩衝區,並重載 operator() 以提供可變參數調用介面:
class Function {
void *addr;
int flags;
void *thisPtr;
union CalRet { __int64 ll; struct { int lo, hi; } i2; char b8[8]; } ret;
public:
Function(void *func, int flag) : addr(func), flags(flag), thisPtr(NULL) {}
Function(HMODULE hmod, const char *name, int flag)
: addr(GetProcAddress(hmod, name)), flags(flag), thisPtr(NULL) {}
Function(void *func, void *thisPtr, int flag)
: addr(func), thisPtr(thisPtr), flags(flag) {}
int operator()(int nargs, ...) {
callfn(addr, &nargs, nargs, thisPtr, flags, ret.b8);
return ret.i2.lo;
}
};
DllModule 封裝 LoadLibrary/FreeLibrary,提供 cFunc(cdecl)與 winFunc(stdcall)快捷建立 Function 物件:
class DllModule {
HMODULE hmodule;
char dllName[128];
public:
DllModule(const char *name) {
hmodule = LoadLibrary(name);
strncpy(dllName, name, 127);
}
~DllModule() { FreeLibrary(hmodule); }
void* get(const char *name) { return GetProcAddress(hmodule, name); }
Function cFunc(const char *name, void *thisPtr = NULL)
{ return Function(get(name), thisPtr, DC_CDECL); }
Function winFunc(const char *name, void *thisPtr = NULL)
{ return Function(get(name), thisPtr, DC_STDCALL); }
};
int main() {
DllModule cdll("msvcrt.dll");
cdll.cFunc("printf")(2, "Hello, %s\n", "baby"); // cdecl
DllModule user32("user32.dll");
user32.winFunc("MessageBoxW")(4, 0, L"Hello", L"OK", 0); // stdcall
// 成員函數調用
class A { public: void show(char *msg) { printf("msg:%s\n", msg); } };
A a;
// 取得成員函數地址需特殊處理
template<class T> void* addressof(T t) { void *ret; __asm { mov eax, t; mov ret, eax } return ret; }
Function(addressof(A::show), &a, DC_STDCALL)(1, "Class function call");
return 0;
}
this 放入 ECX(MSVC thiscall)。