原文链接:http://www.titilima.cn/?action=show&id=275
在一般情况下,动态调用DLL导出函数的方法是:
- 用typedef为目标函数定义函数指针类型。
- 用GetProcAddress获取函数指针。
- 用函数指针进行调用。
但是,如果要调用的函数太多的话,这个方法难免流于繁琐——有太多的typedef、太多的GetProcAddress和太多的函数指针。在本文中将给出一个通用的解决方法,使这些动态调用更加简便。
先看看我们这个函数的声明:
- BOOL__cdeclDllCall(
- PCTSTRlpszDll,//目标函数所在DLL的名称
- PCSTRlpszFunc,//目标函数名称
- intargc,//要调用的参数个数
- PVOIDpRet,//函数调用的返回值
- ...
- );
以MessageBoxA为例,使用方法为:
- intret;
- DllCall(_T("user32.dll"),"MessageBoxA",4,&ret,
- NULL,"Hello,World!","Hello",MB_ICONINFORMATION|MB_YESNO);
换用一个参数的MessageBoxIndirectA,则是:
- MSGBOXPARAMSAparam;
- ZeroMemory(¶m,sizeof(MSGBOXPARAMSA));
- param.cbSize=sizeof(MSGBOXPARAMSA);
- param.dwLanguageId=GetSystemDefaultLangID();
- param.dwStyle=MB_ICONINFORMATION;
- param.lpszCaption="Hello";
- param.lpszText="Hello,World";
- intret;
- DllCall(_T("user32.dll"),"MessageBoxIndirectA",1,&ret,¶m);
实现的原理是动态生成汇编代码,也就是类似这样的一段:
- __declspec(naked)DWORD__cdeclDllCallProc(void)
- {
- __asm
- {
- pushargn
- ...
- pusharg2
- pusharg1
- callproc
- ret
- };
- }
下面列出DllCall的代码,和所有可变参数函数的实现(如sprintf)都差不多。
- BOOL__cdeclDllCall(
- PCTSTRlpszDll,
- PCSTRlpszFunc,
- intargc,
- PVOIDpRet,
- ...)
- {
- va_listarglist;
- intret;
- va_start(arglist,pRet);
- ret=vDllCall(lpszDll,lpszFunc,argc,pRet,arglist);
- va_end(arglist);
- returnret;
- }
最为关键的就是vDllCall的代码了,如下:
- #pragmapack(push,1)
- typedefstruct{
- BYTEop;
- DWORD_PTRdwValue;
- }OPCODE,*POPCODE;
- #pragmapack(pop)
- typedefDWORD(__cdecl*DLLCALL)(void);
- BOOL__cdeclvDllCall(
- PCTSTRlpszDll,
- PCSTRlpszFunc,
- intargc,
- PVOIDpRet,
- va_listarglist)
- {
- HMODULEhDll=LoadLibrary(lpszDll);
- if(NULL==hDll)
- returnFALSE;
- FARPROCproc=GetProcAddress(hDll,lpszFunc);
- if(NULL==proc)
- returnFALSE;
- HANDLEhHeap=GetProcessHeap();
- POPCODEp=(POPCODE)HeapAlloc(hHeap,0,sizeof(OPCODE)*(argc+2));
- inti;
- for(i=argc-1;i>=0;--i)
- {
- //pusharg[i]
- p[i].op=0x68;
- p[i].dwValue=va_arg(arglist,DWORD_PTR);
- }
- //callproc
- 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;//nopnopnopnop
- DLLCALLpfn=(DLLCALL)p;
- DWORDret=pfn();
- HeapFree(hHeap,0,p);
- FreeLibrary(hDll);
- if(NULL!=pRet)
- *(PDWORD)pRet=ret;
- returnTRUE;
- }
其中的指针p就是我们动态生成的调用代码,最后转换成DLLCALL类型的函数指针进行了调用。
最后,需要补充说明四点:
- DllCall只适用于__stdcall调用约定的目标函数。
- 这份vDllCall的代码只适用于x86的CPU,如果在WinCE的环境下(如arm或mips的CPU)使用,需要酌情重新编写动态调用的汇编代码。
- 其中argc参数是指实际压栈的参数个数,而不是C语言调用的参数个数。如API函数WindowFromPoint,虽然函数声明中只有一个参数,但是实际上是将POINT::x、POINT::y分别压栈的。在这种情况下,需要将argc设置为2。
- DllCall的返回值只获取了eax,如果有的函数会返回一个超过4字节的庞大结构,那么这个返回值将并不是你想要的。