链接

  • 基本概念
    • 分类:静态链接、动态链接。
    • 工作步骤:
      • 符号解析:每个目标文件都带有符号表,链接器将每个符号引用与所有目标文件中恰好一个符号定义关联。
      • 重定位:把所有目标文件合并,并更新符号引用的位置。
  • ELF 文件
    • 分类
      • 可重定位目标文件:.o,与一个 .c 对应。
      • 可执行目标文件:如 a.out
      • 共享目标文件:.so,在加载或运行时动态链接。
      • .text:存放代码,可执行,不可写。
      • .rodata:只读数据,如字符串常量、全局常量。
      • .data:数据段,其中的变量为全局变量或静态变量,且被初始化为非 0 值。
      • .bss:数据段,与 .data 类似,但是变量未初始化或以 0 初始化。
        • 如果 COMMON 存在,则未初始化全局变量优先存放在其中。
      • COMMON:未初始化的全局变量。
      • .symtab:符号表,链接时使用。
      • .rel.text.text 节的重定位信息。
      • .rel.data.data 节的重定位信息。
      • .debug:调试符号。
  • 链接器符号
    • 分类
      • 全局符号:当前模块定义、其他模块可引用的符号。
      • 外部符号:当前模块引用、其他模块定义的符号。
      • 局部符号:只在当前模块可用的符号,用 static 修饰,不是局部变量。
    • 符号解析
      • 会成为符号的 C 语言元素:
        • 函数的声明、定义。
        • 全局/静态变量的声明、定义。
        • 函数内静态变量的声明、定义。
      • 符号强弱:
        • 强符号:初始化过的全局符号,函数定义一定是强符号。
          • 根据是函数还是变量、初始化的值,符号进入 .text.data.bss
        • 弱符号:未初始化的全局符号。在编译 .c.o 后,这些符号进入 COMMON。
      • 解析规则:
        • 只有一个强符号可以存在,多个强符号互相冲突。
        • 强符号比同名的多个弱符号都优先,即 COMMON 中会被覆盖。
        • 无强符号时,同名的多个弱符号随机选一个。
        • 选择符号不考虑 C 语言的类型,只与强弱、随机性相关。
    • 重定位
      • 重定位条目
        • typedef struct {
              long offset;
              long type:32, symbol:32;
              long addend;
          } Elf64_Rela;
        • 每一处符号引用都会被转换为 Elf64_Rela,供链接器做重定位处理。
        • offset 表示此引用相对于段起始地址的偏移量。
        • type 表示重定位的类型:
          • R_X86_64_32:32 位绝对地址引用。
          • R_X86_64_PC32:32 位 PC 相对地址引用,偏移量是目标相对于指令末端地址。
        • symbol 表示被引用的符号在符号表中的索引。
        • addend 与指令长度相关,用于处理计算指令末端。
      • 32 位绝对地址引用
        • 此时一定有 addend == 0
        • 重定位结果 refptr = ADDR(symbol) + addend = ADDR(symbol)
      • 32 位 PC 相对地址引用
        • 首先计算符号引用本身的位置 refaddr = ADDR(.text) + offset
        • 再计算重定位结果 refptr = ADDR(symbol) + addend - refaddr = ADDR(symbol) - (refaddr - addend)
        • refaddr - addend 即当前指令的末端的地址。
  • 加载目标文件
    • 静态链接
      • 静态库为 lib<name>.a
      • 编译时 gcc 命令用 -l<name> 完成链接。
      • gcc 命令需要注意顺序,符号解析过程只按照命令行顺序。
        • 每链接一个目标文件或静态库,则尝试用其中的符号解析现有的未定义符号,但符号不会用于后续的解析。
        • 所有文件都处理后,若还有未定义符号,则报错。
        • 所以要把被依赖的库文件放在依赖的目标后面链接。
    • 加载时动态链接
      • 编译时在 gcc 命令参数中添加动态库,对动态库的引用被加入可执行文件。
      • 运行可执行文件时,在加载阶段由动态链接器加载所有动态库。
      • 动态链接器一般为 ld-linux.so 或各种变体。
    • 运行时动态链接
      • 不需要在编译时引用动态库,而是在代码中用 dlopen() 动态加载动态库。
      • 只有在执行到需要的时候才会按需加载。