背景
DLL 相当于 Linux 下的共享对象。PE 格式的二进制文件。
Windows 系统中大量采用 DLL 机制,包括内核、软件更新包、ActiveX 技术等。
DLL (Dynamic-Link Library)
DLL 即动态链接库的缩写,
进程拥有独立的地址空间,当某个 DLL 被加载到地址空间中,所有的程序都可以共享这个 DLL。一个 DLL 在不同的进程中拥用不同的私有数据副本。
基地址和 RVA
DLL 的代码段不是地址无关的
- 基地址
PE 文件被装载时,其进程地址空间中的起始地址就是基地址。每个 PE 文件都有一个优先装载的基地址。
- 相对地址 RVA
Windows 装载 DLL 时,先尝试把他装载到基地址,若该地址区域被占用,选择其他空闲地址。相对地址就是一个地址相对于基地址的偏移
DLL 共享数据段
Windows 允许将 DLL 数据段设置成共享,即任何进程都可以共享该 DLL 的同一份数据段
- DLL 共享数据段可以实现进程间通信
- 安全漏洞很大
- 尽量避免使用 DLL 共享数据段来实现进程间的通信
导出/导入
与 ELF 文件不同,DLL 默认情况下所有的符号都不导出,需要显式的指明要导出的符号,在应用程序中使用 DLL 导出的符号过程,被称为导入。
- 『__declspec(dllexport)』 导出关键字
- 『__declspec(dllimport)』 导入关键字
- 使用『.def』 文件来声明导入导出符号
符号导出导入表
当一个 PE 文件需要将一些函数或变量提供给其他 PE 文件使用时,这种行为叫做符号导出。
- 导出表
所有导出的符号被击中存放在导出表中,提供符号名与符号地址的映射关系。
- 序号
- EXP 文件
创建 DLL 的同事也会得到一个 EXP 文件,是链接器在创建 DLL 时的临时文件。
- 导入表
某个程序使用到了 DLL 的函数或变量,叫做符号导入
- 延迟载入
等你链接一个支持延迟载入的 DLL 时,链接器会产生与普通 DLL 导入非常类似的数据,但操作系统会忽略这些数据。
DLL 优化
影响 DLL 性能的两个原因:
- 频繁的 Rebase 地址(基地址被占用)
- 大量的符号解析
优化方式:
- 装载时重定位(与 ELF 相比,空间换时间)
在 DLL 模块装载时,如果目标地址被占用,操作系统为它分配新的空间,DLL 所涉及到的绝对地址引用都进行重定位。(需要重定位的地址只要加上一个固定差值即可)
- 系统 DLL
Windows 系统在进程空间中专门划分一块区域,映射常用的系统 DLL,装载它们时不需要重定位了
- 序号
一个 DLL 中每个导出的函数都有一个相应的序号,序号标示被导出函数地址在 DLL 导出表中的位置。导入时可以使用函数名也可以使用函数序号。(不推荐使用函数序号)
- 导入函数绑定
大多数情况下, DLL 会以同样的顺序被装载到同样的内存地址,导出符号的地址不变,通过将导出函数的地址保存到模块的导入表中,可以省去每次启动时符号解析过程,被称为 DLL 绑定 (DLL Bingding)。
editbin 工具
对被绑定的程序的导入符号进行遍历查找,找到后把符号的运行时目标地址写入到被绑定程序的导入表内。
C++ 与动态链接
C++ 的动态链接库是一场噩梦,根源是 C++ 的标准值规定了语言层面的规则,对二进制级别没有任何规定
DLL HELL (DLL 噩梦)
由于 DLL 数量、版本众多,相互之间的调用极容易发生问题:
- 使用旧版的 DLL 替换原来一个新版的 DLL 引起的
- 新版 DLL 中的函数无意发生改变引起的,未能保证向下兼容
- 新版 DLL 的安装引入一个新的 BUG
解决方法
- 静态链接(逗逼)
- 防止 DLL 覆盖
- 避免 DLL 冲突
让每个应用程序拥有自己依赖的 DLL,把问题 DLL 的不同版本放到该应用程序的文件夹中,而不是系统文件夹中。装置 DLL 时,先从自己文件夹下寻找,再去系统文件中寻找
- .NET 下 DLL Hell 解决方案
使用 Manifest 清单文件,描述程序集的名子,版本号和各种资源,运行依赖的所有资源。