DLL基础
编写一个简单的DLL
我们首先需要编写该DLL对应的头文件,该头文件可供可执行模块的代码使用,以便于可执行模块使用DLL中的函数和变量。
cpp
//MyLib.h
#ifdef MYLIBAPI
#else
#define MYLIBAPI extern "C" __declspec(dllimport)
#endif
MYLIBAPI int g_nResult;
MYLIBAPI int Add(int nLeft, int nRight);
我们使用了MYLIBAPI来修饰头文件中声明的变量和函数。如果在包含该头文件之前没有定义MYLIBAPI宏,那么该宏修饰的变量和函数说明这些对象是在别处定义的,需要导入才可以使用。
再来看看DLL对应的CPP文件:
cpp
//MyLib.cpp
#include <Windows.h>
#define MYLIBAPI extern "C" __declspec(dllexport)
#include "MyLib.h"
int g_nResult;
int Add(int nLeft, int nRight)
{
g_nResult = nLeft + nRight;
return g_nResult;
}
我们可以看到,在包含MyLib.h头文件之前,定义了MYLIBAPI宏,此时该宏修饰的对象是对外导出使用的。
两分代码中的extern "C"是为了避免C++编译器对函数名和变量名进行改编。
接下来使用如下命令生成DLL:
shell
cl /c .\MyLib.cpp
link /DLL .\MyLib.obj
使用/DLL说明该obj文件应该被当作一个DLL生成,链接器会在DLL文件中保存一些与可执行文件不同的信息,这样加载程序就能够将该文件映像识别为DLL而不是应用程序。
生成的DLL中包含了一张导出表,表中列出了DLL导出的变量和函数的RVA。
上述命令除了生成DLL文件之外,还生成了一个同名的.lib文件。如果链接器检测到DLL中导出了至少一个函数或变量,就会生成这个lib文件。这个文件列出了DLL中所有被导出的函数和变量符号名。这个文件对于生成最终的可执行文件是必须的,稍后我们可以看到如何使用它。
编写测试程序
我们来使用一下上一节写的DLL。
cpp
#include <Windows.h>
#include <strsafe.h>
#include <stdlib.h>
#include "MyLib.h"
int WINAPI WinMain(_In_ HINSTANCE, _In_opt_ HINSTANCE, _In_ LPTSTR, _In_ int)
{
int nLeft = 10, nRight = 25;
TCHAR sz[100];
StringCchPrintf(sz, _countof(sz), TEXT("%d + %d = %d"), nLeft, nRight,
Add(nLeft, nRight));
MessageBox(NULL, sz, TEXT("Last Result"), MB_OK);
StringCchPrintf(sz, _countof(sz), TEXT("The result from last Add is: %d"), g_nResult);
MessageBox(NULL, sz, TEXT("Last Result"), MB_OK);
return(0);
}
我们使用如下命令生成可执行文件:
shell
cl /D_WINDOWS .\MyExeFile1.cpp
link /SUBSYSTEM:WINDOWS .\MyExeFile1.obj user32.lib .\MyLib.lib
编译生成的obj文件中包含一个导入段,这里列出了该文件需要的所有DLL的名称,以及所引用的每个DLL中的变量和函数名称。加载程序会解析这个段,并将所需的DLL映射到该程序的进程地址空间之中。
程序运行
可执行文件的导入段中只有所需DLL的名称,并不包含其具体路径,加载程序会按照一定的顺序检索磁盘中的路径来寻找DLL。在找到所需的DLL后,加载程序会将其加载到进程的地址空间中,根据导出段的RVA和DLL在进程地址中加载的基址生成一个地址,利用该地址对可执行文件的导入段进行修改。
至此动态链接就完成了。这里介绍的是隐式载入时链接,这是最常用的链接。