理解 DLL - 构建它们并允许外部调用它们的函数






4.22/5 (10投票s)
DLL 的基础知识。
引言
从广义上讲,库是模块的集合,每个模块包含一个或多个函数或过程,它们被打包在一起以便在需要时可以轻松重用,在不需要时可以不使用。有应用程序 DLL 和系统 DLL。许多系统 DLL 被称为关键系统组件(kernel32.dll、advapi32.dll、ntdll.dll、hal.dll、user32.dll、gdi32.dll 等)。如果您曾用 C 语言编程,您就会知道,与任何面向对象的编程语言不同,组织的基本单位是函数;C 是一种函数式或过程式(非面向对象)语言。编译器将包含指令(在源代码主体顶部包含的头文件),其中包含预定义函数的原型。头文件的名称基于该头文件中定义的语义相关函数的类型。
这些头文件链接在一起形成一个静态库,而不是动态库。C 编译器将人类可读的源代码转换为机器语言。输出是资源文件、对象文件或模块文件。但是,即使函数的定义未包含在源代码的主体中,C 编译器也无法生成它未看到的函数的地址。您将静态库作为输入传递给链接器,链接器会搜索所有未解析的外部符号。这就是为什么将源文件保存在与包含库(包含预定义函数的头文件定义)的目录相同的目录中很重要。另一方面,DLL 不同。DLL 本身有一个单独的链接步骤。链接 DLL 时,它会生成一个导入库。此导入库按名称(有时按编号)列出该库中包含的所有函数。
假设您有一个图形应用程序,您调用 CreateWindow
在屏幕上放置一个窗口。当编译器生成该模块时,它不知道 CreateWindow
在哪里。因此,我们构建我们的应用程序,并将 user32.dll 的导入库传递给它。此导入库中没有函数的实现。导入库会响应调用,说明 CreateWindow
是所需的函数,并且它位于 user32.dll 中。编译器和链接器不会将 CreateWindow
解析为绝对(相对)地址,而是会在导入地址表中创建一个槽,稍后加载程序将用 CreateWindow
的地址修补该槽。因此,当您的构建的应用程序加载时,Windows 会看到您有一些函数依赖于 user32.dll。因此,它会在进程地址空间中加载 user32.dll(代码和数据在此映射以表示应用程序的实例并运行应用程序)。请注意,只有 DLL 的部分被“虚拟加载”:并非整个 DLL。这可能是一个误解:加载的可执行文件意味着只有它的部分被加载,而不是整个映像。只有它需要的那些部分才会被加载。随着应用程序使用更多功能,它的更多部分以及相应的 DLL 会被加载。这是 Windows 内存管理机制的一部分——任何可以共享的内存都将被共享。一旦 Windows 将 user32.dll 加载到您的地址空间,它就知道 CreateWindow
的地址在哪里。然后它会查找导入地址表,意识到必须解析 CreateWindow
的地址,并在该点进行修补。
创建 DLL 的步骤
如果您使用 Visual Studio 2005 或 2008,请从新项目开始,选择 Win32 项目。对于应用程序设置,选择 DLL 并勾选“空项目”。将项目命名为 Squares
。向导关闭后,右键单击“添加新项”并添加一个 C++ 源文件。将源文件名命名为 Squares_cpp.cpp,如下所示
#include <windows.h />
__declspec(dllexport) double WINAPI square(double x)
{
return x * x;
}
__declspec(dllexport) float __stdcall square(float x)
{
return x * x;
}
这两个函数计算平方:一个计算双精度(十进制),另一个计算浮点数。如果我们希望将这些函数提供给 DLL 外部的调用者,那么我们必须导出它们。这意味着使用修饰符 __declspec(dllexport)
。WINAPI 是来自头文件的修饰符,用于表示标准的调用约定。我们将再添加两个源文件,一个用 C 语言,另一个用 Windows C/C++。但首先生成解决方案。不要尝试“开始调试”。我们只定义了两个函数。我们的目标是使它们可用,以便另一个应用程序可以从外部调用它们。现在,因为我们使用的是 IDE,所以我们现在需要转到 C++ 编译器所在的命令提示符,或者设置环境变量路径
PATH=%PATH%;.;C:\Program Files\Microsoft Visual Studio 9.0\vc\bin
使用我们从 Squares 项目文件夹中复制粘贴的 DLL,我们将使用一个命令来检查此 DLL 的导出项
c:\Program Files\Microsoft Visual Studio 9.0\VC\bin>vcvars32.bat
Setting environment for using Microsoft Visual Studio 2008 x86 tools.
c:\Program Files\Microsoft Visual Studio 9.0\VC\bin>dumpbin /exports Square.dll
Microsoft (R) COFF/PE Dumper Version 9.00.21022.08
Copyright (C) Microsoft Corporation. All rights reserved.
Dump of file Square.dll
File Type: DLL
Section contains the following exports for Square.dll
00000000 characteristics
49FCB08F time date stamp Sat May 02 16:43:59 2009
0.00 version
1 ordinal base
2 number of functions
2 number of names
ordinal hint RVA name
1 0 00011005 ?square@@YGMM@Z = @ILT+0(?square@@YGMM@Z)
2 1 0001100A ?square@@YGNN@Z = @ILT+5(?square@@YGNN@Z)
(the C++ compiler performs name decoration, or name mangling)
Summary
1000 .data
1000 .idata
2000 .rdata
1000 .reloc
1000 .rsrc
4000 .text
10000 .textbss
上面的内容看起来可能毫无意义。我们使用了 dumpbin.exe 工具并转储了 Squares.dll 的导出项。但请注意 C++ 编译器执行的名称修饰。因此,我们将使用“undname.exe”工具。但是我们如何将这些难以辨认的字符复制到命令行呢?右键单击 DOS 提示符的角落,选择标记,然后拖动它直到它选中此 DLL 中包含的第一个函数的所有修饰过的名称。然后按 Enter 键,然后右键单击鼠标并单击粘贴,将该数据传输到包含 undname.exe 命令的命令行
c:\Program Files\Microsoft Visual Studio 9.0\VC\bin>undname ?square@@YGMM@Z
Microsoft (R) C++ Name Undecorator
Copyright (C) Microsoft Corporation. All rights reserved.
Undecoration of :- "?square@@YGMM@Z"
is :- "float __stdcall square(float)"
c:\Program Files\Microsoft Visual Studio 9.0\VC\bin>undname ?square@@YGNN@Z
Microsoft (R) C++ Name Undecorator
Copyright (C) Microsoft Corporation. All rights reserved.
Undecoration of :- "?square@@YGNN@Z"
is :- "double __stdcall square(double)"
这些是 DLL 中包含的两个函数。因为我们使用了 C++ 编译器,所以名称被“修饰”以定义返回类型信息。我们用 C 语言编写的函数不会被修饰。关于 DLL 的这个基本方面的要点是调用约定。我们使用的是标准调用约定,因此我们还将添加一个简单的模块定义文件。这是添加的 C 文件。再次右键单击并添加新项,将其命名为 Square_c.c,然后将其复制并粘贴到 UI 中。
#include <windows.h>
int WINAPI square(int n)
{
return n * n;
}
现在让我们添加一个模块定义文件。右键单击解决方案,然后选择“添加模块定义文件”。
LIBRARY "Squares"
EXPORTS
square
默认情况下,文件名是 Squares.def。现在我们将添加最后一个文件,名为 main.cpp。此文件的目的应该是不言而喻的。如果还不清楚,请继续阅读此文件并记住其格式
#include <windows.h>
HINSTANCE ghInstance;
BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID)
{
switch ( dwReason )
{
case DLL_PROCESS_ATTACH:
ghInstance = hInstance;
break;
case DLL_PROCESS_DETACH:
break;
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
}
return TRUE;
}
现在生成整个解决方案,但不要尝试将其作为可执行文件运行。这是一个动态链接库,其中包含另一个应用程序(我们将构建它)将通过外部调用来使用的函数。解决方案构建完成后,将其保留在 IDE 中,然后单击“添加新项目”。将其创建一个空的 Win32 控制台应用程序项目,并命名为 SqTest
。再次,右键单击解决方案容器并选择“添加新项”。选择一个 *.cpp 源文件,然后将此文件粘贴到空的 IDE 中
#include <windows.h>
#include <iostream>
__declspec(dllimport) double WINAPI square(double);
__declspec(dllimport) float WINAPI square(float);
extern "C" { __declspec(dllimport) int WINAPI square(int); };
int main()
{
int n = 1;
float x = 2.0f;
double y = 3.0l;
std::cout << "square of 1 is " << square(n) << std::endl;
std::cout << "square of 2.0f is " << square(x) << std::endl;
std::cout << "square of 3.0l is " << square(y) << std::endl;
return 0;
}
生成此解决方案,然后选择“运行而不调试”

正如您所见,DLL 中包含的导出函数并未在 DLL 内部实现。我们构建了 SqTest
应用程序,并将 Square.dll 的导入库传递给了它。对 square
函数的调用会导致编译器和链接器在导入地址表中创建一个槽。加载程序随后将导入地址表槽用外部调用的函数的地址进行修补。
参考文献
- Will Palo, MSDN
历史
- 2009 年 5 月 2 日:初次发布