动态链接库 —— Dll 基础
Posted importthis
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动态链接库 —— Dll 基础相关的知识,希望对你有一定的参考价值。
1. DLL 的初识
在 windows 中,动态链接库是不可缺少的一部分,windows 应用程序程序接口提供的所有函数都包含在 DLL 中,其中有三个非常重要的系统 DLL 文件,分别为 Kernel32.dll
、User32.dll
和 GDI32.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 创建过程及应用程序隐式链接到 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,附加选择空项目,如下:
建立好之后,再建立一个头文件和一个源文件,如下:
然后以 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,这个调用是隐式调用,需要用到上图中的 MyDll.dll
、MyDll.lib
这两个文件,创建好后,再创建一个 cpp 源文件,如下:
在 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 中的 Add 函数成功,接下来要说明下代码中的意思。extern "C"
这个修饰符已在代码注释中说明,但这里还需要补充一下额外知识,C 编译器在对函数编译后,函数名不会发生改变,而 C++ 编译器不同,它在对函数编译后会在原函数名的基础上加上一个下划线,在最后面加上 @
符号,其后跟上一个该函数形参所占用的总共字节数,比如:
__declspec(dllexport) LONG __stdcall MyFunc(int a, int b);
经过 C++ 编译器编译后,该函数名会发生改变,变为 [email protected]
,那 C++ 编译器为什么要这么做呢?原因是在 C++ 中,存在函数重载,而在 C 中不存在函数重载,所以在 C 中无需对函数名称进行粉碎,为了让 C++ 编译器不对函数名改编,需加下 extern "C"
,其实方法也不止这一种,还可以在你项目下建立一个 .def
文件,写下如下代码:
EXPORTS
MyFunc
接下来要说的是 __declspec(dllimport)
修饰符,当编译器看到用这个修饰符修饰的变量、函数原型或 C++ 类的时候,会在生成的 .obj
文件中嵌入一些额外的信息。当链接器在链接 Dll 所有的 .obj
文件时,会解析这些信息。
另外,在链接 Dll 的时候,链接器会检测到这些与导出的变量、函数或类有关的嵌入信息,并生成一个 .lib
文件。这个 .lib
文件列出了该Dll 导出的符号。在链接任何可执行模块的时候,只要可执行模块引用了该 Dll 导出的符号,这个 .lib
文件当然是必需的。
以上是关于动态链接库 —— Dll 基础的主要内容,如果未能解决你的问题,请参考以下文章