《C++避坑神器·十五》动态库只有dll文件,没有.lib文件时动态调用dll的中类和成员函数

简介: 《C++避坑神器·十五》动态库只有dll文件,没有.lib文件时动态调用dll的中类和成员函数

前言:

我们知道一个正常的动态库会包含三个文件,分别为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

参考3:https://www.cnblogs.com/kuangke/p/6155368.html

相关文章
|
28天前
|
C++
C/C++给文件加crc校验
C/C++给文件加crc校验
22 1
|
2月前
|
存储 C++
基于C++的简易文件压缩与解压缩工具设计与实现
基于C++的简易文件压缩与解压缩工具设计与实现
16 3
|
1天前
|
安全 编译器 C++
【C++类和对象】const成员函数及流插入提取
【C++类和对象】const成员函数及流插入提取
|
13天前
|
存储 编译器 C++
C++:类之六脉神剑——默认成员函数
C++:类之六脉神剑——默认成员函数
31 0
|
14天前
|
开发工具 对象存储 Android开发
对象存储oss使用问题之C++使用OSS SDK时遍历OSS上的文件时崩溃如何解决
《对象存储OSS操作报错合集》精选了用户在使用阿里云对象存储服务(OSS)过程中出现的各种常见及疑难报错情况,包括但不限于权限问题、上传下载异常、Bucket配置错误、网络连接问题、跨域资源共享(CORS)设定错误、数据一致性问题以及API调用失败等场景。为用户降低故障排查时间,确保OSS服务的稳定运行与高效利用。
24 0
|
14天前
|
存储 编译器 C++
【C++成长记】C++入门 | 类和对象(中) |拷贝构造函数、赋值运算符重载、const成员函数、 取地址及const取地址操作符重载
【C++成长记】C++入门 | 类和对象(中) |拷贝构造函数、赋值运算符重载、const成员函数、 取地址及const取地址操作符重载
|
14天前
|
编译器 C++
【C++成长记】C++入门 | 类和对象(中) |类的6个默认成员函数、构造函数、析构函数
【C++成长记】C++入门 | 类和对象(中) |类的6个默认成员函数、构造函数、析构函数
|
14天前
|
存储 编译器 C++
【C++成长记】C++入门 | 类和对象(上) |类的作用域、类的实例化、类的对象大小的计算、类成员函数的this指针
【C++成长记】C++入门 | 类和对象(上) |类的作用域、类的实例化、类的对象大小的计算、类成员函数的this指针
|
28天前
|
C++ Python
【C++/Python】C++调用python文件
【C++/Python】C++调用python文件
|
28天前
|
C++
【C++】C++封装成DLL并调用(初学者快速入门)
【C++】C++封装成DLL并调用(初学者快速入门)