前言:
我们知道一个正常的动态库会包含三个文件,分别为dll,lib和.h文件,通过在项目属性中进行正常的配置便能够隐式的调用,具体调用方法参考我前面写的文章:
《C++避坑神器·九》小白也能轻易掌握动态链接库DLL的使用
有时候第三方提供的动态库只有一个dll和.h文件,没有.lib文件该如何调用?
下面讲解一种方法让应用程序显式加载所需的 DLL(使用调用),并在应用程序运行时显式链接到所需的导出符号。换句话说,如果应用程序决定要调用 DLL 中的函数,则可以将 DLL 显式加载到进程的地址空间中,获取 DLL 中包含的函数的虚拟内存地址,然后使用此内存地址调用函数。此技术的美妙之处在于,一切都在应用程序运行时完成,并且应用程序可以在完成对 DLL 的工作后从其进程地址空间卸载 DLL。您可能已经猜到了,这种技术称为显式链接。LoadLibrary()
注意:下面提供的一种方法是基于汇编的一种黑客技术,过程仅供参考!!!
假如有个calculate.dll,其.h和.cpp文件如下:
//calculate.h #ifdef CALC_EXPORTS #define CALC_API __declspec (dllexport) #else #define CALC_API __declspec (dllimport) #endif class CALC_API calculate { public: calculate(){} int Add (int a, int b); int Sub (int a, int b); };
//calculate.cpp int calculate::Add(int i, int j) { return (i + j); } int calculate::Sub(int i, int j) { return (i - j); }
现在,通过以下步骤可以显式地加载DLL并使用calculate类中提供的函数:
1、第一步是使用LoadLibrary将calculate.dll加载到你的程序中
HMODULE hMod = LoadLibrary ("calculate.dll"); if (NULL == hMod) { printf ("LoadLibrary failed\n"); return; }
2、通过头文件知道其中的类,所以下一步就是分配一个与类大小匹配的内存块,然后调用构造函数代码。
calculate*pCCalc = (calculate*)malloc(sizeof(calculate)); if (NULL == pCCalc) { printf ("memory allocation failed\n"); return ; }
但是在C++中我们为什么要使用malloc而不用new呢?这是因为new操作符会调用calculate的默认构造函数,而我们根本访问不到它。记住,我们必须要动态地加载DLL,因此在build时没有定义calculate类的构造函数。
因此,我们仅仅获得了一块与calculate类大小相等的未初始化的内存。
3、利用Dumpbin.exe修复类中的函数名。
虽然我们能在头文件中看到成员函数的声明,但这些函数的名字在dll中并不是我们看到的这样。
首先,我们需要利用Dumpbin工具把类中所有的函数全部解析出来。
可以通过左下角菜单中打开vs的dumpbin工具:
输入dumpbin -exports dll路径:
该列表包含成员函数Add、Sub和构造函数calculate的虚拟内存地址,1,3,5后面一串乱码实际是构造函数,Add,Sub对应的函数名称,现在我们要用def文件来修正它。那什么是def文件?
DEF文件的全称是Module-Definition File,即模块定义文件,是用来定义EXE和DLL文件的一种文件格式,以文本形式保存(可用记事本创建/编辑)。由于链接器为大多数模块定义声明提供了对应的命令行选项,所以一般的Win32程序并不需要.DEF文件。但是在编写DLL时,尤其是在编写C++的DLL时,(由于名称修饰)DEF文件还是有它的用武之地的。
我们在自己的项目中新建一个“calculate.def”文件,并在“工程->设置->Link”中添加参数(/def:“.\calculate.def”),并编
辑DEF文件内容如下:
LIBRARY calculate EXPORTS calculate = ??0CCalc@@QAE@XZ Add = ?Add@CCalc@QAEHHH@Z Sub = ?Sub@CCalc@@QAEHHH@Z
这实际上是一个函数名映射。我们正常传入的函数指针比如传入Add,实际是传入?Add@CCalc@QAEHHH@Z
前面我们已经获得了一块内存,现在必须调用构造函数对其进行初始化,所以我们要获取构造函数在DLL中的相对虚拟地址(RVA)。
//定义一个函数指针 typedef void (WINAPI * PCTOR) (); PCTOR pCtor = (PCTOR) GetProcAddress (hMod, "calculate"); if (NULL == pCtor) { printf ("GetProcAddress failed\n"); return; }
通常在调用GetProcAddress 函数时会显示空指针异常。为什么会找不到函数呢?那是因为C++编译器在生成DLL时对输出函数的名称进行来“修饰”,所以DLL中的函数名称已经不再是我们在代码中所写的函数名,这就为什么需要DEF文件来进行“函数名称修复”。
4、现在有了构造函数的地址,接下来就应该要调用它来初始化前面用malloc分配的那块内存。
如果你还记得,当任何成员函数(包括构造函数)被调用时,对象的地址会自动传递到被调用函数,而且这个地址存储在栈中。在基于Intel的机器上,这个对象地址通过ECX寄存器被压入栈顶,所以当你创建一个类并调用其成员函数时,ECX寄存器包含‘this’指针。目前已经有了内存块(以后的对象)的地址,现在用内嵌汇编语句将这个地址拷贝到ECX寄存器。
__asm { MOV ECX, pCCalc }
现在我们已经获得了构造函数的地址,只须要:
pCtor ();
当你的函数指针pCtor()从DLL中返回时,它已经完成了该对象的初始化
注意:
x64编译模式下不支持__asm的汇编嵌入,只有32位支持。从网上的资料上查到虽然不能直接嵌入汇编程序段,但是可以将程序段全部放到一个asm文件下进行编译,最后asm文件编译生成的obj文件和.cpp文件编译生成的obj文件链接到一起就可以生成exe文件了。
x64位下调用asm函数的案例:
创建一个test.asm文件:
.CODE func PROC MOV EAX, 2023;返回2023 RET func ENDP END
把asm文件添加到项目中,对asm文件右击——属性——按图中方式设置:
然后新建一个.h文件来对汇编程序中的代码作声明。这里建立一个名为test.h的头文件。写入如下声明信息
#ifndef __ASMCODE_H #define __ASMCODE_H extern "C" { int _stdcall func(); } #endif
然后可以正常在main函数中调用:
int main(void) { int value = func(); //2023 }
在结尾处我在提供另一种比较简单的方法:
在dll中创建一个基类,里面用纯虚函数声明,并再写一个子类去重写基类中的纯虚函数,头文件中只需要放上基类的所有纯虚函数的声明,外部项目调用dll时只需要包含头文件,项目中在放入dll,用多态的方式创建对象即可直接调用dll中的所有方法。
参考1:https://www.cnblogs.com/IamEasy_Man/archive/2009/10/20/1587096.html
参考2:https://www.codeproject.com/Articles/9405/Using-classes-exported-from-a-DLL-using-LoadLibrar