Linkage

程序从源代码到可执行文件需要经过 4 个步骤, 链接就是其中重要的一步.

链接器的主要工作是将具备引用关系的不同编译单元的编译结果 (i.e. 可重定位目标文件) 相互连接, 组成一个可执行文件.

静态链接器以一组可重定位目标文件和命令行参数作为输入, 生成一个完全链接的, 可以加载运行的可执行目标文件.

链接器必须实现两个主要功能以实现链接:

  1. 符号解析 (symbol resolution): 目标文件中会定义和引用符号, 每个符号对应于一个函数, 全局变量或静态变量. 符号解析的目的是将符号引用和符号定义关联起来.
  2. 重定位 (relocation): 编译器和汇编器生成从 0 开始的代码和数据节. 链接器需要通过将每个符号定义与一个内存地址关联起来实现 重定位 . 随后链接器需要修改所有对该符号定义的引用, 使其指向该地址. 链接器使用汇编器所产生的 重定位条目(relocation entry) 进行这样的重定位.

目标文件

目标文件有三种形式:

  1. 可重定位目标文件
  2. 可执行目标文件
  3. 共享目标文件

编译器和汇编器可以生成可重定位目标文件和共享目标文件, 链接器根据这些文件生成可执行目标文件

符号解析

链接器进行符号解析的方法是将每个引用和它输入的可重定位目标文件符号表中定义的一个确定符号定义关联起来.

全局符号解析

全局符号分为 两种:

  • 符号: 函数和已初始化的全局变量
  • 符号: 未初始化的全局变量

Linux 链接器对于多重定义的全局符号处理规则:

  1. 不允许有多个同名强符号
  2. 如果有一个强符号和多个弱符号同名, 选择强符号
  3. 如果多个弱符号同名, 从中任选一个

对[[静态库]]的解析

链接器按照文件在命令行参数中出现的顺序依次解析符号, 当涉及静态库时, 具体地说, 链接器维护三个集合:

  • 可重定位目标文件集合 EE: EE 中的文件会被组合成可执行目标文件
  • 未解析的符号集合 UU: 即引用但尚未发现定义的符号集合
  • 已发现定义的符号集合 DD: 即在之前的输入文件中已经找到定义的符号集合

初始时, 三个集合均为空:

  • 对于命令行参数中每个文件 ff, 链接器首先判断 ff目标文件 还是 存档文件
    • 如果是 目标文件: 链接器将 ff 添加到 EE, 同时修改 DDUU 反映 ff 中符号的定义和引用
    • 如果是 存档文件: 链接器尝试匹配 UU 中未解析的符号和存档文件 ff 中定义的符号.
      • 对于 匹配到的模块 mm (mmff 中被定义且其引用存在于 UU 中) , 链接器将 mm 添加到 EE, 并修改 UUDD 以反映 mm.
      • 对于 所有未匹配到的模块 , 链接器简单地丢弃这些模块
  • 链接器处理完参数中所有的文件后, 如果 UU 是非空的, 那么说明存在被引用但未定义的符号, 链接器会输出错误并终止. 否则链接器会合并和重定位 EE 中的目标文件, 构建可执行目标文件

重定位

符号解析之后, 代码中每个符号的引用和一个符号定义关联起来. 链接器此时就知道了他的输入目标模块中代码节 (.text) 以及数据节 (.data .rodata) 的确切大小, 即具备了为每个符号分配运行时地址的信息.

重定位由两个步骤组成:

  1. 重定位节和符号定义: 链接器将所有相同类型的节合并为同一个聚合的节. 即, 把来自所有输入模块的 .text, .data 等分别聚合为一个. 随后, 链接器为所有的节以及所有的符号赋予运行时内存地址, 这一步完成时, 程序中所有的指令和全局变量都被分配了唯一的运行时地址.
  2. 重定位节中的符号引用: 链接器修改代码节和数据节中对每个符号的引用, 使得它们指向正确的运行时地址. 这一步的进行依赖于可重定位目标模块中的 重定位条目 (relocation entry) 数据结构