为什么 ld 不能忽略未使用的未解析符号?

Why can't ld ignore an unused unresolved symbol?

考虑以下源文件:

a.c:

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。

为什么会这样?我的意思是,不需要任何复杂的优化就知道 bar() 中并不真正需要 baz() - 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

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

extern int baz();

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

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

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

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

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

如果您使用 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