原文連結:http://www.titilima.cn/?action=show&id=275
在一般情況下,動態調用DLL匯出函數的方法是:
- 用typedef為目標函數定義函數指標類型。
- 用GetProcAddress擷取函數指標。
- 用函數指標進行調用。
但是,如果要調用的函數太多的話,這個方法難免流於繁瑣——有太多的typedef、太多的GetProcAddress和太多的函數指標。在本文中將給出一個通用的解決方案,使這些動態調用更加簡便。
先看看我們這個函數的聲明:
- BOOL __cdecl DllCall(
- PCTSTR lpszDll, // 目標函數所在DLL的名稱
- PCSTR lpszFunc, // 目標函數名稱
- int argc, // 要調用的參數個數
- PVOID pRet, // 函數調用的傳回值
- ...
- );
以MessageBoxA為例,使用方法為:
- int ret;
- DllCall(_T("user32.dll"), "MessageBoxA", 4, &ret,
- NULL, "Hello, World!", "Hello", MB_ICONINFORMATION | MB_YESNO);
換用一個參數的MessageBoxIndirectA,則是:
- MSGBOXPARAMSA param;
- ZeroMemory(¶m, sizeof(MSGBOXPARAMSA));
- param.cbSize = sizeof(MSGBOXPARAMSA);
- param.dwLanguageId = GetSystemDefaultLangID();
- param.dwStyle = MB_ICONINFORMATION;
- param.lpszCaption = "Hello";
- param.lpszText = "Hello, World";
-
- int ret;
- DllCall(_T("user32.dll"), "MessageBoxIndirectA", 1, &ret, ¶m);
實現的原理是動態產生彙編代碼,也就是類似這樣的一段:
- __declspec(naked) DWORD __cdecl DllCallProc(void)
- {
- __asm
- {
- push argn
- ...
- push arg2
- push arg1
- call proc
- ret
- };
- }
下面列出DllCall的代碼,和所有可變參數函數的實現(如sprintf)都差不多。
- BOOL __cdecl DllCall(
- PCTSTR lpszDll,
- PCSTR lpszFunc,
- int argc,
- PVOID pRet,
- ...)
- {
- va_list arglist;
- int ret;
-
- va_start(arglist, pRet);
- ret = vDllCall(lpszDll, lpszFunc, argc, pRet, arglist);
- va_end(arglist);
- return ret;
- }
最為關鍵的就是vDllCall的代碼了,如下:
- #pragma pack(push, 1)
- typedef struct {
- BYTE op;
- DWORD_PTR dwValue;
- } OPCODE, *POPCODE;
- #pragma pack(pop)
- typedef DWORD (__cdecl * DLLCALL)(void);
- BOOL __cdecl vDllCall(
- PCTSTR lpszDll,
- PCSTR lpszFunc,
- int argc,
- PVOID pRet,
- va_list arglist)
- {
- HMODULE hDll = LoadLibrary(lpszDll);
- if (NULL == hDll)
- return FALSE;
- FARPROC proc = GetProcAddress(hDll, lpszFunc);
- if (NULL == proc)
- return FALSE;
- HANDLE hHeap = GetProcessHeap();
- POPCODE p = (POPCODE)HeapAlloc(hHeap, 0, sizeof(OPCODE) * (argc + 2));
- int i;
- for (i = argc - 1; i >= 0; --i)
- {
- // push arg[i]
- p[i].op = 0x68;
- p[i].dwValue = va_arg(arglist, DWORD_PTR);
- }
- // call proc
- p[argc].op = 0xe8;
- p[argc].dwValue = (INT_PTR)proc - (INT_PTR)&p[argc + 1];
- // ret
- p[argc + 1].op = 0xc3;
- p[argc + 1].dwValue = 0x90909090; // nop nop nop nop
- DLLCALL pfn = (DLLCALL)p;
- DWORD ret = pfn();
- HeapFree(hHeap, 0, p);
- FreeLibrary(hDll);
- if (NULL != pRet)
- *(PDWORD)pRet = ret;
- return TRUE;
- }
其中的指標p就是我們動態產生的調用代碼,最後轉換成DLLCALL類型的函數指標進行了調用。
最後,需要補充說明四點:
- DllCall只適用於__stdcall呼叫慣例的目標函數。
- 這份vDllCall的代碼只適用於x86的CPU,如果在WinCE的環境下(如arm或mips的CPU)使用,需要酌情重新編寫動態調用的彙編代碼。
- 其中argc參數是指實際壓棧的參數個數,而不是C語言調用的參數個數。如API函數WindowFromPoint,雖然函式宣告中只有一個參數,但是實際上是將POINT::x、POINT::y分別壓棧的。在這種情況下,需要將argc設定為2。
- DllCall的傳回值只擷取了eax,如果有的函數會返回一個超過4位元組的龐大結構,那麼這個傳回值將並不是你想要的。