链接器不是魔法程序
很令人沮丧地,许多程序员经常(在写这篇文章之前我刚刚遇到过)对编译型的语言中从源代码到静态的可执行程序的过程的看法是:
1. 编写源代码
2. 编译源代码,生成目标文件
3. 一些魔法发生
4. 运行可执行程序
第3步,当然就是链接了。为什么我说得这么夸张?我做技术支持已经数十年了,一再会遇到这样的问题:
1. 链接器说def被定义了不只一次。
2. 链接器说abc是一个未知符号(Unresolved Symbol)。
3. 为什么可执行文件这么大?
“我现在该怎么办?”之后,经常就是一些“似乎是”、“也许”的混杂,满是疑惑的气氛。正是“似乎是”、“也许”显示了链接经常被看成是魔法过程,可能只有巫师和术士才能理解了。编译过程则不会让程序出现这些短语,表明程序员们一般都知道编译器是怎么工作的,至少知道它们做了什么。
链接器其实是一个非常简单、易懂、直白的程序。它做的所有的事情就是将目标文件中的代码和数据区集中到一起,连接符号的引用和定义,取出库中未定义的符号,生成一个可执行文件。就是这些。没有魔法!没有异术!编写一个链接器的主要冗长工作往往在于解码和生成太过于复杂的文件格式,但这不会改变链接器的本质。
我们看一下链接器说“def被定义了不只一次”。很多编程语言,如C,C++和D,里面都有声明和定义。声明经常是在头文件中,像这样:
extern int iii;
它产生了一个符号iii的外部引用。另一方面,定义实际上设定的符号的存储空间,一般出现在实现文件,像这样:
int iii = 3;
每个符号可以有多少个定义呢?正如电影《高地人》中的,只能有一个。于是,iii的定义出现在不只一个实现文件中会怎么样呢?
// File a.c
int iii = 3;
// File b.c
double iii(int x) { return 3.7; }
链接器会抱怨说iii被重定义了。
不是只能是一个定义,而是必须一个。如果iii只是以声明形式出现而没有定义过,那么链接器会抱怨说iii是未定义的符号。
要决定可执行文件为什么是这个大小,看一下链接器选择性生成的map文件。map文件只是可执行文件中的所有符号及其地址的清单。这可以告诉你哪些模块从库中被链接了,以及各个模块的大小,现在你就可以看出文件是因为什么而这么大了。经常也会有一些你不太清楚为什么会链接的库模块;要弄明白,可以从库中暂时地移除可疑的模块,再重新链接。生成的未定义的符号错误可以显示是什么在引用这个模块。
尽管你获得的链接器的消息看起来不是特别明显,但链接器里没有什么魔法。机制是简单直白的;需要弄清楚的只是细节。