请考虑以下源文件:
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.
为什么会发生这种情况呢?我的意思是,不需要任何复杂的优化就可以知道foo()
中并不真正需要baz()
- ld在某个时候自然会注意到这一点-例如,当完成对foo()
的遍历而没有注意到使用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
2条答案
按热度按时间ou6hu8tu1#
如果你使用
gcc
和binutilsld
来编译你的程序,你需要把函数放在不同的部分。它是由-fdata-sections
和-ffunction-sections
命令行选项存档的。如果你不想在你的可执行文件中包含死代码,你需要使用
--gc-sections
ld
选项来启用它。把这些放在一起:
如果您想默认启用它,请简单构建
GCC
并启用这些选项。idv4meu82#
在一个“普通”的传统编译代码中:
编译器创建一个包含两个例程的代码沿着符号
foo
和bar
的定义和对baz
的引用的对象模块。没有任何东西告诉链接器属于foo
的代码在哪里开始和结束,其中属于X1 M4 N1 X的代码开始和结束,或者甚至任何给定的代码段-或对象模块中的任何给定字节-仅属于X1 M5 N1 X或X1 M6 N1 X之一。如果我用汇编语言编写并汇编成一个对象模块,我就可以在foo
中包含跳到bar
的代码(只使用汇编程序计算的硬编码偏移量,而不显示在链接器可见的任何符号中),反之亦然。因此,连接器无法知道
foo
和bar
可以分开。后来,编译器创建了一个协议,以保持函数分离,并在对象模块中提供足够的信息,以便链接器可以确定它们在哪里分离,并告诉链接器可以分离函数。当启用了该选项时,链接器可以在程序中包含
foo
,而不包含bar
。这个特性还不是工具中的默认特性,这是各种构建系统和项目中的遗留问题、惯性和当前实践。