动态链接库 —— Dll 基础

摘要:
如果链接器检测到DLL的源文件输出了至少一个函数或变量,那么链接器还会生一个.lib文件,这个.lib文件非常小,这是因为它不包含任何函数或变量。

1. DLL 的初识

在 windows 中,动态链接库是不可缺少的一部分,windows 应用程序程序接口提供的所有函数都包含在 DLL 中,其中有三个非常重要的系统 DLL 文件,分别为 Kernel32.dllUser32.dllGDI32.dll,下面说下这三个重要的 DLL 的用途:

  • Kernel32.dll:包含的函数用来管理内存、进程以及线程。
  • User32.dll:包含的函数用来执行与用户界面相关的任务,如创建窗口和发送消息。
  • GDI32.dll:包含的函数用来绘制图像和显示文字。

当然,windows 还有其它一些 DLL,用来执行更加专门的任务。比如下面一些 DLL:

  • AdvAPI32.dll:包含的函数与对象的安全性、注册表的操控以及事件日志有关。
  • ComDlg32.dll:包含了一些常用的对话框(如打开文件和保存文件)。
  • ComCtl32.dll:支持所有常用的窗口控件。

2. 为何使用 DLL

下面简要说下使用 DLL 的一些理由:

  • 它们扩展了应用程序的特性。
  • 它们简化了项目管理。
  • 它们有助了节省内存。
  • 它们促进了资源的共享。
  • 它们促进了本地化。
  • 它们有助于解决平台间的差异。
  • 它们可以用于特殊目的(比如 HOOK 安装某些挂钩函数)。

3. DLL 和进程的地址空间

创建 DLL 比创建应用程序简单,DLL 中通常没有用来处理消息循环或创建窗口的代码,DLL 只不过是一组源代码模块,生成 DLL 文件时,需给链接器指定 DLL 开关,这个开关会使链接器在生成的 DLL 文件映像中保存一些与可执行文件略微不同的信息,这样 windows 加载器在加载它们时容易将它们区分开(PE 文件头结构中的文件属性字段会指出)。

如果一个应用程序或者是另外的 DLL 想去调用 DLL 里的函数,则必须将该 DLL 映射到调用进程的地址空间去,可以通过两种方式来调用,分别是隐式调用和显示调用,这两种调用方式以后会说到。

一旦系统将一个 DLL 的文件映像映射到调用进程的地址空间之后,进程中的所有线程就可以调用该 DLL 中的函数了。记住,当线程调用 DLL 中的一个函数的时候,该函数会在线程栈中取得传给它的参数,并使用线程栈来存放它需要的局部变量。此外,该 DLL 中的函数创建的任何对象都为调用线程或调用进程所拥有 —— DLL 绝对不会拥有任何对象。

4. 纵观全局

动态链接库 —— Dll 基础第1张

以上为 DLL 创建过程及应用程序隐式链接到 DLL 的过程,概括了各组件是如何结合到一起的。构建一个 DLL 步骤:

  • 必须先创建一个头文件,在其包含我们想要在 DLL 中导出的函数原型、结构以及符号。
  • 创建 C/C++ 源文件来实现想要在 DLL 模块导出的函数和变量。
  • 在构建该 DLL 模块的时候,编译器会对每个源文件进行处理并产生一个 .obj 模块(每一个源文件对应一个 .obj 模块)。
  • 当所有 .obj 模块都创建完毕后,链接器会将所有 .obj 模块的内容合并起来,产生一个单独的 DLL 映像文件。
  • 如果链接器检测到 DLL 的源文件输出了至少一个函数或变量,那么链接器还会生一个 .lib 文件,这个 .lib 文件非常小,这是因为它不包含任何函数或变量。它只是列出了所有被导出的函数和变量的符号名。

一旦 DLL 构建完成后,那么我们就可以去构建一个可执行模块来调用 DLL 中的函数和变量了,具体调用过程如下:

加载程序先为新的进程创建一个虚拟地址空间,并将可执行模块映射到新进程的地址空间中。加载程序接着解析可执行文件的导入段,也就是 PE 中的导入表,对导入表列出的每个 DLL,加载程序会在用户的系统中对该 DLL 模块进行定位,并将该 DLL 映射到进程的地址空间中。还要注意的一点就是,由于 DLL 模块可以从其它 DLL 模块中导入函数和变量,因此 DLL 模块可能有自已的导入表并需要将它所需的 DLL 模块映射到进程的地址空间中,这一过程可能会耗费更长的时间。一旦加载程序将可执行模块和所有的 DLL 模块映射到进程的地址空间之后,进程的主线程可以开始执行,这样应用程序就能够运行了。

4.1 构建 DLL 模块

打开 VS,我这里用的是 VS2015,新建项目,在 Visual C++ 选项卡下选择 Win32,右侧选择 Win32 控制台应用程序,然后给一个名称,如下:
动态链接库 —— Dll 基础第2张
点击确定后,选择 DLL,附加选择空项目,如下:
动态链接库 —— Dll 基础第3张
建立好之后,再建立一个头文件和一个源文件,如下:
动态链接库 —— Dll 基础第4张
然后以 MyDll.h 文件中输入如下代码:

#pragma once

// extern "C" 修饰符只有在编写 C++ 代码的时候,才会用到此修饰符
// 在编写 C 代码时不应该使用该修饰符,C++ 编译器通常会对函数名和变量名进行改编
// 如果一个 DLL 是用 C++ 编写的,而可执行文件是用 C 编写的,在构建 DLL 时
// 编译器会对函数名进行改编,但是在构建可执行文件时,编译器不会对函数名进行改编
// 当链接器试图链接可执行文件时,会发现可执行文件引用了一个不存在的符号并报错
// extern "C" 用来告诉编译器不要对变量名或函数名进行改编
// 那么这样用 C、C++ 或任何编程语言编写的可执行模块都可以访问该变量或函数
// 换句话说,是为了防止名称被粉碎
extern "C" __declspec(dllimport) int g_nResult;

extern "C" __declspec(dllimport) int Add(int nLeft, int nRight);

MyDll.cpp 文件中输入如下代码:

#include <windows.h>

#include "MyDll.h"

int g_nResult;

int Add(int nLeft, int nRight)
{
	g_nResult = nLeft + nRight;

	return g_nResult;
}

在代码完成后,点生成解决方案,这样它就会生成 Dll 文件,如下:
动态链接库 —— Dll 基础第5张
其中在头文件中还做了部分注释,还有部分说明后面再说。

4.2 构建可执行模块

我们先在解决方案下再创建一个新的工程来调用这个 Dll,这个调用是隐式调用,需要用到上图中的 MyDll.dllMyDll.lib 这两个文件,创建好后,再创建一个 cpp 源文件,如下:
动态链接库 —— Dll 基础第6张
MyDllTest.cpp 文件中输入如下代码:

#include <iostream>

#include "../MyDll/MyDll.h"

#pragma comment(lib, "../Debug/MyDll.lib")

int main()
{
	int nLeft = 10;
	int nRight = 25;

	std::cout << Add(nLeft, nRight) << std::endl;

	return 0;
}

然后我们去编译链接它,输出如下:
动态链接库 —— Dll 基础第7张
程序运行后得出了正确的答案,说明调用 Dll 中的 Add 函数成功,接下来要说明下代码中的意思。extern "C" 这个修饰符已在代码注释中说明,但这里还需要补充一下额外知识,C 编译器在对函数编译后,函数名不会发生改变,而 C++ 编译器不同,它在对函数编译后会在原函数名的基础上加上一个下划线,在最后面加上 @ 符号,其后跟上一个该函数形参所占用的总共字节数,比如:

__declspec(dllexport) LONG __stdcall MyFunc(int a, int b);

经过 C++ 编译器编译后,该函数名会发生改变,变为 _MyFunc@8,那 C++ 编译器为什么要这么做呢?原因是在 C++ 中,存在函数重载,而在 C 中不存在函数重载,所以在 C 中无需对函数名称进行粉碎,为了让 C++ 编译器不对函数名改编,需加下 extern "C",其实方法也不止这一种,还可以在你项目下建立一个 .def 文件,写下如下代码:

EXPORTS
    MyFunc

如果你不想用 .def 文件,我们还可以用另外一个方法来导出未经改编的函数名,我们可以在 Dll 的源文件中添加一行类似下面的代码:

#pragma comment(linker, "/export:MyFunc=_MyFunc@8")

这行代码传动使得编译器产生一个链接器指示符,该指示符告诉链接器要导出一个名为 MyFunc 的函数,该函数的入口点与 _MyFunc@8 相同。与上面的方法相比,这种方法相对来说不太方便,因为在写这行代码的时候,我们必须自己对函数名进行改编,这种方法并没有什么特别之处,它只不过能让我们避免使用 .def 文件而已。
接下来要说的是 __declspec(dllimport) 修饰符,当编译器看到用这个修饰符修饰的变量、函数原型或 C++ 类的时候,会在生成的 .obj 文件中嵌入一些额外的信息。当链接器在链接 Dll 所有的 .obj 文件时,会解析这些信息。
另外,在链接 Dll 的时候,链接器会检测到这些与导出的变量、函数或类有关的嵌入信息,并生成一个 .lib 文件。这个 .lib 文件列出了该Dll 导出的符号。在链接任何可执行模块的时候,只要可执行模块引用了该 Dll 导出的符号,这个 .lib 文件当然是必需的。

4.3 运行可执行模块

启动一个可执行模块的时候,操作系统的加载器会先为进程创建虚拟地址空间,接着把可执行模块映射到进程的地址空间中,之后加载程序会检查可执行模块的导入表,试图对所需的 Dll 进行定位并将它们映射到进程的地址空间中。

由于导入表只包含 Dll 的名称,不包含 Dll 的路径,因此加载程序必须在用户的磁盘上搜索 Dll,下面是加载程序的搜索的顺序:

  • 包含可执行文件的目录。
  • windows 的系统目录,该目录可以通过 GetSystemDirectory 得到。
  • windows 目录,该目录可以通过 GetWindowsDirectory得到。
  • 进程的当前目录。
  • PATH 环境变量中所列出的目录。
注意对应用程序当前目录的搜索位于 windows 目录之后,这个改变始于 windows xp sp2,其目的是为了防止加载程序在应用程序的当前目录中找到伪造的系统 Dll 并将它们载入,从而保证系统 Dll 始终都是从它们在 windows 目录中的正式位置载入的。 ## 5. 执行流程   随着加载程序将 Dll 模块映射到进程的地址空间中,它会同时检查每个 Dll 的导入表,如果一个 Dll 有导入表,那么加载程序会继续将所需的额外的 Dll 模块映射到进程的地址空间中。由于加载程序会对载入的 Dll 模块进行记录,因此即使多个模块用到了同一个模块,该模块也只会被载入和映射一次。

当加载程序将所有的 Dll 模块都载入并映射到进程的地址空间中后,它开始修复所有对导入符号的引用。为了完成这一工作,它会再次查看每个模块导入表,对导入表中列出的每个符号,加载程序会检查对应 Dll 的导出表,看该符号是否存在。如果该符号存在,那么加载程序会取得该符号的 RVA 并给它加上 Dll 模块被载入到的虚拟地址。接着加载程序会将这个虚拟地址保存到可执行模块的导入表中。

(完)

免责声明:文章转载自《动态链接库 —— Dll 基础》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇如何使用 eclipse进行断点 debug 程序压力测试下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

VBA学习_2:数组:同类型的多个变量的集合

声明数组:名称、数据类型和数组大小 1、通过起始和终止索引号定义数组大小   指定开始索引:Public | Dim 数组名称(a To b) As 数据类型, a和b为整数(不能是变量),分别表示数组的开始和终止索引号,可保存数据个个数(b-a+1)个   未指定开始索引:Public | Dim数组名称(a) As 数据类型  ,从0开始到a个元素...

Linux等待队列原理与实现

当进程要获取某些资源(例如从网卡读取数据)的时候,但资源并没有准备好(例如网卡还没接收到数据),这时候内核必须切换到其他进程运行,直到资源准备好再唤醒进程。 waitqueue (等待队列) 就是内核用于管理等待资源的进程,当某个进程获取的资源没有准备好的时候,可以通过调用  add_wait_queue() 函数把进程添加到  waitqueue 中,然...

yaml 文件中引用变量来读取 python 代码的设置值

在接口自动化测试的时候,yaml 文件一般放测试的数据或当配置文件使用,yaml 文件存放静态的数据是没问题的,python的数据类型基本上都是支持的。有时候我们想在 yaml 文件中引用变量来读取 python 代码的设置值。 一:yaml文件中是静态数据,那么,我们用python可以正常的读取。 在接口自动化中,yaml文件保存测试数据。 我们可以用p...

Linux 信号signal处理机制

信号是Linux编程中非常重要的部分,本文将详细介绍信号机制的基本概念、Linux对信号机制的大致实现方法、如何使用信号,以及有关信号的几个系统调用。 信号机制是进程之间相互传递消息的一种方法,信号全称为软中断信号,也有人称作软中断。从它的命名可以看出,它的实质和使用很象中断。所以,信号可以说是进程控制的一部分。 一、信号的基本概念 本节先介绍信号的一...

WSGI详解

WSGI接口 了解了HTTP协议和HTML文档,我们其实就明白了一个Web应用的本质就是: 浏览器发送一个HTTP请求; 服务器收到请求,生成一个HTML文档; 服务器把HTML文档作为HTTP响应的Body发送给浏览器; 浏览器收到HTTP响应,从HTTP Body取出HTML文档并显示。 所以,最简单的Web应用就是先把HTML用文件保存好,用...

bash 的环境配置文件

http://www.cnblogs.com/ggjucheng/archive/2012/11/01/2750179.html bash 的环境配置文件 你是否会觉得奇怪,怎么我们什么动作都没有进行,但是一进入 bash 就取得一堆有用的变量了? 这是因为系统有一些环境配置文件案的存在,让 bash 在启动时直接读取这些配置文件,以规划好 bash 的...