The Linker Is not a Magical Program
DEPRESSINGLY OFTEN(在我写这篇文章之前又发生了),很多程序员的观念中,将源码编译到静态链接的可执行文件的主要过程是:
- 编辑源码
- 将源码编译为目标文件(obj文件)
- 然后发生了一些神奇的事情
- 可以跑了
第3步,即链接这一步。为毛我要将其说得如此荒诞?想当年,我还是个技术支持,就一次又一次地关注这些内容:
- 链接器说
def
表示多次定义; - 链接器说
abc
表示某个符号未解析; - 为何最终执行文件会那么大?
紧接着会告诉你“我接下来做什么?”,它们通常都融合了“seems to(貌似)”或者“somehow(某种程度)”的短句,一看就自带光环属性。所谓的“貌似”和“某种程度”会让人觉得链接处理是一个非常神奇的过程,只有巫师和术士才能理解。编译过程不会详细说明这些短句类型,这就意味着程序员要自己理解编译器是如何工作的,或者至少明白它们在做什么。
链接器又笨、又乏味、又简单粗暴的程序。它所做的事情就是把目标文件的代码区和数据区串联起来,把引用连接到它们定义的地方,把未解析的符号推到库之外,然后输出一个可执行文件。仅此而已,没有咒语!没有魔法!链接器繁琐的地方是需要解码或生成极其复杂的文件格式,当然这并没有改变链接器的本质。
所以,我们才说链接器提示def
就是告诉你多次定义。很多编程语言,如C、C++和D,都包含声明和定义。申明通常放在头文件里,就像这样:
1 | extern int iii; |
用于生成iii
符号给外部调用。此外,定义通常是为了给符号留出空间,它通常在实现文件中,就像这样:
1 | int iii = 3; |
那么每个符号可以被定义多少次呢?就像电影Highlander一样,它们只允许一次。所以,如果iii
定义在多个实现文件中出现?
1 | // file a.c |
链接器就会警告说iii被多次定义。
不仅是只能定义一个,也必须被定义一个。如果iii仅被声明过,却从来没有定义,那编译器同样会警告说iii是一个未定义的符号。
要搞清楚可执行文件为什么体积大,就要看链接器选了哪些映射文件生成。在可执行文件中映射文件只是一堆符号表,以及它们对应的地址。这能告诉你哪些模块会从库中被链接,以及每个模块的大小。现在你就能看到肥胖的根源了。很多时候,你根本不清楚为什么要链接库中的某个模块,为了搞清楚这点,暂时将模块从库中删除,重新链接,然后“未定义符号”的错误会告诉你到底谁在引用这些模块。
尽管链接器弹出的提示消息并不那么直观,但它真的没什么神奇的。它的机制很简单,你必须要掌握每种情况下的细节。