65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.22/5 (10投票s)

2009年5月2日

CPOL

6分钟阅读

viewsIcon

45326

downloadIcon

1

DLL 的基础知识。

引言

从广义上讲,库是模块的集合,每个模块包含一个或多个函数或过程,它们被打包在一起以便在需要时可以轻松重用,在不需要时可以不使用。有应用程序 DLL 和系统 DLL。许多系统 DLL 被称为关键系统组件(kernel32.dlladvapi32.dllntdll.dllhal.dlluser32.dllgdi32.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;
}

生成此解决方案,然后选择“运行而不调试”

Capture.JPG

正如您所见,DLL 中包含的导出函数并未在 DLL 内部实现。我们构建了 SqTest 应用程序,并将 Square.dll 的导入库传递给了它。对 square 函数的调用会导致编译器和链接器在导入地址表中创建一个槽。加载程序随后将导入地址表槽用外部调用的函数的地址进行修补。

参考文献

  • Will Palo, MSDN

历史

  • 2009 年 5 月 2 日:初次发布
© . All rights reserved.