『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( )

  • 装载的方式:利用局部性原理,将程序最常用的部分驻留在物理内存中,其余的留在磁盘(动态装载)

    • 覆盖装入:通过覆盖管理器将程序切分为若干模块,使新调入的模块覆盖已在内存的模块

      • 覆盖规则:若模块A和模块B不会相互调用,两者就可能互相覆盖

      • 调用树:程序员将各模块按调用关系组织成树结构

        当一个模块被调用时,务必保证从该模块到树根的所有模块都在内存中

      禁止跨树调用,如模块C不可调用D、B、E、F


    • 页映射:虚拟存储机制的一部分,通过物理页映射与页调换解决物理内存不足的问题

      • 页面调换算法:如FIFO、LRU等

  • 可执行文件的装载:

    • 进程的建立:每个进程拥有独立的虚拟地址空间

      1. 创建独立的虚拟地址空间:建立页映射的数据结构(如页目录)

        注意:此时暂时不填写页映射关系,可以等到之后缺页时再逐步设置

      2. 建立虚拟空间可执行文件的映射关系:设置VMA,即进程地址空间中的段空间 VMA表示虚拟地址ELF中指定段的位置间的关系(段表记录)

        注意:可执行文件需要被映射到虚拟空间,所以又称作映像文件

      3. CPU跳转到可执行文件的入口地址,启动运行

    • 页错误:进程建立完毕后,执行任务时出现缺页异常,页错误处理程序如下:

      1. 查询虚拟空间可执行文件之间的地址映射,找到空页面对应的VMA
      2. 将对应的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区域:

        1. 代码 VMA:权限只读、可执行;含映像文件
        2. 数据 VMA:权限可读写、可执行;含映像文件
        3. 堆 VMA:权限可读写、可执行;无映像文件(匿名)
        4. 栈 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( )系统调用后:

        1. execve( )调用sys_execve( ),sys_execve( )接着调用do_execve( )

        2. do_execve( )读取ELF文件的前128字节,通过ELF文件头的魔数判断文件类型

          如ELF魔数为 0x7F、e、l、f;Java魔数为 c、a、f、e;shell脚本魔数为 #!

        3. do_execve( )调用search_binary_handle( )匹配合适的装载处理函数

          如ELF对应load_elf_binary( );脚本文件对应load_script( )

        4. 对于load_elf_binary( ),依次执行以下步骤:

        5. 检查ELF格式有效性(如魔数)

        6. 寻找 .interp 段,设置动态链接路径

        7. 根据ELF程序头表,对ELF的代码和数据进行映射

        8. 初始化ELF进程环境,将系统调用的返回地址设置为ELF的e_entry

        9. 系统调用返回至sys_execve( )后,直接跳转到e_entry


    • Windows装载PE文件

      • PE文件在内存中是页对齐的,各段的长度都是页大小的整数倍

      • 目标地址(target address)与相对虚拟地址(RVA):分别指文件在虚存中的基地址,以及文件内偏移量

        注意:由于PE文件装载的基地址可能发生变化,所以引入了RVA

      • 装载流程:

        1. 读取文件的第一页,即DOS头、PE头与段表
        2. 若进程地址空间中目标地址被占用,则要另选一个装载地址
        3. 根据段表信息,将PE文件中的各段映射到虚存地址空间中
        4. 若装载地址不是目标地址,则进行Rebasing
        5. 装载所有PE文件所需的DLL文件
        6. 对PE文件中所有导入符号进行解析
        7. 根据PE头中指定的参数,初始化栈和堆,最后启动进程
      • PE扩展头(PE Optional Header):含有与装载相关的信息

        • Image Base :若该地址未被占用,则优先尝试将PE装载到该处
        • AddressOfEntryPoint :PE文件第一条运行指令的RVA
        • SectionAlignment :段对齐粒度,一般即系统页大小
        • SizeOfImage :内存中经过节对齐的映像体大小
        • SizeOfCode与SizeOfInitializedData:代码段长度初始化的数据段长度
        • BaseOfCode :代码段始址的RVA
        • BaseOfData :数据段始址的RVA

二、动态链接

  • 动态链接的概念:与静态链接不同,动态链接等到程序开始运行时才将各目标文件进行链接

  • 动态链接的应用:

    • 节省磁盘与内存空间:被多个程序共享的目标文件仅保留一个副本,通过复用共享节省空间
    • 程序开发与发布:程序更新不必发布完整的文件,只需要发布已更新的目标文件即可
    • 程序可扩展性和兼容性:
      1. 扩展性:将程序模块做成插件,动态载入程序
      2. 兼容性:不同的平台提供相同的动态链接库,使程序可以运行在不同的平台上
  • 动态链接文件:

    • Linux :动态共享对象(DSO),以“.so”为扩展名
    • Windows :动态链接库(DLL),以“.dll”为扩展名

    注意:在使用动态链接库时,程序被分成了可执行模块共享对象两类模块;静态链接下可执行文件是一个整体

    与可执行文件不同,共享对象的最终装载地址在编译阶段是不确定

    C语言库的运行库glibc的动态链接形式为libc.so(整个系统唯一的副本)

  • 动态链接器:

    • 链接规则:若引用符号来自静态目标文件,链接器会在链接时重定位

      若引用符号来自动态共享对象,链接器会在装载时重定位

    注意:动态链接器也会被映射入进程地址空间,在程序运行前完成动态链接工作


  • 装载时重定位:链接时将所有对绝对地址的重定位延后到装载时完成

    待装载的目标地址确定后,再根据文件中各符号的相对偏移量进行重定位

    注意装载时重定位比地址无关速度更快(省去了计算间接跳转),但无法实现代码共享

  • 地址无关代码(PIC):共享对象中,将指令中需要修改的部分分离出来,与数据放在一起

    • 模块内部的函数调用:调用者与被调用者相对偏移保持不变,可以直接相对寻址无需重定位

      注意:其它模块可能覆盖本模块的函数,故实际上只能把同模块符号当作外部符号处理

      可以考虑将同模块下的被调用函数设置为static的,保证其不被覆盖,无需重定位,可提高效率

    • 模块内部的数据访问:访问指令(.text)和被访问数据(.data)的相对偏移保持不变:

      1. 调用 __i686.get_pc_thunk.cx 函数,获取当前访问指令的PC于 %ecx 寄存器
      2. 当前访问指令的地址 + 相对偏移 = 被访问数据的地址

      注意:由于共享目标的装载地址不确定,所以不能直接使用绝对地址访问时据

    • 模块间的数据访问:将地址相关的部分放在数据段中的全局偏移表,实现代码地址无关

      • 全局偏移表(GOT):指向外部变量的指针数组,装载时填写数组各表项
      • 访问数据流程:GOT与访问代码的相对偏移保持不变
        1. 根据当前访问指令PC和相对偏移求出变量地址在GOT中的位置
        2. 根据对应表项中存放的变量地址访问数据(间接访问)
    • 模块间的函数调用:与跨模块访问数据类似,同样使用GOT间接查找地址:

      1. 根据当前访问指令PC和相对偏移求出函数地址在GOT中的位置
      2. 根据对应表项中存放的函数地址跳转访问(间接跳转)

      注意:共享对象中地址无关的代码段可由多个进程共享;共享对象中的数据段在多个进程中有独立副本


    • -shared和-fPIC :gcc的两个与动态链接有关的参数

      • -shared:产生共享对象(如 .so)
      • -fPIC:指示GCC产生地址无关代码

  • 数据段的地址无关性

    • 方案:装载时重定位,即在共享对象装载时填补重定位入口
    • 重定位表:记录共享对象的重定位入口信息

三、延迟绑定

  • 延迟绑定的实现:将函数引用的链接延后,直到函数第一次被调用时才进行绑定(重定位)

    注意:延迟绑定可以防止因函数引用过多导致程序运行前链接开销过大,减缓性能

  • PLT (Procedure Linkage Table):ELF中的GOT被拆分成两个独立的段,分别为.got和.got.plt

    • .got保存外部变量引用的地址;.got.plt保存外部函数引用的地址

    • .got.plt的结构:

      1. Address of .dynamic:存放.dynamic段的地址

      2. Module ID:本模块的ID,其地址位于 (GOT + 4)

      3. _dl_runtime_resolve( )的地址:其地址位于 (GOT + 8)

        _dl_runtime_resolve( )负责将函数地址填入.got.plt中的表项

      4. 其它外部函数引用的地址

    • 实现延迟绑定的流程,通过地址 bar@plt 实现间接跳转

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    PTL0:
    /* 将本模块的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表示依赖文件名
  • 动态符号表(.dynsym):仅保存与动态链接相关的符号,不包含内部私有符号

  • 动态符号字符串表(.dynstr):用于查找动态链接符号

    注意静态链接下的符号表(.symtab)与字符串表(.strtab)包含了所有的符号信息(包括动态链接符号)

  • 动态链接重定位表:确定动态导入符号的运行地址,包括符号名、重定位地址修正方式等信息

    注意:对于使用地址无关技术的ELF,由于数据段(含绝对地址)的存在,仍需要装载时重定位

    • .rel.dyn:对外部数据引用修正的重定位表,修正的位置位于.got及数据段

    • .rel.plt:对外部函数引用修正的重定位表,修正的位置位于.got.plt


    • 重定位地址修正方式:新增R_386_RELATIVE、R_386_GLOB_DAT、R_386_JUMP_SLOT重定位入口类型

      1. R_386_RELATIVE:装载目标基地址 + 内部变量的偏移量,即基址重置(Rebasing)
      2. R_386_GLOB_DAT:将变量绝对地址填入.got中的对应表项
      3. 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):对应属性值为可执行文件的启动入口地址

      注意:辅助信息存储在栈中环境变量指针的上方,命令行信息的下方


五、动态链接的步骤与实现:“三步走”

  • 动态链接器的自举(bootstrap):

    • 自举:动态链接器的装载重定位工作不依赖于其它任何共享对象
    • 自举流程:动态链接器独立完成其自身全局变量与静态变量的重定位
      1. 自举代码找到自己的GOT,再通过.got.plt的第一个表项找到.dynamic段的地址
      2. 通过.dynamic段可以找到动态链接器自身的重定位表符号表,并对自身执行重定位
  • 装载共享对象:将共享对象的代码与数据映射到进程地址空间中

    • 符号的装填:将所有模块以及动态链接器的符号合并入全局符号表

      1. 动态链接器将可执行文件和自身的符号表合并入全局符号表
      2. 动态链接器根据.dynamic段中的DT_NEED项获得所有需要的共享对象
      3. 每装载一个新的共享对象,就会将其符号表合并入全局符号表

      注意:由于共享对象也可能依赖于共享对象,所以装载的过程实际可看作遍历图的过程

    • 全局符号介入:对于多模块定义的同名符号

      若同名符号在先前装载中已经加入全局符号表,则后加入的符号被覆盖忽略

      注意:“覆盖优先级”问题会引发全局的同名函数因被覆盖而失效

  • 重定位与初始化:自举、装载完毕后,动态链接器开始执行重定位操作

    • 重定位:动态链接器遍历所有可执行文件和共享对象的重定位表,修正它们的GOT表项
    • 初始化:重定位完成后,执行每个共享对象的 .init 段代码,进行初始化操作

  • Linux 动态链接器

    • 软链接:Linux 动态链接器的路径/lib/ld-linux.so.2是软链接,指向了真正的链接/lib/ld-x.y.z.so

    • ld.so的运作流程:动态链接器本身是一种特殊的可执行文件,内核会为其安排一个合适的装载地址

      1. 入口 _start 调用 _do_start( ) 函数独立进行重定位(即自举
      2. 进入 _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的行为
      1. 导出函数的行为发生改变,其功能与旧版本的函数功能不一致
      2. 导出函数被删除
      3. 导出数据的结构发生变化,如结构成员删除
      4. 导出函数的接口发生变化,如函数参数或函数返回值发生变化
  • 共享库版本命名: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符号版本机制的两个扩展

        1. .symver汇编宏:对符号sym设置符号标签,如 .symver sym, sym@VERS_1.1

        2. 允许多个版本的同一符号处于同一个共享库中,如:

        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做出以下规定:

      1. /lib:存放最关键的基础共享库,如动态链接器、C语言运行库等
      2. /usr/lib:存放非系统运行所需的关键性共享库
      3. /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:用于指定变量或函数符号的导入或导出
      1. __declspec(dllexport):表示从本DLL导出的符号
      2. __declspec(dllimport):表示从其它DLL导入的符号
  • 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

    1. 模块A导入b.dll中的foo函数,并在A的导入表中记录函数名“foo”
    2. 动态链接时,动态链接器在b.dll的函数名表中查找“foo”,再根据序号表找到对应的序号
    3. 根据序号算出函数在EAT中的下标,从而获得其RVA
  • EXP文件:链接器创建DLL的同时得到的临时文件,其中经过两遍扫描:

    1. 第一遍:扫描并收集所有目标文件的导出符号信息,创建DLL导出表,并将导出表放入EXP文件
    2. 第二遍:将EXP文件与其它目标文件链接在一起,输出为DLL文件的导出表
  • 导出重定向:EAT中存的并不是函数的RVA,而是指向导出表中的某个字符串

    该字符串是符号重定向后指向的“模块名 + 函数名

  • 导入表:保存了模块使用的来自DLL的变量或函数信息,本质上是IMAGE_IMPORT_DESCRIPTOR结构数组 数组元素结构如下:

    • FirstThunk:指向导入地址数组(IAT)
    • OriginalFirstThunk:指向导入名称表(INT);INT是与IAT完全相同的副本
  • 导入地址数组(IAT):每个表项对应一个被导入的符号

    1. 装载但未重定位时:表项元素值为对应导入符号的序号或符号名
    2. 链接完成后:表项元素值改写为对应导入符号的真正地址

  • 重定基地址(Rebasing):

    • PE中的DLL代码段不是地址无关的,拥有固定的装载基地址0x10000000
    • 装载时重定位:若DLL模块的目标地址被占用,系统会为其选择新的装载地址,对所有绝对地址重定位
    • 改变默认基址:在装载前直接改变DLL的装载基地址
    • 系统DLL:系统在进程中划出 0x70000000 ~ 0x80000000 的区域映射常用的系统DLL,如kernel32.dll

  • 导入函数绑定:在符号解析与重定位之前直接将外部函数的地址存入导入表

    • DLL绑定:遍历被绑定程序的导入表,将符号的目标地址写入被绑定程序的导入表(INT)内
    • 绑定失效:符号绑定存在以下两种失效的可能:
      • DLL更新:被依赖的DLL更新导致DLL的导出函数地址改变
      • DLL装载时重定位:被依赖的DLL的装载地址与被绑定时不一致
    • 绑定失效的解决方案:
      1. 绑定时:将导入DLL的时间戳(Timestamp)与校验和(Checksum)存入被绑定文件的导入表中
      2. 装载时:确认被装载的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包括版本一致、编译平台一致等


『operating system-2』dynamic link
http://larry0454.github.io/2023/07/25/operating_system/dynamic-link/
Author
WangLe
Posted on
July 25, 2023
Licensed under