《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
在这里插入图片描述

📢博客主页: 主页
📢欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正!
📢本文由 梦回阑珊 原创,首发于 CSDN,转载注明出处🙉
📢代码改变世界,你来改变代码!✨

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

梦回阑珊

一毛不嫌多,一分也是爱

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值