《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

相关文章
|
12天前
|
存储 C++
【C++】string类的使用③(非成员函数重载Non-member function overloads)
这篇文章探讨了C++中`std::string`的`replace`和`swap`函数以及非成员函数重载。`replace`提供了多种方式替换字符串中的部分内容,包括使用字符串、子串、字符、字符数组和填充字符。`swap`函数用于交换两个`string`对象的内容,成员函数版本效率更高。非成员函数重载包括`operator+`实现字符串连接,关系运算符(如`==`, `<`等)用于比较字符串,以及`swap`非成员函数。此外,还介绍了`getline`函数,用于按指定分隔符从输入流中读取字符串。文章强调了非成员函数在特定情况下的作用,并给出了多个示例代码。
|
12天前
|
存储 编译器 C语言
【C++】string类的使用①(默认成员函数
本文介绍了C++ STL中的`string`类,它是用于方便地操作和管理字符串的类,替代了C语言中不便的字符数组操作。`string`基于`basic_string`模板,提供类似容器的接口,但针对字符串特性进行了优化。学习资源推荐[cplusplus.com](https://cplusplus.com/)。`string`类提供了多种构造函数,如无参构造、拷贝构造、字符填充构造等,以及析构函数和赋值运算符重载。示例代码展示了不同构造函数和赋值运算符的用法。
|
12天前
|
算法 C++ 容器
|
12天前
|
存储 编译器 程序员
|
13天前
|
存储 编译器 文件存储
|
17天前
|
编译器 C++
【C++】类和对象④(类的默认成员函数:取地址及const取地址重载 )
本文探讨了C++中类的成员函数,特别是取地址及const取地址操作符重载,通常无需重载,但展示了如何自定义以适应特定需求。接着讨论了构造函数的重要性,尤其是使用初始化列表来高效地初始化类的成员,包括对象成员、引用和const成员。初始化列表确保在对象创建时正确赋值,并遵循特定的执行顺序。
|
17天前
|
编译器 C++
【C++】类和对象③(类的默认成员函数:赋值运算符重载)
在C++中,运算符重载允许为用户定义的类型扩展运算符功能,但不能创建新运算符如`operator@`。重载的运算符必须至少有一个类类型参数,且不能改变内置类型运算符的含义。`.*::sizeof?`不可重载。赋值运算符`=`通常作为成员函数重载,确保封装性,如`Date`类的`operator==`。赋值运算符应返回引用并检查自我赋值。当未显式重载时,编译器提供默认实现,但这可能不足以处理资源管理。拷贝构造和赋值运算符在对象复制中有不同用途,需根据类需求定制实现。正确实现它们对避免数据错误和内存问题至关重要。接下来将探讨更多操作符重载和默认成员函数。
|
17天前
|
存储 编译器 C++
【C++】类和对象③(类的默认成员函数:拷贝构造函数)
本文探讨了C++中拷贝构造函数和赋值运算符重载的重要性。拷贝构造函数用于创建与已有对象相同的新对象,尤其在类涉及资源管理时需谨慎处理,以防止浅拷贝导致的问题。默认拷贝构造函数进行字节级复制,可能导致资源重复释放。例子展示了未正确实现拷贝构造函数时可能导致的无限递归。此外,文章提到了拷贝构造函数的常见应用场景,如函数参数、返回值和对象初始化,并指出类对象在赋值或作为函数参数时会隐式调用拷贝构造。