【问题标题】:Why can't ld ignore an unused unresolved symbol?为什么 ld 不能忽略未使用的未解析符号?
【发布时间】:2022-01-21 20:45:17
【问题描述】:

考虑以下源文件:

交流

extern int baz();

int foo() { return 123; }
int bar() { return baz() + 1; }

b.c:

extern int foo();

int main() { return foo(); }

现在,当我尝试使用这些资源构建程序时,会发生以下情况:

$ gcc -c -o a.o a.c
$ gcc -c -o b.o b.c
$ gcc -o prog a.o b.o
/usr/bin/ld: a.o: in function `bar':
a.c:(.text+0x15): undefined reference to `baz'
collect2: error: ld returned 1 exit status

这是在 Devuan GNU/Linux Chimaera 上,带有 GNU ld 2.35.2,GCC 10.2.1。

为什么会这样?我的意思是,不需要任何复杂的优化就可以知道baz()bar() 中并不真正需要 - ld 在某些时候自然会注意到这一点 - 例如在完成对bar() 的遍历时,没有注意到使用baz() 的位置。

现在,你可以说“einpoklum,你没有让编译器为你找麻烦”——我想这很公平,但即使我在这些指令中使用-O3,我也会得到相同的结果错误。

注意:启用 LTO 和优化后,我们可以规避这个问题:

$ gcc -c -flto -O1 -o b.o b.c
$ gcc -c -flto -O1 -o a.o a.c
$ gcc -o prog -O1 -flto a.o b.o
$ /prog ; echo $?;
123

【问题讨论】:

  • 启用死代码删除并使用 -fdata-sections -ffunction-sections
  • @0___________: 1. 但不管怎样,链接时死代码都会被删除,不是吗?它不像链接器将库的 all 放在可执行文件中......还是这样? 2. 为什么我需要使用神秘的-f 开关才能让一些微不足道的事情发生? :-(
  • @它有用吗?
  • @0___________:我不确定您所说的“启用死代码删除”是什么意思。 -O3 会处理它吗?另外,这些开关是用于编译还是用于链接?当我在所有 gcc 调用中使用 -O3 -flto -fdata-sections -ffunction-sections 时,它不起作用。
  • 编译器 不知道bar 不会被使用,并将其放入公共.text 部分。链接器知道,但它不能消除具有函数粒度的代码,它可以删除部分。这就是为什么每个函数都需要放在一个单独的部分中。

标签: c linux linker undefined-symbol lto


【解决方案1】:

在此代码的“普通”传统编译中:

extern int baz();

int foo() { return 123; }
int bar() { return baz() + 1; }

编译器创建一个对象模块,其中包含两个例程的代码以及符号foobar 的定义以及对baz 的引用。没有什么可以告诉链接器属于foo 的代码在哪里开始和结束,属于bar 的代码在哪里开始和结束,甚至任何给定的代码片段——或对象模块中的任何给定字节——只属于到foobar 之一。如果我用汇编语言编写并组装成一个对象模块,我可以在foo 中包含跳转到bar 的代码(仅使用由汇编程序计算的硬编码偏移量,并且不会在链接器可见的任何符号中显示)反之亦然。

所以链接器无法知道foobar可以分开。

后来,为编译器创建了一个协议,以保持函数分离,并在目标模块中提供足够的信息,链接器可以确定它们在哪里分离,并告诉链接器可以分离函数。当启用该选项时,链接器可能能够在程序中包含foo,而不包含bar

此功能还不是工具中的默认功能,这是各种构建系统和项目的遗留问题、惯性和当前实践。

【讨论】:

  • OP 可以自己构建 gcc,如果他愿意,默认启用这些选项。
  • 有趣! 1. 但是,当使用大型库时,这不会使链接速度变慢吗?我的意思是,如果我的库有 100 个函数,但我的程序有 1 个,那么我需要做 100 倍的外部符号解析,而不是只处理我使用的函数。我也许可以完全避免阅读一些对象和库。 2.那个协议叫什么? 3. 不能用编译器开关启用这个选项吗?
【解决方案2】:

如果您使用 gcc 和 binutils ld 构建程序,则需要将函数放在单独的部分中。它由-fdata-sections & -ffunction-sections 命令行选项存档。

与数据相同。然后,如果您不希望在可执行文件中包含死代码,则需要使用 --gc-sections ld 选项启用它。

把这一切放在一起:

$ gcc -fdata-sections -ffunction-sections -c -o a.o a.c
$ gcc -c -o b.o b.c
$ gcc -Wl,--gc-sections -o prog a.o b.o
$ /prog ; echo $?
123

如果您想在启用这些选项的情况下默认启用简单构建GCC

【讨论】:

  • 已编辑以使建议更加明确。
  • 有关“自己构建 GCC”的建议仅在您是构建 GCC 的人时才有意义。如果您是库作者,则无法控制编译器构建选项...
  • @einpoklum 实际上我经常从源代码构建工具链。 gcc 是开源的 :)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-08-20
  • 2017-02-11
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多