『operating system-2』dynamic link
装载与动态链接
一、可执行文件的装载与进程
进程虚拟地址空间:每个运行的程序拥有自己独立的虚拟地址空间
在32位平台下:
- Linux:高1GB分给操作系统,低3GB分给用户程序
- Windows:高2GB分给操作系统,低2GB分给用户程序
注意:硬件位数决定了寻址空间的上限:如32位的硬件平台决定了虚拟地址空间为 0 ~ \(2^{32} - 1\)
PAE(Physical Address Extension):扩充硬件地址线,以扩展32位平台的物理寻址能力
- 窗口映射:用某段虚拟空间作为窗口映射高于4GB的额外物理块
- Windows :AWE机制(Address Windowing Extensions)
- Linux :采用系统调用mmap( )
- 窗口映射:用某段虚拟空间作为窗口映射高于4GB的额外物理块
装载的方式:利用局部性原理,将程序最常用的部分驻留在物理内存中,其余的留在磁盘(动态装载)
覆盖装入:通过覆盖管理器将程序切分为若干模块,使新调入的模块覆盖已在内存的模块
覆盖规则:若模块A和模块B不会相互调用,两者就可能互相覆盖
调用树:程序员将各模块按调用关系组织成树结构:
当一个模块被调用时,务必保证从该模块到树根的所有模块都在内存中
禁止跨树调用,如模块C不可调用D、B、E、F
页映射:虚拟存储机制的一部分,通过物理页映射与页调换解决物理内存不足的问题
- 页面调换算法:如FIFO、LRU等
可执行文件的装载:
进程的建立:每个进程拥有独立的虚拟地址空间
创建独立的虚拟地址空间:建立页映射的数据结构(如页目录)
注意:此时暂时不填写页映射关系,可以等到之后缺页时再逐步设置
建立虚拟空间与可执行文件的映射关系:设置VMA,即进程地址空间中的段空间 VMA表示虚拟地址与ELF中指定段的位置间的关系(段表记录)
注意:可执行文件需要被映射到虚拟空间,所以又称作映像文件
CPU跳转到可执行文件的入口地址,启动运行
页错误:进程建立完毕后,执行任务时出现缺页异常,页错误处理程序如下:
- 查询虚拟空间与可执行文件之间的地址映射,找到空页面对应的VMA
- 将对应的ELF文件数据调入物理内存,并建立虚拟页与新物理页之间的映射关系
进程虚拟空间分布
ELF文件的链接与执行视图:
“链接”视图:将ELF文件划分为多个段(Section)
“执行”视图:将ELF文件划分为多个程序头(Segment),每个Segment被映射到同一个VMA
程序头表:可执行文件中保存装载信息,本质上是 Elf32_Phdr 结构体数组
程序头(Elf32_Phdr):将可执行文件中相同权限的Section合并至同一个Segment装载映射
- 段类型(p_type):共包含LOAD、DYNAMIC、INTERP等
- 段偏移(p_offset):Segment在可执行文件中的偏移
- 段虚拟地址(p_vaddr):Segment在进程虚拟地址空间中的始址
- 段物理地址(p_paddr):一般与p_vaddr相同
- 段文件长度(p_filesz):Segment在ELF文件中的长度(可能为0)
- 段长度(p_memse):Segment在进程虚拟地址空间中的长度(可能为0)
- 段权限(p_flags):Segment内共同的权限属性,如可读、可写、可执行
- 段对齐属性(p_align):Segment 对齐字节为 2 ^ p_align
注意:p_memse可能大于p_filesz,多余的部分留给被合并的.bss段,初始化为全0
堆与栈:堆一般是向上扩展的,栈是向下扩展的
进程地址空间中的各VMA区域:
- 代码 VMA:权限只读、可执行;含映像文件
- 数据 VMA:权限可读写、可执行;含映像文件
- 堆 VMA:权限可读写、可执行;无映像文件(匿名)
- 栈 VMA:权限可读写、不可执行;无映像文件(匿名)
段地址对齐(Segment):段长往往不是页大小的整数倍,段地址往往不是页对齐的
装载:通过虚拟内存页映射机制将可执行文件加载到进程地址空间
段合并:将ELF文件中各Segment间接壤的部分合并共享同一个物理页
装载时,将共享物理页分别映射到两个虚拟页,其余物理页正常映射
注意:段合并在逻辑上将可执行文件划分为以页为单位的若干个物理块,可以消去物理页碎片
一个物理页可能包含多个Segment的信息
进程栈的初始化
栈空间布局:由esp指针指向栈顶
其中环境变量指PATH或HOME等
进程启动后,堆栈中的信息会传递给 main( ) 函数(即argc和argv)
Linux内核装载ELF文件:
装载系统调用:int execve(const char* filename, char const argv[ ], char const envp[ ]);
装载流程:在进入execve( )系统调用后:
execve( )调用sys_execve( ),sys_execve( )接着调用do_execve( )
do_execve( )读取ELF文件的前128字节,通过ELF文件头的魔数判断文件类型
如ELF魔数为 0x7F、e、l、f;Java魔数为 c、a、f、e;shell脚本魔数为 #!
do_execve( )调用search_binary_handle( )匹配合适的装载处理函数
如ELF对应load_elf_binary( );脚本文件对应load_script( )
对于load_elf_binary( ),依次执行以下步骤:
检查ELF格式有效性(如魔数)
寻找 .interp 段,设置动态链接路径
根据ELF程序头表,对ELF的代码和数据进行映射
初始化ELF进程环境,将系统调用的返回地址设置为ELF的e_entry
系统调用返回至sys_execve( )后,直接跳转到e_entry
Windows装载PE文件
PE文件在内存中是页对齐的,各段的长度都是页大小的整数倍
目标地址(target address)与相对虚拟地址(RVA):分别指文件在虚存中的基地址,以及文件内偏移量
注意:由于PE文件装载的基地址可能发生变化,所以引入了RVA
装载流程:
- 读取文件的第一页,即DOS头、PE头与段表
- 若进程地址空间中目标地址被占用,则要另选一个装载地址
- 根据段表信息,将PE文件中的各段映射到虚存地址空间中
- 若装载地址不是目标地址,则进行Rebasing
- 装载所有PE文件所需的DLL文件
- 对PE文件中所有导入符号进行解析
- 根据PE头中指定的参数,初始化栈和堆,最后启动进程
PE扩展头(PE Optional Header):含有与装载相关的信息
- Image Base :若该地址未被占用,则优先尝试将PE装载到该处
- AddressOfEntryPoint :PE文件第一条运行指令的RVA
- SectionAlignment :段对齐粒度,一般即系统页大小
- SizeOfImage :内存中经过节对齐的映像体大小
- SizeOfCode与SizeOfInitializedData:代码段长度与初始化的数据段长度
- BaseOfCode :代码段始址的RVA
- BaseOfData :数据段始址的RVA
二、动态链接
动态链接的概念:与静态链接不同,动态链接等到程序开始运行时才将各目标文件进行链接
动态链接的应用:
- 节省磁盘与内存空间:被多个程序共享的目标文件仅保留一个副本,通过复用共享节省空间
- 程序开发与发布:程序更新不必发布完整的文件,只需要发布已更新的目标文件即可
- 程序可扩展性和兼容性:
- 扩展性:将程序模块做成插件,动态载入程序
- 兼容性:不同的平台提供相同的动态链接库,使程序可以运行在不同的平台上
动态链接文件:
- Linux :动态共享对象(DSO),以“.so”为扩展名
- Windows :动态链接库(DLL),以“.dll”为扩展名
注意:在使用动态链接库时,程序被分成了可执行模块和共享对象两类模块;静态链接下可执行文件是一个整体
与可执行文件不同,共享对象的最终装载地址在编译阶段是不确定的
C语言库的运行库glibc的动态链接形式为libc.so(整个系统唯一的副本)
动态链接器:
链接规则:若引用符号来自静态目标文件,链接器会在链接时重定位
若引用符号来自动态共享对象,链接器会在装载时重定位
注意:动态链接器也会被映射入进程地址空间,在程序运行前完成动态链接工作
装载时重定位:链接时将所有对绝对地址的重定位延后到装载时完成
待装载的目标地址确定后,再根据文件中各符号的相对偏移量进行重定位
注意:装载时重定位比地址无关速度更快(省去了计算间接跳转),但无法实现代码共享
地址无关代码(PIC):共享对象中,将指令中需要修改的部分分离出来,与数据放在一起
模块内部的函数调用:调用者与被调用者相对偏移保持不变,可以直接相对寻址,无需重定位
注意:其它模块可能覆盖本模块的函数,故实际上只能把同模块符号当作外部符号处理
可以考虑将同模块下的被调用函数设置为static的,保证其不被覆盖,无需重定位,可提高效率
模块内部的数据访问:访问指令(.text)和被访问数据(.data)的相对偏移保持不变:
- 调用 __i686.get_pc_thunk.cx 函数,获取当前访问指令的PC于 %ecx 寄存器
- 当前访问指令的地址 + 相对偏移 = 被访问数据的地址
注意:由于共享目标的装载地址不确定,所以不能直接使用绝对地址访问时据
模块间的数据访问:将地址相关的部分放在数据段中的全局偏移表,实现代码地址无关
- 全局偏移表(GOT):指向外部变量的指针数组,装载时填写数组各表项
- 访问数据流程:GOT与访问代码的相对偏移保持不变
- 根据当前访问指令PC和相对偏移求出变量地址在GOT中的位置
- 根据对应表项中存放的变量地址访问数据(间接访问)
模块间的函数调用:与跨模块访问数据类似,同样使用GOT间接查找地址:
- 根据当前访问指令PC和相对偏移求出函数地址在GOT中的位置
- 根据对应表项中存放的函数地址跳转访问(间接跳转)
注意:共享对象中地址无关的代码段可由多个进程共享;共享对象中的数据段在多个进程中有独立副本
-shared和-fPIC :gcc的两个与动态链接有关的参数
- -shared:产生共享对象(如 .so)
- -fPIC:指示GCC产生地址无关代码
数据段的地址无关性
- 方案:装载时重定位,即在共享对象装载时填补重定位入口
- 重定位表:记录共享对象的重定位入口信息
三、延迟绑定
延迟绑定的实现:将函数引用的链接延后,直到函数第一次被调用时才进行绑定(重定位)
注意:延迟绑定可以防止因函数引用过多导致程序运行前链接开销过大,减缓性能
PLT (Procedure Linkage Table):ELF中的GOT被拆分成两个独立的段,分别为.got和.got.plt
.got保存外部变量引用的地址;.got.plt保存外部函数引用的地址
.got.plt的结构:
Address of .dynamic:存放.dynamic段的地址
Module ID:本模块的ID,其地址位于 (GOT + 4)
_dl_runtime_resolve( )的地址:其地址位于 (GOT + 8)
_dl_runtime_resolve( )负责将函数地址填入.got.plt中的表项
其它外部函数引用的地址
实现延迟绑定的流程,通过地址 bar@plt 实现间接跳转
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15PTL0:
/* 将本模块的ID压入 _dl_runtime_resolve() 的栈帧 */
push *(GOT + 4)
/* 跳转至 _dl_runtime_resolve() */
jmp *(GOT + 8)
...
bar@plt:
/* 目标模块第二次调用 bar@plt,会直接跳转到.got.plt中的正确地址 */
jmp *(bar@GOT)
/* 将外部函数引用在重定位表.rel.plt中的下标 n 压入 _dl_runtime_resolve() 的栈帧 */
push n
/* 目标模块第一次调用 bar@plt,会先跳转到 PLT0 进行延迟绑定 */
jmp PLT0
...
四、动态链接的相关结构
“.interp”段:即解释器(interpreter),保存可执行文件的动态链接器的路径
“.dynamic”段:保存动态链接器所需的信息,本质上是Elf32_Dyn结构的数组
Elf32_Dyn:属性值(d_tag) + 数值(d_val)或指针变量(d_ptr)
- 属性值(d_tag):
- DT_REL:动态链接重定位表的地址
- DT_SYMTAB:动态链接符号表的地址,对应的d_ptr表示.dynsym的地址
- DT_STRTAB:动态链接字符串表的地址,对应的d_ptr表示.dynstr的地址
- DT_STRSZ:动态链接字符串表的大小,对应的d_val表示表大小
- DT_INIT:初始化代码地址
- DT_NEED:依赖的共享对象文件,对应的d_ptr表示依赖文件名
- 属性值(d_tag):
动态符号表(.dynsym):仅保存与动态链接相关的符号,不包含内部私有符号
动态符号字符串表(.dynstr):用于查找动态链接符号
注意:静态链接下的符号表(.symtab)与字符串表(.strtab)包含了所有的符号信息(包括动态链接符号)
动态链接重定位表:确定动态导入符号的运行地址,包括符号名、重定位地址修正方式等信息
注意:对于使用地址无关技术的ELF,由于数据段(含绝对地址)的存在,仍需要装载时重定位
.rel.dyn:对外部数据引用修正的重定位表,修正的位置位于.got及数据段
.rel.plt:对外部函数引用修正的重定位表,修正的位置位于.got.plt
重定位地址修正方式:新增R_386_RELATIVE、R_386_GLOB_DAT、R_386_JUMP_SLOT重定位入口类型
- R_386_RELATIVE:装载目标基地址 + 内部变量的偏移量,即基址重置(Rebasing)
- R_386_GLOB_DAT:将变量绝对地址填入.got中的对应表项
- R_386_JUMP_SLOT:将函数绝对地址填入.got.plt中的对应表项
动态链接时进程堆栈初始化:保存动态链接器所需的辅助信息
辅助信息数组(Auxiliary Vector):存于栈中,本质上是一个Elf32_auxv_t结构体数组
Elf32_auxv_t:类型值(a_type)+ 属性值(a_val)
- 类型值(a_type):
- AT_NULL(0):对应属性值为辅助信息数组的结束
- AT_EXEFD(2):可执行文件的文件句柄;动态链接器使用文件句柄访问文件
- AT_PHDR(3):可执行文件的程序头表;动态链接器也可通过内存映像访问文件
- AT_PHENT(4):对应属性值为程序头表中每个表项的大小
- AT_PHNUM(5):对应属性值为程序头表中表项的数量
- AT_BASE(7):对应属性值为动态链接器的装载地址
- AT_ENTRY(9):对应属性值为可执行文件的启动入口地址
注意:辅助信息存储在栈中环境变量指针的上方,命令行信息的下方
- 类型值(a_type):
五、动态链接的步骤与实现:“三步走”
动态链接器的自举(bootstrap):
- 自举:动态链接器的装载重定位工作不依赖于其它任何共享对象
- 自举流程:动态链接器独立完成其自身全局变量与静态变量的重定位
- 自举代码找到自己的GOT,再通过.got.plt的第一个表项找到.dynamic段的地址
- 通过.dynamic段可以找到动态链接器自身的重定位表和符号表,并对自身执行重定位
装载共享对象:将共享对象的代码与数据映射到进程地址空间中
符号的装填:将所有模块以及动态链接器的符号合并入全局符号表
- 动态链接器将可执行文件和自身的符号表合并入全局符号表
- 动态链接器根据.dynamic段中的DT_NEED项获得所有需要的共享对象
- 每装载一个新的共享对象,就会将其符号表合并入全局符号表
注意:由于共享对象也可能依赖于共享对象,所以装载的过程实际可看作遍历图的过程
全局符号介入:对于多模块定义的同名符号
若同名符号在先前装载中已经加入全局符号表,则后加入的符号被覆盖忽略
注意:“覆盖优先级”问题会引发全局的同名函数因被覆盖而失效
重定位与初始化:自举、装载完毕后,动态链接器开始执行重定位操作
- 重定位:动态链接器遍历所有可执行文件和共享对象的重定位表,修正它们的GOT表项
- 初始化:重定位完成后,执行每个共享对象的 .init 段代码,进行初始化操作
Linux 动态链接器
软链接:Linux 动态链接器的路径/lib/ld-linux.so.2是软链接,指向了真正的链接/lib/ld-x.y.z.so
ld.so的运作流程:动态链接器本身是一种特殊的可执行文件,内核会为其安排一个合适的装载地址
- 入口 _start 调用 _do_start( ) 函数独立进行重定位(即自举)
- 进入 _dl_main( ),进行共享对象的装载、符号解析与重定位等多个任务
注意:动态链接器本身必须是静态链接(statically linked)的,不依赖于其它共享对象
六、显式运行时链接
动态链接库:在程序运行时可以控制加载(或卸载)的共享对象
- 动态库的装载:由动态链接器提供的API实现(以下四种)
dlopen( ):打开并加载一个动态库到进程地址空间
C函数原型:void* dlopen(const char* filename, int flag);
filename:动态库的路径(绝对路径 or 相对路径)
注意:若该参数传入NULL,则会返回全局符号表的句柄
函数符号解析方式(flag):延迟绑定(第一次调用才绑定)or 加载时立即绑定
返回值:被加载模块的句柄,如加载失败则返回NULL
dlsym( ):寻找所需要的符号
- C函数原型:void* dlsym(void* handle, char* symbol);
- handle:由dlopen( )打开的文件的句柄,若为NULL则在全局符号表中查找
- symbol:被查找的字符串
- 返回值:若查找函数符号。则返回函数地址;若查找变量符号,则返回变量地址
dlclose( ):将已加载的模块卸载,取消进程地址空间于该模块间的映射关系
加载引用计数器:调用dlopen( )加载某模块时,计数器自增;调用dlclose( )卸载某模块时,计数器自减
计数器减至0时,对应模块被真正卸载C函数原型:int dlclose(void* handle);
handle:已打开模块的句柄
返回值:返回 0 表示成功关闭;返回非 0 表示关闭出现错误,可通过dlerror( )捕获错误信息
dlerror( ):在调用了dlopen( ),dlsym( )或dlclose( )后,可通过调用dlerror( )判断是否调用成功
调用成功返回NULL,调用错误则返回对应的报错信息C函数原型:char* dlerror(void);
七、Linux共享库
共享库的版本兼容性:
- 共享库的版本更新:兼容更新(增量开发)& 不兼容更新(改变原版本)
- 改变C语言共享库ABI的行为
- 导出函数的行为发生改变,其功能与旧版本的函数功能不一致
- 导出函数被删除
- 导出数据的结构发生变化,如结构成员删除
- 导出函数的接口发生变化,如函数参数或函数返回值发生变化
共享库版本命名:libname.so.x.y.z
- 库名称:以“lib”为前缀开头,中间是库的名字name和后缀名.so
- 主版本号(x):表示库的重大升级,不同主版本号间的共享库不兼容
- 次版本号(y):表示库的增量升级,即在保持原有库不变的基础上增加新接口
- 发布版本号(z):表示库的错误修正或性能改进,即不增不改
注意:依赖于旧的次版本号的共享库可以兼容新的次版本号的共享库,不同发布版本号的共享库之间都可以互相兼容
SO-NAME:把完整版本名的次版本号和发布版本号去掉,作为指向最新版本库的软连接
ldconfig:当Linux系统安装或更新一个共享库时,就会使所有软连接指向最新版的共享库
次版本号交会问题:程序使用了高次版本号中的接口,而系统中只有低次版本号的共享库
基于符号的版本机制:解决次版本号交会问题
Solaris中的符号版本机制
- 符号版本脚本:实现了版本内部符号的定义与版本间符号的继承
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18/* 定义符号SUNW_1.1 */
SUMW_1.1 {
global:
global_symbol1;
...
local:
local_symbol1;
...
}
/* SUNW_1.2 继承了 SUNW_1.1的所有符号 */
SUNW_1.2 {
global:
global_symbol2;
...
local:
local_symbol2:
...
} SUNW_1.1;- 范围机制:将共享库中的部分符号设置为local的,防止共享库的调用者使用这些局部函数
- 构建时,链接器可以记录最终输出文件中依赖的最低版本号对应的共享库
运行时,链接器会根据记录的最低版本号,检查当前共享库中的符号集合是否包含所需的版本
Linux中的符号版本机制
GCC对Solaris符号版本机制的两个扩展
.symver汇编宏:对符号sym设置符号标签,如
.symver sym, sym@VERS_1.1
允许多个版本的同一符号处于同一个共享库中,如:
1
2
3
4
5
6
7/* 将sym的1.1版本导出为old_sym */
asm(".symver old_sym, sym@VERS_1.1");
/* 将sym的1.2版本导出为new_sym */
asm(".symver new_sym, sym@VERS_1.2");
...
void old_sym();
void new_sym();- 符号重载:同一个符号可拥有不同的版本号,将共享库的更新粒度缩减为接口层面
如使用1.1版本的sym就会链接old_sym( ),使用1.2版本的sym就会链接new_sym( )
共享库系统路径
File Hierarchy Standard:规定了系统中系统文件应如何存放,促进了各开源系统间的兼容性 FHS做出以下规定:
- /lib:存放最关键的基础共享库,如动态链接器、C语言运行库等
- /usr/lib:存放非系统运行所需的关键性共享库
- /usr/local/lib:存放第三方应用程序的库,如python解释器
注意:共享库对应的可执行文件一般放在lib对应的bin目录下
共享库的查找
查找方式:根据.dynamic段中的DT_NEED项保存的路径搜索对应的共享库
- 绝对路径:动态链接器直接按照该绝对路径查找
- 相对路径:动态链接器在/lib、/usr/lib、以及/ld.so.conf 配置文件指定的路径中查找
ldconfig :该程序主要有两个用途
- 调整SO_NAME:负责各共享库 SO_NAME 的创建、删除或更新
- 调整/etc/ld.so.cache:将各共享库的 SO_NAME 收集起来,存放在缓存目录 /ld.so.cache 中
注意:动态链接器的查找顺序为 /etc/ld.so.cache \(\rightarrow\) /usr/lib or /lib
/etc/ld.so.cache 缓存加快了动态链接器搜索共享库的速度
环境变量:主要有以下三种
LD_LIBRARY_PATH:存放若干个路径构成的环境变量,每个路径之间由冒号分割 动态链接器会优先在该环境变量保存到路径中查找共享库(比/etc/ld.so.cache优先级更高)
LD_PRELOAD:在动态链接器开始装载共享库之前,会提前预装该环境变量中保存的共享库 根据全局符号介入机制,预装共享库包含的符号可以覆盖所有后装载的符号
注意:正常情况下不应使用LD_LIBRARY_PATH和LD_PRELOAD
LD_DEBUG:打开动态链接器的调试功能,根据不同参数打印链接信息:
bindings:显示动态链接器的符号绑定过程
libs:显示共享库的查找过程
versions:显示各符号的版本依赖关系
reloc:显示重定位过程
symbols:显示符号表查找过程
...
八、Windows下的动态链接
基地址与RVA:对于可执行exe文件,ImageBase = 0x400000;对于共享库dll文件,ImageBase = 0x10000000
RVA:相对基地址的偏移量
注意:对于dll文件,如果其优先基地址已被占据,则PE装载器会选用其它空闲地址
导入与导出:DLL中需要显示地告诉编译器导出哪些符号;ELF中默认导出所有全局符号
- __declspec:用于指定变量或函数符号的导入或导出
- __declspec(dllexport):表示从本DLL导出的符号
- __declspec(dllimport):表示从其它DLL导入的符号
- __declspec:用于指定变量或函数符号的导入或导出
DLL显示运行时链接:Windows提供了3个支持运行时链接的API:
- LoadLibrary( ):装载一个DLL到进程的地址空间,与dlopen( )类似
- GetProcAddress( ):在DLL中查找一个符号,与dlsym( )类似
- FreeLibrary( ):卸载某个已经加载的模块,与dlclose( )类似
导出表(IMAGE_EXPORT_DIRECTORY):PE文件头中DataDirectory数组的第一项
- 导出地址表(EAT):存放各导出函数的RVA
- 函数名表(Name Table):存放各导出函数的名称
- 序号表(Name-Ordered Table):存放各导出函数名称对应的序号
序号:导出函数的序号 = 函数在EAT中的下标 + 1
导入符号流程:动态链接器需要查找导入符号的RVA
- 模块A导入b.dll中的foo函数,并在A的导入表中记录函数名“foo”
- 动态链接时,动态链接器在b.dll的函数名表中查找“foo”,再根据序号表找到对应的序号
- 根据序号算出函数在EAT中的下标,从而获得其RVA
EXP文件:链接器创建DLL的同时得到的临时文件,其中经过两遍扫描:
- 第一遍:扫描并收集所有目标文件的导出符号信息,创建DLL导出表,并将导出表放入EXP文件
- 第二遍:将EXP文件与其它目标文件链接在一起,输出为DLL文件的导出表
导出重定向:EAT中存的并不是函数的RVA,而是指向导出表中的某个字符串
该字符串是符号重定向后指向的“模块名 + 函数名”
导入表:保存了模块使用的来自DLL的变量或函数信息,本质上是IMAGE_IMPORT_DESCRIPTOR结构数组
数组元素结构如下: - FirstThunk:指向导入地址数组(IAT)
- OriginalFirstThunk:指向导入名称表(INT);INT是与IAT完全相同的副本
导入地址数组(IAT):每个表项对应一个被导入的符号
- 装载但未重定位时:表项元素值为对应导入符号的序号或符号名
- 链接完成后:表项元素值改写为对应导入符号的真正地址
重定基地址(Rebasing):
- PE中的DLL代码段不是地址无关的,拥有固定的装载基地址0x10000000
- 装载时重定位:若DLL模块的目标地址被占用,系统会为其选择新的装载地址,对所有绝对地址重定位
- 改变默认基址:在装载前直接改变DLL的装载基地址
- 系统DLL:系统在进程中划出 0x70000000 ~ 0x80000000 的区域映射常用的系统DLL,如kernel32.dll
导入函数绑定:在符号解析与重定位之前直接将外部函数的地址存入导入表中
- DLL绑定:遍历被绑定程序的导入表,将符号的目标地址写入被绑定程序的导入表(INT)内
- 绑定失效:符号绑定存在以下两种失效的可能:
- DLL更新:被依赖的DLL更新导致DLL的导出函数地址改变
- DLL装载时重定位:被依赖的DLL的装载地址与被绑定时不一致
- 绑定失效的解决方案:
- 绑定时:将导入DLL的时间戳(Timestamp)与校验和(Checksum)存入被绑定文件的导入表中
- 装载时:确认被装载的DLL未被更新,且未发生装载时重定位
DLL HELL :DLL版本不兼容问题
DLL HELL发生的主要原因:
- 应用程序安装时新版本的DLL覆盖了旧版本的DLL,导致只能依赖旧版本DLL的程序崩溃
- 新版本的DLL的安装引入了一个新的BUG
- DLL缺失或误删、操作系统更新...
DLL HELL解决方法:
- 静态链接:通过静态链接的方法链接其所需要的所有库,避免使用DLL
- 防止DLL覆盖:Windows文件保护技术(WFP)可以阻止未授权的第三方应用覆盖系统DLL
- 解决DLL冲突:让每个程序拥有自己专门依赖的DLL,隔离不同版本的DLL
.NET框架下DLL HELL解决方案:
程序集:可执行文件集(.exe) or 库程序集(DLL 动态链接库)
Manifest文件:描述程序集的清单文件
文件内容:程序集名称、版本号、成员列表、依赖资源等;<XML>格式
强文件名:系统类型 + 文件名 + 版本号 + 平台环境 + 公钥
注意:强文件名允许不同版本的相同库共存而不发生冲突 如 目录下每个DLL都拥有以其强文件名命名的独立目录
SxS Manager:并行(Side-By-Side)管理器,根据应用程序的manifest加载版本正确的DLL
注意:Manifest机制要求应用程序在系统中必须拥有与manifest中指定的完全相同的DLL包括版本一致、编译平台一致等