链接器会对所有的输入文件进行扫描,之后就可以确定段的大小,符号定义和符号引用的对应关系,并确定需要包含的库中的哪些模块,将这些段放置在输出地址空间的什么地方。
扫描完成后的下一步就是链接过程的核心,重定位。当我们提到重定位时,其实这个概念包含两种工作,一种是指当段不是从0地址开始的时候,需要调整程序中受到影响的地址,另一种是解析外部符号的引用,通常情况下,这两种工作时同时处理的。
链接器的第一次扫描会列出各个段的位置,并收集程序中全局符号与段的相对位置关系。一旦链接器确定了每一个段的位置,需要根据这个段的地址修改存储区中所有与之相关的地址信息,在大多数体系结构中,数据中的地址都是绝对的,嵌入到指令中的地址可能是绝对的,也可能是相对的。因此,链接器需要对他们进行修改,我们稍后会讨论这个问题。
第一遍扫描的过程中链接器会建立第5章描述的全局符号表,同时还会将引用全局符号的存储的地址解析替换为全局符号的实际地址。
7.1 硬件和软件重定位。
由于几乎所有的现代计算机都具有硬件重定位功能,可能会有人疑问为什么链接器或者加载器需要进行软件重定位,答案是,这样的设计出于运行时的性能考虑,也有一部分在优化绑定过程的时间。
硬件重定位允许操作系统位每个进程从一个约定好的固定位置开始分配独立的空间,这就使得程序的加载更加容易,并且可以避免一个地址空间中的程序错误破坏另一个地址空间中的程序,软件链接器或者加载器的重定位过程,就是将输入文件合并为一个大文件以加载到硬件重定位提供的地址空间中,然后就根本不需要修改任何加载时的地址了。
在286或者386这种可以创建几千个段的机器上,实际上有可能做到为每一个例程或者全局数据分配一个段,独立的进行软件重定位,每一个例程或数据可以从各自段的0位置开始,所有的全局引用都变成段间饮用,通过查找系统段表来处理,并在运行时进行绑定,不幸的是,x86的段查找非常慢,而且如果程序对每一个过程调用或者全局数据引用都要进行段查找的话,那速度就更慢了。 还有一个重要的因素,虽然运行时绑定会在一定程序上改善性能,但是大多数程序都没有采用,为了让程序更加可靠,链接器最终选择将所有程序文件绑定在一起并且在链接时确定符号地址,这样的设计,可以使得程序在调试时的表现相对固定,而且出货后仍然能保持一致性,当一个程序的运行环境中使用的库版本于开发人员的使用版本不一致的时候,容易引起库的二进制兼容问题,这种问题是程序错误的主要原因之一,并且难以发现,及时不考虑286风格的段的引起的访存负载,动态连接比起静态链接而言也要慢很多,当程序用不到这些功能的时候,显然没有必要引入这些代价。
7.2 链接时重定位和加载时重定位
很多系统即执行链接时重定位,也执行加载时重定位。链接器将一系列的输入文件合并成一个输出文件。并为其确定将要加载到的地址,但是,这个程序被加载时,所预设的那个加载地址可能不可用。或者已经被其他程序占用,此时加载器会将它加载到另外的地址上,并且重新定位被加载的程序以使之与实际的家在地址相对应,在一些系统上,每一个程序都按照加载到地址0的位置而被链接。而实际程序的加载地址是根据有效的存储空间而定的,因此这个程序在被加载到一个固定地址的方式来链接,并且这个地址通常一定是有效的,一般不会进行加载时重定位,除非发生该地址已经被别的程序所占用之类的异常情况。
相比链接时重定位,加载时重定位比较简单,链接时重定位,需要根据段的大小和段的位置重新定位程序中的地址项,而在加载时,整个程序会被当成一个巨大的段,加载器的重定位只需要判断原计划的加载地址和实际的加载地址之间的差异即可。