在Linux操作系统下,如若将一个C文件编译成ELF文件格式的可重定向目标文件格式之后,还需要进行链接生成可执行目标文件才能让机器执行,一般来说,链接的工作包括符号解析和重定向,这里就这两个过程进行讨论。

4.1 符号解析

关于符号解析的内容在3中已经有所讨论,当然那里也只是简单地进行讨论了一下强弱符号的内容,在这里进一步讨论符号解析的过程。

编译系统通常会提供将多个目标文件打包成一个单独库文件的机制,这个库文件就是静态库,一般来说,当我们最后将写好的C编译汇编成可重定向目标文件只有,最后链接生成可供操作系统执行的可执行文件时,会默认到自动库中寻找相应的目标模块,并将该模块与其链接生成可执行目标文件。
例如当我们用gcc来对main.o进行链接生成可执行文件时,如下:

  1. gcc -static -o main main.o

即使我们没有显式指明所要链接的静态库,gcc也会默认从C函数库libc.a中寻找相应的链接模块。

注:-static 表示静态链接,关于静态链接和动态链接在后续第5篇中讨论。

那在整个链接过程中,符号解析是如何完成的呢?也就是本模块中所引用的符号都是如何对应到其所指定的变量或者函数的呢?

实际上,链接器的工作顺序与我们在命令行中所指定的可重定位目标文件与静态库文件顺序是相关的,也就是说,链接器程序会按顺序从左到右扫描相应的文件,并在此期间维护三个集合,最终通过这三个集合来判断是否可重定位目标文件中的所有符号都是被定义过的。

(实际上,命令中的文件顺序也是值得注意的,不同的顺序可能导致不同的结果,有事甚至会导致错误,这里不就此细节深究,可以参考《计算机系统基础》袁春风 P181)

具体方式如下:我们将这三个集合成为E,U,D

E为最终将要合并在一起组成新的可执行目标文件的集合(链接结束之后在该集合中的所有可重定向目标文件会合并)

U为未解析符号的集合(显然,如若链接结束之后该集合不为空,说明存在未被解析的符号,即出错)

D为当前为止加入到E中所有可重定向目标文件中的定义符号的集合。

其实看到这里就大致知道了链接器是如何工作的了,首先按顺序读取每一个输入的文件,若为目标文件f,则将其加入E,再根据f中未解析符号和定义的符号对U和D进行修改(将f中定义符号加入到D中,并扫描D中查找f中引用的符号,存在没有匹配的就加入到U中)。若为库文件f,则将U中的符号与f中每个模块(打包后的每个目标文件为模块)进行匹配,若发现模块m中存在与U中相匹配的符号x,则将m加入到E中,并x从U移动到D,进一步修改U和D。

最终,通过U是否为空判断是否所有符号均存在定义,如若成功,则将E中所有目标文件合并生成可执行文件。否则就失败报错。

这样,就完成了符号解析过程。

4.2 重定位

这一工作建立在符号解析完成的基础上,前面提到的E集合中的可重定向目标文件需要合并生成可执行目标文件,重定向的工作就在这里,并确定运行时每个符号在虚拟地址空间中的地址,也就是说,在重定向工作完成之后,所有在可重定向目标文件中的没有明确其地址的函数调用以及外部变量地址在这里都会得到确定。当然,由于合并导致的本模块中定义变量的地址也会发生变化。

实际上,需要进行重定向的信息在可重定向文件的.rel.data和.rel.text节中给出,包括哪些地方需要重定向等,重定向的过程主要将多个ELF格式的可重定向目标文件中的.text节以及.data节进行合并,并通过上述的.rel.data和.rel.text节中的信息进行相应的修改.text节中指令内容,从而完成重定向工作。

这里的介绍较为简略,主要是为了对整个过程有个粗略的了解,详细的过程可以参考袁春风的《计算机系统基础》书第四章 程序的链接4.4.2节 重定位的过程