链接器如何处理静态库


在软件开发中,链接是将各个编译单元组合成最终的可执行文件或库的关键步骤。对于使用 GNU 工具链(例如 GCC、G++)的开发者来说,理解链接器 ld​ 的工作原理至关重要。本文将深入探讨 GNU 链接器的工作机制,特别是关于链接顺序的细节,并提供一些实用技巧。

链接的基础知识

GNU 链接器 ld​ 的基本工作单位是目标文件 (Object File) ,即 .o​ 文件,它们是源代码经过编译但尚未链接的中间产物。链接器主要关注目标文件中的两种符号:

  • 导出符号 (Exported Symbols / Output Symbols) :定义在目标文件内部,可以被其他目标文件使用的符号,例如函数或全局变量。
  • 未定义符号 (Undefined Symbols) :目标文件中引用了但没有定义的符号,需要在链接阶段找到其定义。

链接器的核心任务是解析所有目标文件的未定义符号,将它们与其它目标文件中的导出符号匹配起来,最终生成可执行文件或库。

重要原则:ld​ 要么链接整个目标文件,要么完全不链接。即使一个目标文件中只有少数符号被使用,整个目标文件的内容也会被链接到最终目标中。这也是为什么一些简单的 “Hello, World” 程序体积却很大的原因之一(因为它们链接了一些标准库,即使只用到了其中一小部分功能)。

查看符号

我们可以使用 nm​ 工具来查看目标文件和库中的符号信息。

  • 查看目标文件中的未定义符号

    nm -u <object_file.o>

    例如:nm -u main.o

  • 查看目标文件或库中的所有符号

    nm <object_file.o or library.a>

    例如:nm b.o​或者:nm libb.a

符号类型说明:

  • U​:表示未定义的符号。
  • T​:表示代码段中的定义的符号。
  • D​:表示已初始化的数据段中的定义的符号。
  • B​:表示未初始化的数据段中的定义的符号。
  • C​:表示COMMON 符号,通常是未初始化的全局变量。
  • W​:表示弱符号。
  • ...​:更多符号类型请参考 man nm​。

目标文件链接:简单的算法

当只涉及目标文件链接时,ld​ 使用一个简单的算法:

  1. 维护两个列表:

    • 已知符号列表 (Defined Symbols List) :记录已解析的符号及其定义所在的目标文件。
    • 未知符号列表 (Undefined Symbols List) :记录尚未找到定义的符号。
  2. 依次处理每个目标文件:

    • 将目标文件中的所有导出符号加入已知符号列表。如果遇到符号冲突(多重定义),则报错。
    • 用目标文件的导出符号解析未知符号列表中的符号,并将已解析的符号从未知符号列表中移除。
    • 将目标文件中剩余的未定义符号加入未知符号列表
  3. 处理完所有目标文件后,如果未知符号列表非空,则报告 “undefined reference” 错误。

在仅链接目标文件的情况下,链接顺序不影响最终结果。

静态库链接:顺序的重要性

静态库(.a​ 文件)本质上是一组目标文件的集合。链接静态库时,ld​ 的行为略有不同,链接顺序变得至关重要

  1. ld​ 依次检查静态库中的每个目标文件。
  2. 如果一个目标文件的导出符号可以解决未知符号列表中的任何符号,则将该目标文件加入链接,更新已知符号列表未知符号列表(参考上一节的步骤)。
  3. 如果一个目标文件无法解决未知符号列表中的任何符号,则跳过该目标文件。在没有特定选项的情况下,ld后续不会重新考虑被跳过的目标文件
  4. 如果步骤 2 中加入的目标文件引入了新的未定义符号,则 ld​ 会重新扫描 同一个 静态库,尝试解析新引入的未定义符号。此过程重复进行,直到没有新的未定义符号,或者本轮扫描无法解析出任何新的未定义符号。

**注意:**上述规则意味着同一个静态库内的目标文件链接顺序无关紧要。但是,不同静态库之间的链接顺序非常重要。

示例:

假设有三个源文件:

bar.cpp​:

#include <iostream>void bar() {    std::cout << "bar()" << std::endl;}

foo.cpp​:

#include <iostream>void bar();void foo() {    std::cout << "foo()" << std::endl;    bar();}

main.cpp​:

#include <iostream>void foo();int main() {    std::cout << "main()" << std::endl;    foo();    return 0;}

如果将 foo.cpp​ 和 bar.cpp​ 分别编译成静态库 libfoo.a​ 和 libbar.a​,则以下链接命令:

  • g++ -o app.exe main.o libfoo.a libbar.a​:成功
  • g++ -o app.exe libfoo.a libbar.a main.o​:失败,提示 undefined reference to 'foo'
  • g++ -o app.exe main.o libbar.a libfoo.a​:失败,提示 undefined reference to 'bar'

解释:

  • 第一个命令成功是因为 main.o​ 的未定义符号 foo​ 可以由 libfoo.a​ 解决,libfoo.a​ 的未定义符号bar​ 可以由libbar.a​ 解决。
  • 第二个命令失败,因为 ld​ 先处理 libfoo.a​ 和 libbar.a​,当时 main.o​ 未处理,foo​ 还不在未知符号列表中,所以 libfoo.a​ 被跳过。然后处理main.o​ 时 foo​ 成为未定义符号,而 libfoo.a​ 已经被跳过,foo​ 无法解析。
  • 第三个命令失败,因为 main.o​ 需要 foo​,libbar.a​ 无法提供,所以被跳过。libfoo.a​ 提供了 foo​,但是需要 bar​,而 libbar.a​ 已经被跳过。

经验法则: 如果库 A 依赖于库 B,则在链接命令中 A 应该放在 B 之前。

循环依赖

当静态库 A 依赖于静态库 B,同时静态库 B 也依赖于静态库 A 时,会出现循环依赖。

例如,将上例中的 bar.cpp​ 修改为:

#include <iostream>void foo();void bar() {    std::cout << "bar()" << std::endl;    foo();}

此时,libfoo.a​ 和 libbar.a​ 相互依赖。

  • g++ -o app.exe main.o libfoo.a libbar.a​:成功
  • g++ -o app.exe main.o libbar.a libfoo.a​:失败,提示 undefined reference to 'foo'

在循环依赖的情况下,简单的调整静态库顺序可能无法解决问题。

动态库链接 (SO)

对于动态库(.so​ 文件在 Linux/Unix 上),链接行为有所不同。

  • SO 文件: 链接时 ld​ 将 .so​ 文件视为一个整体进行处理,类似于处理单个目标文件。多个 .so​ 文件之间的链接顺序通常不影响结果。

重要区别: 生成 SO 时允许存在未解析的符号(在运行时解析)。

解决链接问题的进阶选项

如果默认的链接行为无法解决问题,ld​ 提供了一些选项来控制链接过程:

1. -start-group​ 和 -end-group

这两个选项可以将多个静态库视为一个组,使得 ld​ 在组内所有静态库中循环扫描,直到无法解析出新的未定义符号或者没有新的未定义符号。这可以解决循环依赖问题。

g++ -o app.exe main.o -Wl,-start-group libfoo.a libbar.a -Wl,-end-group

注意: 使用 -start-group​ 和 -end-group​ 会增加链接时间,尤其是在大型项目中。

2. --whole-archive​ 和 --no-whole-archive

--whole-archive​ 选项会强制 ld​ 将其后所有静态库中的所有目标文件都链接进来,无论这些目标文件是否被需要。--no-whole-archive​ 选项则恢复默认的链接行为。

g++ -o app.exe main.o -Wl,--whole-archive libbar.a -Wl,--no-whole-archive libfoo.a

注意: 使用 --whole-archive​ 会显著增大最终生成文件的体积,因为会链接进大量可能不需要的代码。

总结

本文深入探讨了 GNU 链接器 ld​ 的工作机制,特别是关于链接顺序的细节,内容要点包括:

  • 导出符号和未定义符号的概念。
  • nm​ 工具查看符号信息的使用方法。
  • 目标文件之间的链接顺序不重要。
  • 静态库之间的链接顺序很重要:被依赖的库应该放在依赖它的库的后面
  • 循环依赖需要特殊处理,可以使用 -start-group​ 和 -end-group​ 选项。
  • 动态库的链接行为与静态库有所不同。
  • 谨慎使用 --whole-archive​ 选项。

文章作者: 张兵帅
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 张兵帅 !
  目录