收藏

4章 静态链接

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

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
上一篇下一篇

猜你喜欢

热点阅读