原文地址:http://www.codeproject.com/dll/classesexportedusingLL.asp
引言
我见过相当多的用来说明在程序中如何使用从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类中提供的函数:
- 第一步是使用LoadLibrary将Calc.DLL加载到你的程序中。
HMODULE hMod = LoadLibrary ("Calc.dll");
if (NULL == hMod)
{
printf ("LoadLibrary failed\n");
return 1;
}
- 因为你有Calc.DLL的头文件,所以下一步就是分配一个与类大小匹配的内存块,然后调用构造函数代码。
CCalc *pCCalc = (CCalc *) malloc (sizeof (CCalc));
if (NULL == pCCalc)
{
printf ("memory allocation failed\n");
return 1;
}
但是在C++中我们为什么要使用malloc而不用new呢?这是因为new操作符会调用CCalc's的默认构造函数,而我们根本访问不到它。记住,我们必须要动态地加载DLL,因此在build时没有定义CCalc类的构造函数。
因此,我们仅仅获得了一块与CCalc类大小相等的未初始化的内存。
- 如果你使用Dumpbin.exe(位于Microsoft Visual Studio\VC98\Bin文件夹下)来查看输出函数,你会看到DLL的一个输出函数列表(我已经使用一个DEF文件修复了函数名)。如下图所示:
列表包含了函数Add, Sub, GetLastUsedFunc和构造函数的虚拟内存地址。
前面我们已经获得了一块内存,现在必须调用构造函数对其进行初始化,所以我们要获取构造函数在DLL中的相对虚拟地址(RVA)。
PCTOR pCtor = (PCTOR) GetProcAddress (hMod, "CCalc");
if (NULL == pCtor)
{
printf ("GetProcAddress failed\n");
return 1;
}
PCTOR是一个在UserOfCalc.cpp中定义的函数指针,其定义代码如下:
typedef void (WINAPI * PCTOR) ();
- 现在有了构造函数的地址,接下来就应该要调用它来初始化前面用malloc分配的那块内存。但怎样才能使一个对象和这个构造函数关联起来呢?
如果你还记得,当任何成员函数(包括构造函数)被调用时,对象的地址会自动传递到被调用函数,而且这个地址存储在栈中。在基于Intel的机器上,这个对象地址通过ECX寄存器被压入栈顶,所以当你创建一个类并调用其成员函数时,ECX寄存器包含‘this’指针。下面的截图会使问题清晰一点。
你应该注意到汇编窗口中当前执行指令
LEA ECX, [EBP-4]
完成时, ECX和‘&bmw’的值是相同的。在不同的处理器架构的机器上,使用的寄存器可能不是ECX,这里只是举例说明。
- 回到我们的Calc.dll,已经有了内存块(以后的对象)的地址,现在用内嵌汇编语句将这个地址拷贝到ECX寄存器:
__asm { MOV ECX, pCCalc }
- 现在我们已经获得了构造函数的地址,只须要:
pCtor ();
- 当你的函数指针pCtor()从DLL中返回时,它已经完成了该对象的初始化。
- 要调用Calc类的其它任何成员函数,只须再次拷贝pCalc到ECX并获取输出函数载进程中的地址,然后直接调用即可。你观察任何简单类的反汇编代码都会发现,在每次成员函数调用之前总有一个汇编指令将‘this’拷贝到ECX。这就是我们前面所做的事。在《使用……》一文中,作者在“代码”的第三点提到了“使用一个DEF文件修复了函数名”,但是并没有讲解什么是DEF文件,也没有说明应该如何修复,可能会使某些初学者(包括我自己)感到疑惑。我也上网搜索了一下,讲解DEF文件作用以及详细使用方法的文章不多且比较零散,本文在此用一个简单例子简单阐述一下DEF文件一般的使用方法,以方便需要者查阅。
DEF文件的全称是Module-Definition File,即模块定义文件,是用来定义EXE和DLL文件的一种文件格式,以文本形式保存(可用记事本创建/编辑)。由于链接器为大多数模块定义声明提供了对应的命令行选项,所以一般的Win32程序并不需要.DEF文件。但是在编写DLL时,尤其是在编写C++的DLL时,(由于名称修饰)DEF文件还是有它的用武之地的。
※注:关于“名称修饰”在很多地方都有介绍,文中不作讲解。
DEF文件的主要内容是由一系列的声明(statement)组成,包括NAME、LIBRARY、DISCRIPTION、STACKSIZE、SECTIONS、EXPORTS、VERSION。
¨ NAME:指定输出文件的文件名,设置image基址
¨ LIBRARY(DLL):指定DLL的内部名称和加载时的基址
¨ DISCRIPTION:文件描述
¨ STACKSIZE:设置栈的大小
¨ SECTIONS:设置image文件的一个或多个段属性
¨ EXPORTS:定义输出列表
¨ VERSION:指定文件版本
其中最常用的是LIBRARY、EXPORTS和DISCRIPTION。
示例
1. 现在有一个已经编写好的类要用DLL输出,并通过函数名对DLL进行动态调用。其头文件和源文件如下:
Header File:
#ifdef LIBDLL_EXPORTS
#define LIBDLL_API __declspec(dllexport)
#else
#define LIBDLL_API __declspec(dllimport)
#endif
#include <iostream.h>
// This class is exported from the LibDll.dll
class LIBDLL_API CTest
{
int data;
public:
CTest();
void print();
};Source File:
#include "stdafx.h"
#include "LibDll.h"
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
CTest::CTest()
{
this->data = 0;
}
void CTest::print()
{
cout<<"The member function print() is from a DLL.\n";
}
从代码中可以看到,DLL中定义了一个CTest类,它有一个构造函数和一个成员函数print()。2. 然后新建一个Win32 Console Application来调用DLL。
Header File:
#define LIBDLL_API __declspec(dllimport)
#include <iostream.h>
// This class is exported from the LibDll.dll
class LIBDLL_API CTest
{
int data;
public:
CTest();
void print();
};Source File:
#include "stdafx.h"
#include <iostream.h>
#include <malloc.h>
#include <windows.h>
#include "LibDll.h"
typedef void (WINAPI *PCTOR)();
typedef void (*PPRINT)();
inline void CTest_print(HMODULE, CTest*);
inline void CTest_CTest(HMODULE, CTest*);
int main(int argc, char* argv[])
{
//加载DLL
HMODULE hmod = LoadLibrary("LibDll.dll");
if(hmod == NULL)
{
cout<<"Failed loading DLL.\n";
return 1;
}
//创建类对象
CTest* pCTest = (CTest*)malloc(sizeof(CTest));
//初始化CTest对象
CTest_CTest(hmod, pCTest);
//调用成员函数
CTest_print(hmod, pCTest);
FreeLibrary(hmod);
free(pCTest);
cout<<"Press [Enter] to exit.";
cin.peek();
return 0;
}//end main
void CTest_print(HMODULE hMod, CTest* pObj)
{
PPRINT pprint = (PPRINT)GetProcAddress(hMod, "print");
if(pprint == NULL)
{
cout<<"Function print() not found.\n";
}
else
{
__asm{ MOV ECX, pObj}
pprint();
}
}
void CTest_CTest(HMODULE hMod, CTest* pObj)
{
PCTOR pCtor = (PCTOR)GetProcAddress(hMod, "CTest");
if(pCtor == NULL)
{
cout<<"Function CTest() not found.\n";
}
else
{
__asm{ MOV ECX, pObj}
pCtor();
}
}本来到这里就应该可以正常运行了,但是你会发现在执行CTest_CTest函数时会提示"Function CTest() not found."。为什么会找不到函数呢?那是因为C++编译器在生成DLL时对输出函数的名称进行来“修饰”,所以DLL中的函数名称已经不再是我们在代码中所写的函数名,这个时候就需要用DEF文件来进行“函数名称修复”。
3. 用Dumpbin的/EXPORTS参数打开LibDll.Dll,截图如下:
其中1和3就是CTest类的构造函数和print()函数的实际名称(吓人吧……)。然后我们在DLL的工程目录中新建一个“LibDll.def”文件,并在“工程->设置->Link”中添加参数(/def:".\libdll.def"),并编
辑DEF文件内容如下:
LIBRARY LibDll
EXPORTS
CTest = ??0CTest@@QAE@XZ
print = ?print@CTest@@QAEXXZ相信你已经看出来了,这实际上是一个函数名映射。
再用Dumpbin打重新编译得到的DLL文件,截图如下:
图中的4和5就是修复后的函数名。
现在再运行第二步中的程序就可以成功地调用DLL里的函数了!
尾注
文中只使用了DEF文件的一小部分功能,详细资料请参见MSDN。
按照MSDN上的说法,有三种方法可以用来输出函数,按推荐顺序如下:
- 在源代码中使用__declspec(dllexport)关键字(调用工程需包含*.lib)
- 使用DEF文件中的EXPORTS声明(不需要*.lib,可实现动态调用)
- 在LINK命令中使用/EXPORT参数(效果和DEF文件相同)
具体应该使用哪种方法,还应该视用途由使用者自己决定。