7章: 动态链接

2022-07-17  本文已影响0人  my_passion

1 引入

(1) 静态链接 2个问题

1) 2个进程 (prog1 / prog2) 共享 .o 时, 磁盘和内存中 要存 2 份 .o => 空间资源浪费

2) 1个 (不依赖其他 .o 的) .o 变, 所有 .o 要重新链接 => 程序 更新、部署、发布 难

    |
    |   引入
    |/

(2) 动态链接

1) 思想: 把 链接/重定位 过程 (从 静态链接的 链接时) 延迟到 装载/运行时

    [1] 多进程 `共享的 .o` 在磁盘和内存中 `只存 1 份`
    
    [2] 1个 .o(不依赖其他 .o) 变 -> 只要 覆盖该 .o 

[3] 可扩展性&兼容性 好, 插件(Plug-in)式: 运行时 可 动态(选择)装载 各模块

2) 实现: 把程序 按模块拆分 成各 (相对) 独立 的 part, 程序 装载/运行时 再链接

    动态链接文件
                    
        Linux 
            `动态共享对象` DSO (Dynamic Shared Objects); 即 `elf 动态链接文件`
                .so
                    C 运行库 glibc: /lib/libc.so
        Windows 
            动态链接库 DLL (Dynamical Linking Library)
            .dll

Note: 理论上可直接用 .o 进行动态链接

3) 动态链接符号 (运行时)地址空间: 装载时, 由 装载器 动态分配

2 例子

    proj1.c  -> 编译 —— —— —— -> proj1.o 
                \ 共                 | 
                 \                  Linker
                  \                     |
                    Lib.c -> 编译 -> Lib.so: 动态符号 foobar 
                 /            |
                / 享      gcc -fPIC -shared -o Lib.so Lib.c
    proj1.c                         
    
    
    linker 发现 proj1.o 和 proj2.o 中引用的 符号 foobar 是 Lib.so 中的 动态符号 => `重定位 延迟到 装载时`

3 地址无关代码

(1) 共享对象 固定装载地址: 地址冲突

(2) 装载时 重定位

问题: 指令无法 在 多进程间 共享

    解决:

(3) 地址无关 代码 PIC (Position-Independent Code)

1) 思想: 将 指令中 需要重定位 的部分 分离, 与 数据 放一起

2) 共享对象4 种寻址模式

共享模块 中 地址引用是否跨模块 分为2类: 模块 内/外 引用

    ——————————————————————————————————————————————————————————
                4种 地址引用
    ——————————————————————————————————————————————————————————      
           | 调用、指令跳转                |   数据访问
    ——————————————————————————————————————————————————————————  
    模块内 | (1) 相对 跳转、调用      | (2) `相对` 地址访问
    模块外 | (3) 间接 跳转、调用 (GOT)    | (4) `间接` 访问 (GOT)
    ——————————————————————————————————————————————————————————  

[1] 模块内 地址引用: 相对 位置固定

    1] `callee 与 caller` 间: `相对 地址调用 ( call )`

    2] `指令` 与它要访问的 `数据` 间: 只需 相对于 PC 所指当前指令 加上 一 固定偏移

        转化为
        - - - > 相对于 `PC 所指 call <__i686.get_pc_thunk.cx> 指令
                的 next 指令( 函数 ...get_pc_thunk... 返回地址: 放 %ecx) 加上一固定偏移

[2] 模块外 地址引用: GOT (Global Offset Table, 全局偏移表)

    pointer array 
    
    各 表项 `指向` 放在 `进程虚拟内存 数据段(.data VMA) 中 地址相关的 func/var`

3) -fPIC / -fPIE: 产生 地址无关代码

4 延迟绑定(Lazy Binding)

动态链接下, 一开始就 link 所有 func => 程序 启动速度 慢

func 第1次 被用到时, 才绑定 (符号查找、重定位) => 加快 程序启动速度

实现: PLT (Procedure Linkage Table): GOT + 增加1个 中间层 间接跳转

    1) PLT
        _dl_runtime_resolve(moduleReferingFunc, func)
            将 func/bar 最终映射在 `.data VMA 中的 地址` 填到  GOT 的 `bar@GOT 表项`
                                                   |    
    2) PLT 结构     bar@GOT             GOT 结构 |/
            表项 —— —— —— —— —— —— —— ——>          表项 —— —— ——> `bar()` 在 进程虚拟空间 `.data VMA` 中 的 `地址`
                 GOT 中 指向 bar的 表项

5 动态链接 相关 结构 : 存在 ELF 可执行文件

动态链接
    OS 装载 可执行文件
        |
        |   控制权 转交
        |/
    
    动态链接器  的 入口地址 
        |     \
        |      \ Linux 下, 是 
        |       \
        |        共享对象 ld.so -> 被 OS 装载进 进程地址空间
        |/
    自身初始化 + 动态链接 
        |
        |   控制权 转交
        |/
    可执行文件 的 入口地址
    
                    
(1) .interp 段 // (interpreter 解释器)
    
    1) 作用
        `ELF 可执行文件` 中 `.interp  段 决定 `动态链接器 的 位置`
                            
    2) 内容                                           Linux 下
        字符串 = 可执行文件 所需 动态链接器的 路径 —— —— ——> /lib/ld-linux.so.2

(2) .dynamic 段
    
    存 动态链接器 所需基本信息
        动态链接 符号表位置/重定位表位置
        依赖于哪些共享对象
        共享对象 初始化代码 的 地址

(3) 动态符号表 .dynsym

           依赖于
    proj1  — —— —> Lib.so
           \        |
            \       | 定义 -> foobar 是 Lib.so 的 导出函数 (Export Function)
       引用  \        |
        |     \/    |/
        |     foobar() 函数
        |       
        |/  
    foobar 是 proj1 的 `导入函数 (Import Function)`
        
    
    动态符号表 .dynsym
        
(4) 动态链接 重定位表

    可执行文件 / 共享对象, 一旦 `依赖于 other 共享对象` ( 即, `有 导入导出符号`)
        其 code 或 data 中就会 `引用 导入符号`                         
            导入符号地址 在 运行时 才确定
                => 运行时 重定位
                
    用 PIC 的 可执行文件/共享对象 也需要 重定位
                |
                |
                |/
            代码段 中 绝对地址的引用 被 分离 -> 变成 GOT -> 放 数据段
            
            数据段 中 除了 GOT 之外, 还有 数据本身 绝对地址的引用
        
    ————————————————————————————————————————————————————————————    
    重定位表    |   修正 data 引用          |   修正 函数引用
    /修正的位置  |                           |
    ————————————————————————————————————————————————————————————
    静态链接    |   .rel.data               |   .rel.text
    ————————————————————————————————————————————————————————————
    动态链接    |   .rel.dynamic + 数据段  |   .rel.plt 
                |    / .got(也在数据段)      |    / .got.plt
    ————————————————————————————————————————————————————————————

6 动态链接 步骤 与 实现

    (1) 3 step
    
        1) 动态链接器 `自举(BootStrap)`

            ——————————————————————————————————————
                              | 由谁完成 重定位 ?
            ——————————————————————————————————————                                  
                普通共享对象  | 动态链接器
            ——————————————————————————————————————
                动态链接器     | 自身 
                (特殊共享对象)|
            ————————|——————————————————————————————     
                    |/
            `鸡生蛋, 蛋生鸡` 的 无限循环问题 
                    |
                    |   解决
                    |/
                2个问题
                    动态链接器
                        1> 本身 不能依赖 other 共享对象
                            -> 编程时 人为控制
                    
                        2> 本身 所需 globalVar 和 staticVar 的 重定位 由自身完成
                                                      称
                            -> 启动时 用 精巧的代码 - - -> 自举
            
        2) 装载 共享对象
            
            [1] 从 可执行文件 开始 构建 `依赖(共享对象)关系图`
                `.dynamic 段` 中, 入口为 `DT_NEEDED` 的 类型 -> 指出的是 `所依赖的共享对象`
                
            [2] 图遍历: 广度优先 / 深度优先
                打开 共享对象
                    读 ELFHeader 和 .dynamic 段 
                        相应 代码/数据段 映射到 进程空间
                        符号表 合并到 `全局符号表`
                
        
        3) 重定位 与 初始化
    
    (2) Linux 动态链接器 实现
    
        OS 内核 装载完 `ELF 可执行文件` 
            |                           1) `ELFHeader 中 e_entry`
            |/                       /  
        返回 用户空间                 / 静态链接 (没 .interp 段)    
        + 控制权 转交给  程序入口     
                |                   \ 动态链接 (有 .interp 段)
                |                    \  
        据 ELF 文件是否有 .interp 段  \ 
                                       \
                                        2) 内核 分析 `动态链接器 地址(在 .interp 段)` 
                                            |
                                            |
                                            |/
                                        将 动态链接器 映射到 进程空间
                                            |
                                            |
                                            |/
                                        控制权 转交给 `动态链接器 的 e_entry`
            
            共享库 与 可执行文件 没本质区别
                ELFHeader 的 标志位 和 扩展名       
    
    (3) 3个问题 `动态链接器` (ld.so)
        
        1)  本身 `是 动态/静态链接` ?
            静态
                它 不能依赖 other 共享对象
        
        2) 本身 `必须是 PIC 吗` ?
            可以是(更简单), 也可以不是(代码段 无法共享 / 自举时 还要对 代码段 重定位)
        
        3) 可被当作 可执行文件 运行, 那它的 `装载地址` 是多少 ?
            0x00000000 -> 无效地址 -> 作为 `共享库`, OS 内核 装载 ld.so 时, 会为其 选择合适的装载地址 

7 显式运行时 链接

动态链接器 提供 3 个 API

        (1) dlopen()  打开动态库
        (2) dlsym()   查找符号
        (3) dlerror() 错误处理
        (4) dlclose() 关闭动态库
静态链接: 磁盘/内存 中 2 份共享库 Lib.o.jpg 静态链接: 磁盘/内存 中 1 份共享库 Lib.so/o.jpg 动态链接过程.jpg 4种 寻址模式.jpg 相对偏移 调用指令.jpg 模块 内 数据访问.jpg 模块 间 数据访问.jpg 模块间 调用、跳转.jpg GOT 中 PLT.jpg Lib.so 中 .got.plt 结构.jpg
上一篇下一篇

猜你喜欢

热点阅读