一、可重定位目标文件
图7-3 展示了一个典型的ELF可重定位目标文件的格式。ELF头(ELF header)以一个16 字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。
ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括 ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如 X86-64)、节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量。
二、符号和符号表
每个可重定位目标模块m都有一个符号表,它包含m定义和引用的符号的信息。在链接器的上下文中,有三种不同的符号:
(1)由模块m定义并能被其他模块引用的全局符号。全局链接器符号对应于非静态的 C 函数和全局变量。
(2)由其他模块定义并被模块引用的全局符号。这些符号称为外部符号,对应于在其他模块中定义的非静态C函数和全局变量。
(3)只被模块m定义和引用的局部符号。它们对应于带static属性的C函数和全局变量。这些符号在模块w中任何位置都可见,但是不能被其他模块引用。
符号表是由汇编器构造的,使用编译器输出到汇编语言.s文件中的符号。symtab节 中包含ELF符号表。这张符号表包含一个条目的数组。图7-4展示了每个条目的格式。
name 是字符串表中的字节偏移,指向符号的以null结尾的字符串名字。value是符号的地址。size是目标的大小(以字节为单位)。 type 通常要么是数据,要么是函数。
三、符号解析
链接器解析符号引用的方法是将每个引用与它输入的可重定位目标文件的符号表中的一个确定的符号定义关联起来。对那些和引用定义在相同模块中的局部符号的引用,符号解析是非常简单明了的。编译器只允许每个模块中每个局部符号有一个定义。
对全局符号的符号解析很棘手,还因为多个目标文件可能会定义相同名字的全局符号。在这种情况中,链接器必须要么标志一个错误,要么以某种方法选出一个定义并拋弃其他定义。
1.链接器如何解析多重定义的全局符号
链接器的输人是一组可重定位目标模块。每个模块定义一组符号,有些是局部的,有些是全局的。如果多个模块定义同名的全局符号,会发生什么呢?
根据强弱符号的定义,Linux链接器使用下面的规则来处理多重定义的符号名:
规则1: 不允许有多个同名的强符号。
规则2: 如果有一个强符号和多个弱符号同名,那么选择强符号。
规则3: 如果有多个弱符号同名,那么从这些弱符号中任意选择一个。
2.与静态库链接
在之前都是假设链接器读取一组可重定位目标文件,并把它们链接起来,形成一个输出的可执行文件。但实际上,所有的编译系统都提供一种机制,将所有相关的目标模块打包成为一个单独的文件,称为静态库(static library), 它可以用做链接器的输人。当链接器构造一个输出的可执行文件时,它只复制静态库里被应用程序引用的目标模块。
如果不使用静态库,那么一个很大的缺点是系统中每个可执行文件现在都包含着一份标 函数。集合的完全副本,这对磁盘空间是很大的浪费。另一个大的缺点是,对任何标准函数的任何改变,无论多么小的改变,都要求库的开发人员重新编译整个源文件,这是一个非常耗时的操作,使得标准函数的开发和维护变得很复杂。
在Linux 系统中,静态库以一种称为存档(archive)的特殊文件格式存放在磁盘中。存档文件是一组连接起来的可重定位目标文件的集合,有一个头部用来描述每个成员目标文件的大小和位置。存档文件名由后缀.a标识。
3.链接器如何使用静态库来解析引用
在符号解析阶段,链接器从左到右按照它们在编译器驱动程序命令行上出现的顺序来扫描可重定位目标文件和存档文件。(驱动程序自动将命令行中所有的.c文件翻译为.o文件。)在这次扫描中,链接器维护一个可重定位目标文件的集合E(这个集合中的文件会被合并起来形成可执行文件),一个未解析的符号(即引用了 但是尚未定义的符号)集合U,以及一个在前面输人文件中已定义的符号集合D。初始时,E、U 和D均为空。
对于命令行上的每个输人文件f,链接器会判断f 是一个目标文件还是一个存档文件。如果f 是一个目标文件,那么链接器把f 添加到E,修改U和D来反映f 中的符号定义和引用,并继续下一个输入文件。
如果f 是一个存档文件,那么链接器就尝试匹配U中未解析的符号和由存档文件成员定义的符号。如果某个存档文件成员m,定义了一个符号来解析U 中的一个引用,那么就将m 加到E 中,并且链接器修改U 和D 来反映m 中的符号定义和引用。对存档文件中所有的成员目标文件都依次进行这个过程,直到U 和D 都不再发生变化。此时,任何不 包含在E中的成员目标文件都简单地被丢弃,而链接器将继续处理下一个输入文件。
如果当链接器完成对命令行上输人文件的扫描后,U是非空的,那么链接器就会输出一 个错误并终止。否则,它会合并和重定位£中的目标文件,构建输出的可执行文件。
不幸的是,这种算法会导致一些令人困扰的链接时错误,因为命令行上的库和目标文件的顺序非常重要。在命令行中,如果定义一个符号的库出现在引用这个符号的目标文件之前,那么引用就不能被解析,链接会失败。