9.1 DLL 简介
- dll 文件的扩展名不一定是 dll, 也可以是 ocx
- dll 文件相比于linux 的共享对象, 更加强调模块化
9.1.1 进程地址空间和内存管理
- 一个dll 在不同的进程中拥有不同的私有数据的副本, 这个类似 ELF 共享对象, 不过由于ELF 中代码段是地址无关的, 可以实现多个进程共享一份代码, 但是dll 代码却并不是地址无关的, 因而只能在某些情况下被多个进程间共享
9.1.2 基地址和RVA(相对地址)
- 对一个可执行exe 文件, Image base 一般是 0x400000, 而对于dll 文件, 这个值一般是 0x10000000
9.1.3 dll 共享数据段
- windows 允许将dll 的数据段设置成共享的, ie, 在任何进程中都可以共享这个dll 的同一份数据段
- 实际上, dll 有两个数据段, 一个是进程间共享, 一个是私有
9.1.4 dll 的简单例子
- windows 平台 C++ 支持导出符号, ie, 使用 __declspec 属性关键字修饰某个函数或者变量
- __declspec(dllexport) 表示从本dll导出符号
- __declspec(dllimport) 表示从别的dll 中导入符号
- 当然, 有一种更为通用的声明导入导出符号的方法, ie, 定义 def 文件, (这种方法对其他语言同样有效)
9.1.5 创建dll
- 使用MSVC 编译器 cl 编译时候
cl /LDd Math.c
- LDd 表示生成 Debug 版本的DLL, LD 表示生成 Release 版本的DLL
- 另外, 我们可以通过dumpbin 工具看到DLL 的导出符号
9.1.6 使用DLL
- 编译命令行:
cl /c TestMath.c
link TestMath.obj Math.lib
- 首先使用编译器将TestMath.c 编译成TestMath.obj, 然后使用连接器将 TestMath.obj 和 Math.lib 链接在一起产生一个可执行文件 TestMath.exe
- lib 文件, 实际上是对 dll 中导出的函数做了一定的描述
9.1.7 使用模块定义文件
- 通过def 文件可以描述DLL 导出属性, 可以避免MSVC 由于__cdecl, __stdcall, __fastcall 调用而导致的符号修饰
- windows 平台一般通用的是采用 __stdcall 方式
- 另外通过 def 文件控制, 可以类似 ld 的链接控制脚本, 可以控制一些链接的过程
9.1.8 dll 显式运行时链接
- LoadLibrary 类似 dlopen
- GetProcAddress 类似 dlsym 查找某个符号的地址
- FreeLibrary 类似 dlclose 卸载某个已经加载了的模块
9.2 符号到处导入表
9.2.1 导出表
- 导出表
在导出结构中, 最后三个成员指向的是3个数组, 这三个数组是导出表中最重要的结构, 他们是 导出地址表 EAT, 符号名表 , 和 名字序号对应表
- 序号
- 原先为了节省内存空间, 采用序号标识, 进行对函数的导出工作
- 并且, 使用序号进行导入导出, 可以节省函数名查找的时间
- 但是使用序号导入导出, 维护比较费劲
- 现在dll 导出方式基本都是使用 符号表, 但是仍然兼容原先的序号方式
9.2.2 EXP 文件
- 这实际上是一个临时文件, 用来存放创建dll 时候的导出表的信息
9.2.3 导出重定向
- 可以讲一个导出符号重定向到另一个dll中
9.2.4 导入表 IAT
- ELF 中 .rel.dyn 和 rel.plt 两个段中分别保存了该模块所需要导入的变量和函数的符号以及所在模块等信息, 而 .got 和 .got.plt 则保留了这些变量和函数的真正地址
- 可以使用 dumpbin 查看一个模块依赖于哪些dll信息
- windows 的动态连接器在装载一个模块的时候, 需要改写导入表的IAT, 这点很像ELF 中的 .got
- 但是, PE 的导入表一般是只读的
- 对 windows 而言, 动态连接器实际上是windows 内核的一个部分, 因而可以修改 PE 装载之后的任意一个部分的内容, 包括内容和页面属性。 但是运行程序不可以。
- 另外, dll 支持延迟载入
9.2.5 导入函数的调用
- IAT 相当于 ELF 中的GOT 的作用
9.3 DLL 优化
- 由于 DLL 的代码段和数据段本身并不是地址无关的, 如果装载时候的目标地址被占用, 那么就需要进行rebase (重定位), 同时, 由于导入符号在运行的时候需要被逐个解析, 因而非常的耗时。
9.3.1 重定位地址
- windows PE 采用装载时重定位的方法, 对每个绝对地址的引用都进行重定位
- exe 文件由于是第一个装入虚拟空间, 因而基本不存在重定位的问题, 但是dll 文件会经常遇到这么一个问题
- 由于每个进程都需要有一份单独的dll 代码段副本, 所以相比于 ELF 的代码段地址无关方案而言, 更加消耗内存, 但是他的运行速度更快, 因为不需要每次计算GOT 的位置(空间换时间)
- 对于系统 DLL, 他们由于经常被用到, 所以系统在进程空间中专门划分一块 0x70000000~0x80000000区域, 用来映射这些常用的系统dll
9.3.2 序号
- 一个导出函数可以没有函数名, 但必须有一个唯一的序号
- 对于windows api 而言, 他们的函数名在多个版本中保持不变, 但是序号是不停的变化的
9.3.3 导入函数绑定
- 可以避免每次程序运行都要重新执行符号的查找, 解析和重定位操作
- 使用editbin 遍历导入符号, 找到之后就将符号运行时候的目标地址写入到绑定程序的导入表中, INT 就是用来保存绑定符号的地址的
9.4 C++ 与 动态链接
- 尽量不要使用 C++ 编写dll
9.5 DLL HELL
- 解决方法:
- 使用静态链接
- 放置dll 覆盖
- 避免dll 冲突
- .net 下, 可以通过manifest 清单文件 指定需要的dll 版本
- WinSxS 目录下有独立的目录, 记录机器类型, 名字, 公钥, 版本号
- WinSxS 目录下有独立的目录, 记录机器类型, 名字, 公钥, 版本号