美文网首页收藏
4章 静态链接

4章 静态链接

作者: my_passion | 来源:发表于2022-07-10 13:24 被阅读0次

    2 个 目标文件 (.o) 如何链接成 可执行文件 (.out)

        // a.c
        extern int sharedd;         
        int main()                  
        {                           
            int a = 100;
            swap(&a, &shared);
        }
        
        // b.c      
        int shared = 1;
        void swap(int* a, int* b) 
        { /* ...*/ }
        
        // 得 a.o b.o
        $ gcc -c a.c b.c 
    

    4.1 空间与地址分配

    段 / 符号 映射: 目标文件 -> 可执行文件 -> 进程虚拟空间

    1 相似段合并 => 各 段虚拟地址 确定

        .bss 段
            在 .o elf 内 不占空间
            装载时, 在 `进程虚拟空间` 才占空间
            
        各 .o 的 .code 段 -> 合并: 放 elf 的 .code 段
                         .data 段                    .data 段
            ————————————————————————————————————————————————    
                    |   VMA 虚拟内存地址
            ————————————————————————————————————————————————            
            链接前 |   所有段 的 VMA = 0: 虚拟空间 还没分配
            ————————————————————————————————————————————————
            链接后 |   VMA = `进程虚拟内存地址`
                    |           文件偏移 忽略
            ————————————————————————————————————————————————
    

    2 各段内 符号: 段 虚拟地址 + 段内相对偏移

        .o
            符号地址确定, 各 符号表 中 符号 定义/引用
            
        elf
            放 全局符号表, 函数符号 / 变量符号 放 .code 段 / .data 段
            各符号 `段内相对偏移 固定`
        
        .out 
            符号 虚拟地址 = 段虚拟地址 + 段内相对偏移`
    
    例: 进程虚拟地址分配
    
        elf: 默认从 0x0804 8000 开始分配
            
        .code 段虚拟地址 = 0x0804 8094
        
        main 符号 位于 a.o 的 .code 段 最开始
            => 段内 相对偏移 = 0
            => 进程虚拟空间 地址 = 0x0804 8094 + 0 = 0x0804 8094        
    
    全局符号表
    ——————————————————————————————————  
        符号  类型  (进程)虚拟地址
    ——————————————————————————————————
        main    函数  0x0804 8094
        swap    函数  0x0804 80c8
        shared  变量  0x0804 9108
    ——————————————————————————————————
    

    4.2 符号解析 与 重定位

    1 符号解析 = 匹配 符号声明 与 符号定义 = 据 符号引用全局符号表 查匹配的 符号(进程虚拟) 地址

        无匹配 => `符号未定义` symbol undefined:
    
            1) 符号 声明 与 定义 不一致
            2) 目标文件 路径不对
            3) 缺少 某个 库
    

    2 重定位

        linker 
            查 重定位表(.rel.data 段)
            据 指令修正方式(绝对/相对 寻址修正) 调整 
    

    (1) 查 重定位表 得 .o 文件中 要重定位的 符号位置: 0x1c / 0x27

    $ objdump -r a.o    
    
    
        ...                  
            OFFSET  TYPE            VALUE
            0x1c    R_386_32        shared
            0x27    R_386_PC32      swap
        
        重定位表 是 array, elem 是 `重定位入口` struct 
                                                
            r_offset        
                重定位入口 的 偏移 
    
            r_info   
                
                重定位入口 的 
                    类型 ( FUNC / VAR)            : 低 8 位
                        -> 绝对/相对 寻址修正
                        
                    符号 在 全局符号表 中 下标  : 高 24 位
                        + 查 全局符号表 -> 符号虚拟地址
    

    (2) compiler 不知道 所引用的 sym 地址, fill 临时假地址

    .o 反汇编 -d(disassembly)

    $ objdump -d a.o
    
    
        ...                 
        0000 0000 <main>:   
        .                  0x1c: 指令 (相对地址) 偏移
        .                   |    0x0000 0000 = 0
        .                   |/  /
        18:     c7 44 24 04 00 00 00    movl $0x0, 0x4(%esp)
        1f:     00                               \_ _ _    
        ...                                            \           
        26:     e8 fc ff ff ff          call 27 <main + 0x27>       
                  /|    \                                           
                   |     \                                         
                  0x27  小端存储: 低地址存低字节 0xffff fffc = -4      
    

    (3) 指令修正

    1) globalVar - 绝对寻址修正: .o / .elf -> 0 / 符号虚拟地址 (查 全局符号表 )

            —————————————————————————————————————————————————————
                            |   `重定位入口处` 的 值        
            —————————————————————————————————————————————————————                               
            .o 中        |   固定填 0
            —————————————————————————————————————————————————————
            .elf 中      |   填 符号虚拟地址 (查 全局符号表 )
            ————————————————————————————————————————————————————                 
    

    2) func -> 相对寻址修正: .o / .elf -> 临时假地址 / x = S + A - P

    S: callee 符号虚拟地址

    A: 临时假地址 = 要修正的长度 的负值, 通常 = -4

    P: caller 中 要修正的指令位置 = 引用 funcSym 的 .code 段(本例是 main)起始地址 + 重定位表 中 OFFSET

    P - A = call S 指令的 next 指令的地址

    P - A + x = S

        S = 0x0804 80c8
        A = -4 
        P = 0x0804 8094 + 0x27 = 0x0804 80bb
        S + A - P = 0x09 
        
        ——————————————————————————————————————————————————————————————————————————  
                |   重定位入口 处 的值
        ——————————————————————————————————————————————————————————————————————————                  
        .o 中    |   临时假地址 = `要修正的长度 (-A)` : Compiler 可计算出           
        ——————————————————————————————————————————————————————————————————————————      
        .elf 中 |   x = S + A - P = 0x09 
        —————————————————————————————————————————————————————————————————————————
    

    => 查 elf 可执行文件 的 反汇编

        $ objdump -d ab                   
                                          
            ...                           
        0x0804 8094 <main>:                         shared
        ...                                             /
        0x0804 80ac: c7 44 24 04 08 91 04        movl $0x8049708, 0x4(%esp)
        0x0804 80b3: 08                   
        ...                               
        0x0804 80ba: e8 09 00 00 00              call 80480c8<swap>
        0x0804 80bf: ...  \                             \
            |              \                             \
            |_ _ _ _ _ _ _ _\ + = 0x09 + 0x0804 80bf = 0x0804 80c8  
            |
        call 指令 的 next 指令 的 地址
    
    相似段合并.jpg 段映射 / 段地址分配.jpg 绝对/相对 地址指令.jpg

    4.3 COMMON 块

        弱符号 机制  
            (1) 允许 `同名` 但 `不同类型` 的 `符号定义` 存在于 多个文件, 只要它们 `未初始化`
            
            (2) 典型 弱符号
                uninit_global_var
            
            (3)
            ——————————————————————————————————————————————————————————————        
                    |   处理 弱符号, 为啥 (在 .o 中) 不放 .bss 段 ?
            ——————————————————————————————————————————————————————————————        
            编译器     |   将 `1个编译单元` 编译成 `1个 目标文件 .o` 时, 
                    |   `不知道` 弱符号 `最终应占的空间大小`
                    |   => 无法 在 .o 中 为其 分配/预留 空间 (大小)
                    |   => 无法 在 .o 中 将其放 .bss 段
                    |   => 标记为 `符号表` 中的 `COMMOM 型` 
            ——————————————————————————————————————————————————————————————              
            链接器  |  据 所有 .o 可确定 弱符号 大小
                    |   => .elf 文件中 可将其放 `(虚拟) .bss 段`
                    |   => 最终放 进程虚拟内存 中 .bss 段
            ——————————————————————————————————————————————————————————————  
            
            (4) note
            
                链接器 
                    1) 不知道 变量类型 (int / float 等)
                    2) 知道 变量大小(size)
                    3) 能识别 变量 在/是 `COMMON 型`/弱符号
                        
                uninit_local_static_var 强符号 -> 编译器 会放 .o 的 .bss 段         
            
            (5)
                ————————————————————————————————————————————————————————————————————
                多个 `同名 不同类型` |  `链接器` 如何处理?
                    的 强/弱符号  | 最终在 .elf/进程虚拟空间 中 `所占空间大小`
                ————————————————————————————————————————————————————————————————————
                1) 2 强               |  报错 `符号重定义` (symbol multidefined)
                ————————————————————————————————————————————————————————————————————
                2) 1 强 other 弱     |     = 强符号大小
                ————————————————————————————————————————————————————————————————————
                3) 全 弱           |  按 COMMON 型 链接规则: = size 最大者 的 size  
                ————————————————————————————————————————————————————————————————————
    

    4.4 C++ 去重 -> 函数级链接 / 全局构造与析构函数 ( 链接后 放 .init / .fini 段) / ABI

    (1) C++ 中 `必须由 编译器 和 链接器 共同协作` 才能完成的 `2 个特性`
        
    1) 去重: 消除 重复代码
    
    产生 `重复代码` 的 
        
        1> 表现:
            在 `不同 编译单元 (.o)` 生成 `相同 code`
        
        2> 3个 弊端:
            1] 空间浪费 
            2] 指向 同一函数 `2个 函数指针 可能不相等`
            3] 指令 cache 的 命中率 降低
                CPU 缓存 指令和数据 + 同一份指令 有多份 copy 
                        
    3>  原因:
    1] 模板
    
        本质上 像 宏
    
        模板 在 `某个编译单元 被 实例化` 时, `它(模板) 并不知道` 自己是否在 `other 编译单元` 也被实例化
            => 模板 在 多个编译单元 同时实例化为 `相同类型` 时, 产生 重复 code
            
                
        4> 解决
            模板 的 `每份 实例化代码 单独放 1 个段`
                add<T>()
                    编译器 
                        1.o 2 个段
                            .temp.add<int>
                            .temp.add<float>
                        2.o 2 个段
                            .temp.add<int>
                            .temp.add<float>
                    |
                    |   gcc 标识 `Link Once` 段
                    |/
                    
                    链接器
                        .elf
                            2 个 .o 中 的 `同名(同实例化代码) 代码段` 
                                `只取 1个` 放到 .elf 中 .code 段
                    |
                    |
                  gcc / visual C++ 都是这种做法
                    gcc
                        编译器
                            将 `模板 实例化代码 所放段`  标识为 `Link Once 段` 
                                段名
                                    .gnu.linlonce.decoratedNameOfTemplateInstantiation
    
    2] vtbl
        含 vf 的 class
            编译器 在 `每个 引用 该 class` 的 `编译单元(.o)` 中都会生成 `class 相应的 vtbl` -> 代码重复
                        
                3-6] extern inline func 
                     默认 ctor / copy ctor / operator=
            
            4> 解决
            
                都类似 模板 的去重思路
                    |
                    |   问题: 不同 编译单元(.o) 的 编译器版本/编译优化选项不同 
                    |   => 同名段 content 不同
                    |/
                编译器 任选 1 个 作 链接输入 + 给出 warning
                    
            |
            |   由模板 去重思路 延伸到 `优化/减小 链接后 的 .elf 文件 size`: 
            |       对 任一 `函数 或 变量` -> 放 .o 中 `独立段` 
            |/
            
        `函数(/变量) 级别链接`
            只 引用 .o 中 某个 func/var 时, 
                `不用将 .o 整体 链接`, `只对 所用到的 func/var 进行 链接` 
                    |
                    | 好处: 链接输出的 `.elf 文件 变小`
                    |
                    | 代价: 编译 / 链接 变慢
                    |   所有函数/var 放 独立段 => 段数量大大增加
                    |   =>
                    |       1> .o 文件变大
                    |       2> 链接器 重定位过程 复杂
                    |/
            gcc 编译链接选项: -ffunction-sections / fdata-sections
        
        
    2) 全局构造 与 析构: 在 main 之前/后 执行
        
        Linux 系统 程序入口: _start -> Lunux 库 Glibc 的 part
        
        .elf 文件 2 个特殊段
            `.init / .fini 段`
                存 进程 初始化/终止 的 可执行指令
                
                main 之前/后, Glibc 安排执行 .init / .fini 段 可执行指令(代码)
                
                    => C++ 全局 构造/析构 函数 -> 链接后 放 .init / .fini 段
    
    (2) C++ 可执行代码 二进制兼容性 —— ABI (Application Binary Interface) 二进制层面的接口
        
        要考虑 ABI 的 原因: 
            重载 / 继承 / vf / 异常 机制, 使得 C++ 背后的 数据结构 非常复杂, 在 不同 编译器和链接器 之间 不可移植
    
        
        两个编译器 编译出的 目标文件 能链接 的 条件
            同
                目标文件格式
                符号修饰规则
                内存分布
                函数调用方式
                
        许多团体和社区都在致力于 C++ ABI 标准的统一
    

    4.5 静态库 链接

        (1) 程序 I/O
                  |
                  |  用
                  |/
              
            scanf / printf
        
                  |  是对 OS 的 API 的 wrap (包装)
                  |
                  |  调 
                  |/
                  
            Linux 下 API: write 的 system call
        
        (2) 静态库 
            是一组 `目标文件(.o) 的 集合`
            
            Linux 下 C 语言 `静态库 libc 文件`: /user/lib/`libc.a` 是 `glibc 项目` 的一部分
                                     
                glibc 是用 C 语言开发的, 由 数千个 C 源文件组成
                    编译输出 数量相同的 目标文件
                        scanf.o / printf.o
                        fread.o / fwrite.o
                        malloc.o
    
    静态库链接.jpg

    相关文章

      网友评论

        本文标题:4章 静态链接

        本文链接:https://www.haomeiwen.com/subject/smswbrtx.html