引言

我见过相当多的用来说明在程序中如何使用从DLL中输出的class的代码,但这些方法都是通过隐式链接完成的。回忆一下DLL的概念,有两种方法可以使用DLL中输出的函数:一是在程序代码中简单地引用DLL中符号,这使得加载器在程序启动时隐式地加载(链接)所需的DLL,这就是众所周知的“隐式链接”。

第二种方法就是在程序运行过程中显式地加载所需的DLL(使用LoadLibrary())并且显式地链接到需要的输出符号。换句话说,如果程序要调用DLL中的一个函数,可以显式地加载一个DLL到她的进程地址空间,然后获得函数在DLL中的虚拟内存地址,并利用这个地址来调用函数。这种方法的优美之处就在于所有的工作都是在程序运行过程中完成的,并且程序可以从进程地址空间中卸载不再需要的DLL。这种方法就是“显式链接”。

背景

前面窝已经介绍了函数的调用方法,但是怎么使用输出类呢?对于隐式链接的DLL,调用类和调用函数没有什么区别;而在一般情况下,想要显示加载DLL并使用其中的类是不可能的。但是我写这篇文章并不是为了告诉你为什么这不可能,而是要告诉你如何来实现它。对了!就是使用LoadLibrary()。

在继续下文之前我想告诉你,以下的代码很粗糙,如果你准备将其用于你的项目中,请先征得你老板的同意。但是这些代码不仅用于让你加深理解,在实在没有办法的情况下也不失为一种极端的解决方法。

代码

在示例代码中,我创建了一个名为Calc.DLL计算器DLL,并在一个名为UserOfcalc的命令行程序中使用其提供的计算功能。

// Calc.DLL包含了一个名为CCalc的输出类
//  它包含3个方法,分别是 Add、Sub和GetLastFunc()
 
// CALC.H – CCalc类声明
 
#include <tchar.h>
 
#ifdef CALC_EXPORTS
#define CALC_API __declspec (dllexport)
#else
#define CALC_API __declspec (dllimport)
#endif
 
#define SOME_INSTN_BUF        260
 
class CALC_API CCalc
{
private:
TCHAR m_szLastUsedFunc[SOME_INSTN_BUF];
 
public:
    CCalc (); 
 
    int Add (int i, int j);
    int Sub (int i, int j);
    TCHAR* GetLastUsedFunc ();
 
};

DLL的实现部分在文件Calc.cpp中:

#include "Calc.h"
#include <windows.h>
 
BOOL APIENTRY DllMain (HANDLE, DWORD, LPVOID)
{
    return TRUE;
}
 
// 构造函数,初始化m_szLastFuncCalled数组
CCalc::CCalc ()
{
 
    memset (m_szLastUsedFunc, 0, sizeof (m_szLastUsedFunc));
    strcpy (m_szLastUsedFunc, "No function used yet");
}
 
 
int CCalc::Add (int i, int j)
{
    strcpy (m_szLastUsedFunc, "Add used");
    return (i + j);
}
 
int CCalc::Sub (int i, int j)
{
    strcpy (m_szLastUsedFunc, "Sub used");
    return (i - j);
}

现在,通过以下步骤可以显式地加载DLL并使用Calc类中提供的函数:

  1. 第一步是使用LoadLibrary将Calc.DLL加载到你的程序中。
HMODULE hMod = LoadLibrary ("Calc.dll");
if (NULL == hMod)
{
    printf ("LoadLibrary failed/n");
    return1;
}
  1. 因为你有Calc.DLL的头文件,所以下一步就是分配一个与类大小匹配的内存块,然后调用构造函数代码。
CCalc *pCCalc = (CCalc *) malloc (sizeof (CCalc));
if (NULL == pCCalc)
{
    printf ("memory allocation failed/n");
    return1;
}

但是在C++中我们为什么要使用malloc而不用new呢?这是因为new操作符会调用CCalc's的默认构造函数,而我们根本访问不到它。记住,我们必须要动态地加载DLL,因此在build时没有定义CCalc类的构造函数。

因此,我们仅仅获得了一块与CCalc类大小相等的未初始化的内存。

  1. 如果你使用Dumpbin.exe(位于Microsoft Visual Studio/VC98/Bin文件夹下)来查看输出函数,你会看到DLL的一个输出函数列表(我已经使用一个DEF文件修复了函数名)。如下图所示:

列表包含了函数Add, Sub, GetLastUsedFunc和构造函数的虚拟内存地址。

前面我们已经获得了一块内存,现在必须调用构造函数对其进行初始化,所以我们要获取构造函数在DLL中的相对虚拟地址(RVA)。

PCTOR pCtor = (PCTOR) GetProcAddress (hMod, "CCalc");注:这里也许需要.dll文件中CCalc函数的实际名称,具体查询方法可以参考:
if (NULL == pCtor)
{
    printf ("GetProcAddress failed/n");
    return1;
}

PCTOR是一个在UserOfCalc.cpp中定义的函数指针,其定义代码如下:

typedefvoid (WINAPI * PCTOR) ();

  1. 现在有了构造函数的地址,接下来就应该要调用它来初始化前面用malloc分配的那块内存。但怎样才能使一个对象和这个构造函数关联起来呢?

如果你还记得,当任何成员函数(包括构造函数)被调用时,对象的地址会自动传递到被调用函数,而且这个地址存储在栈中。在基于Intel的机器上,这个对象地址通过ECX寄存器被压入栈顶,所以当你创建一个类并调用其成员函数时,ECX寄存器包含‘this’指针。下面的截图会使问题清晰一点。

你应该注意到汇编窗口中当前执行指令

LEAECX, [EBP-4]

完成时,ECX和‘&bmw’的值是相同的。在不同的处理器架构的机器上,使用的寄存器可能不是ECX,这里只是举例说明。

  1. 回到我们的Calc.dll,已经有了内存块(以后的对象)的地址,现在用内嵌汇编语句将这个地址拷贝到ECX寄存器:

__asm { MOVECX, pCCalc }

  1. 现在我们已经获得了构造函数的地址,只须要:

pCtor ();

  1. 当你的函数指针pCtor()从DLL中返回时,它已经完成了该对象的初始化。
  2. 要调用Calc类的其它任何成员函数,只须再次拷贝pCalc到ECX并获取输出函数载进程中的地址,然后直接调用即可。你观察任何简单类的反汇编代码都会发现,在每次成员函数调用之前总有一个汇编指令将‘this’拷贝到ECX。这就是我们前面所做的事。

注:测试的时候也许会出现“获取函数地址失败!”,具体解决方法有2个

方法一:在文中已经说过,可以查看.dll文件中函数的实际地址

方法二:修改Calc.dll的CCalc函数,在函数前加上extern "C",再编译Calc.dll文件所在的工程,复制新生成的Calc.dll覆盖工程目录下的Calc.dll,原来的代码获取函数地址时使用CCalc,结果运行就成功了。