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 创建过程及应用程序隐式链接到 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 文件,如下:



其中在头文件中还做了部分注释,还有部分说明后面再说。

4.2 构建可执行模块

  我们先在解决方案下再创建一个新的工程来调用这个 Dll,这个调用是隐式调用,需要用到上图中的 MyDll.dllMyDll.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++ 编译器编译后,该函数名会发生改变,变为 _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 基础的更多相关文章

  1. VC++动态链接库(DLL)编程深入浅出(转帖:基础班)

    1.概论 先来阐述一下DLL(Dynamic Linkable Library)的概念,你可以简单的把DLL看成一种仓库,它提供给你一些可以直接拿来用的变量.函数或类.在仓库的发展史上经历了“无库-静 ...

  2. VC++动态链接库(DLL)编程深入浅出(zz)

    VC++动态链接库(DLL)编程深入浅出(zz) 1.概论 先来阐述一下DLL(Dynamic Linkable Library)的概念,你可以简单的把DLL看成一种仓库,它提供给你一些可以直接拿来用 ...

  3. 【TensorFlow】:解决TensorFlow的ImportError: DLL load failed: 动态链接库(DLL)初始化例程失败

    [背景] 在scikit-learn基础上系统结合数学和编程的角度学习了机器学习后(我的github:https://github.com/wwcom614/machine-learning),意犹未 ...

  4. 创建一个动态链接库 (DLL),使用VS2010

    在本演练中,您将创建一个动态链接库 (DLL),其中包含可供其他应用程序使用的有用例程.使用 DLL 是一种重用代码的绝佳方式.您不必在自己创建的每个程序中重新实现这些例程,而只需对这些例程编写一次, ...

  5. VC++动态链接库(DLL)编程深入浅出(四)

    这是<VC++动态链接库(DLL)编程深入浅出>的第四部分,阅读本文前,请先阅读前三部分:(一).(二).(三). MFC扩展DLL的内涵为MFC的扩展,用户使用MFC扩展DLL就像使用M ...

  6. VC-基础:VC++动态链接库(DLL)编程深入浅出

    1.概论 先来阐述一下DLL(Dynamic Linkable Library)的概念,你可以简单的把DLL看成一种仓库,它提供给你一些可以直接拿来用的变量.函数或类.在仓库的发展史上经历了“无库-静 ...

  7. 编译可供C#调用的C/C++动态链接库dll文件

    编译可供C#调用的C/C++动态链接库dll文件,C语言控制台应用程序,探索生成dll过程 由于项目需求,需要公司另一个团队提供相关算法支持,是用C语言编译好的dll库提供给我们进行调用. 但是拿到d ...

  8. VS2010编写动态链接库DLL及单元测试用例,调用DLL测试正确性

    转自:http://blog.csdn.net/testcs_dn/article/details/27237509 本文将创建一个简单的动态链接库,并编写一个控制台应用程序使用该动态链接库,该动态链 ...

  9. 无法加载 DLL“rasapi32.dll”: 动态链接库(DLL)初始化例程失败。

    无法加载 DLL“rasapi32.dll”: 动态链接库(DLL)初始化例程失败. 在Asp.Net项目中使用WebClient或HttpWebRequest时出现以上错误 解决方案:把以下代码放在 ...

随机推荐

  1. oracle表名、字段名大小写问题。

    oracle  表名 .字段名 默认不区分大小写,除非建表语句中带双引号 如CREATE TABLE "TableName"("ID" number). CRE ...

  2. python之路——网络基础

    你现在已经学会了写python代码,假如你写了两个python文件a.py和b.py,分别去运行,你就会发现,这两个python的文件分别运行的很好.但是如果这两个程序之间想要传递一个数据,你要怎么做 ...

  3. kettle 创建任务定时执行数据抽取

    定时执行脚本 使用SPOON 工具建立好转换文件 .ktr,创建下面的.BAT文件,用操作系统的任务调用批处理. G:\soft\data-integration\pan.bat /norep -fi ...

  4. Python学习---Java和Python的区别小记

    Java和Python的区别小记 注意这里使用的是 and/or/not  非java中的&&,||,!Java中的true是小写 Python中函数就是对象,函数和我们之前的[1,2 ...

  5. Vscode rg.exe cpu 占用过高

    文件-> 首选项 -> 设置 -> 搜索search.followSymlinks 或者 修改settings.json 添加 "search.followSymlinks ...

  6. iOS开发之UIView

    在iPhone里你能看到的.摸到的,都是UIView. 视图坐标系统: UIKit中的坐标都是基于这样的坐标系统:以左上角为坐标的原点,原点向下和向右为坐标轴方向. 坐标值由浮点数来表示,内容的布局和 ...

  7. C#预定义类型、引用类型

    一.预定义的值类型 一个字节(1Byte)=8位(8Bit) BitArarry类可以管理位Bit. 1.整型 所有的整形变量都能用十进制或十六进制表示:long a=0x12AB 对一个整形值如未指 ...

  8. Aizu 2249 & cf 449B

    Aizu 2249 & cf 449B 1.Aizu - 2249 选的边肯定是最短路上的. 如果一个点有多个入度,取价值最小的. #include<bits/stdc++.h> ...

  9. Java基础知识强化之集合框架笔记76:ConcurrentHashMap之 ConcurrentHashMap简介

    1. ConcurrentHashMap简介: ConcurrentHashMap是一个线程安全的Hash Table,它的主要功能是提供了一组和Hashtable功能相同但是线程安全的方法.Conc ...

  10. Window窗口布局 --- DecorView浅析

    开发中,通常都是在onCreate()中调用setContentView(R.layout.custom_layout)来实现想要的页面布局,我们知道,页面都是依附在窗口之上的,而DecorView即 ...