为什么 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; }
编译器创建一个目标模块,其中包含两个例程的代码以及符号 foo
和 bar
的定义以及对 baz
的引用。没有什么可以告诉链接器属于 foo
的代码在哪里开始和结束,属于 bar
的代码在哪里开始和结束,甚至任何给定的代码段或对象中的任何给定字节模块——仅属于 foo
或 bar
之一。如果我用汇编编写并组装成一个目标模块,我可以在 foo
中包含跳转到 bar
的代码(仅使用由汇编器计算的 hard-coded 偏移量,并且不会在任何链接器可见的符号)或 vice-versa.
所以链接器无法知道foo
和bar
可以分开
后来,为编译器创建了一个协议,以保持函数分离,并在目标模块中提供足够的信息,链接器可以确定它们在哪里分离,并告诉链接器可以分离函数。启用该选项后,链接器可以在程序中包含 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
。
考虑以下源文件:
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; }
编译器创建一个目标模块,其中包含两个例程的代码以及符号 foo
和 bar
的定义以及对 baz
的引用。没有什么可以告诉链接器属于 foo
的代码在哪里开始和结束,属于 bar
的代码在哪里开始和结束,甚至任何给定的代码段或对象中的任何给定字节模块——仅属于 foo
或 bar
之一。如果我用汇编编写并组装成一个目标模块,我可以在 foo
中包含跳转到 bar
的代码(仅使用由汇编器计算的 hard-coded 偏移量,并且不会在任何链接器可见的符号)或 vice-versa.
所以链接器无法知道foo
和bar
可以分开
后来,为编译器创建了一个协议,以保持函数分离,并在目标模块中提供足够的信息,链接器可以确定它们在哪里分离,并告诉链接器可以分离函数。启用该选项后,链接器可以在程序中包含 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
。