《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

相关文章
|
2月前
|
程序员 C++ 容器
在 C++中,realloc 函数返回 NULL 时,需要手动释放原来的内存吗?
在 C++ 中,当 realloc 函数返回 NULL 时,表示内存重新分配失败,但原内存块仍然有效,因此需要手动释放原来的内存,以避免内存泄漏。
|
2月前
|
存储 前端开发 C++
C++ 多线程之带返回值的线程处理函数
这篇文章介绍了在C++中使用`async`函数、`packaged_task`和`promise`三种方法来创建带返回值的线程处理函数。
83 6
|
2月前
|
C++
C++ 多线程之线程管理函数
这篇文章介绍了C++中多线程编程的几个关键函数,包括获取线程ID的`get_id()`,延时函数`sleep_for()`,线程让步函数`yield()`,以及阻塞线程直到指定时间的`sleep_until()`。
45 0
|
2月前
|
Linux C++
Linux c/c++文件的基本操作
在Linux环境下使用C/C++进行文件的基本操作,包括文件的创建、写入、读取、关闭以及文件描述符的定位。
25 0
Linux c/c++文件的基本操作
|
2月前
|
编译器 C语言 C++
C++入门3——类与对象2-2(类的6个默认成员函数)
C++入门3——类与对象2-2(类的6个默认成员函数)
39 3
|
2月前
|
编译器 C语言 C++
详解C/C++动态内存函数(malloc、free、calloc、realloc)
详解C/C++动态内存函数(malloc、free、calloc、realloc)
388 1
|
2月前
|
存储 编译器 C++
C++入门3——类与对象2-1(类的6个默认成员函数)
C++入门3——类与对象2-1(类的6个默认成员函数)
51 1
|
2月前
|
编译器 C语言 C++
C++入门6——模板(泛型编程、函数模板、类模板)
C++入门6——模板(泛型编程、函数模板、类模板)
70 0
C++入门6——模板(泛型编程、函数模板、类模板)
|
2月前
|
Linux C++
Linux c/c++文件虚拟内存映射
这篇文章介绍了在Linux环境下,如何使用虚拟内存映射技术来提高文件读写的速度,并通过C/C++代码示例展示了文件映射的整个流程。
65 0
|
2月前
|
Linux C++
Linux c/c++文件移动
这篇文章介绍了在Linux环境下,使用C/C++语言通过命令方式和文件操作方式实现文件移动的方法。
93 0