编译与链接(二)——静态链接

 

         连接通过编译过程,一个源文件(.c)就生成了一个对应的目标文件(.o)。一个工程,不可能是一个文件组成,从几十个到几百个,大的项目工程有成千上完个文件,这些文件通过编译,只是从源文件变成了目标文件,但是这些文件不能单独运行,各个文件(模块)之间存在一定的关系,要使工程正常工作,各个目标文件和库必须连接在一起,形成一个最终的可执行的文件。下面就详细介绍链接工作的过程。链接过程简单描述链接过程分为静态链接和动态链接,动态链接以后将详细介绍,本文主要介绍静态链接过程。从源文件到可执行文件的过程如图1表示:

把各个目标文件看成一个个的积木,链接过程就是拼积木的过程。模块之间如何组合的问题可以归结为模块之间如何通信的问题,最常见的通信方式有两种:一种是模块之间的函数调用,另一种是模块之间的变量访问。链接的主要内容就是把各个模块之间相互引用的部分都处理好,使得各个模块之间能够正确地衔接。从原理上讲,链接的工作无非就是把一些指令对其他符号地址的引用加以修正。符号是用来表示一个地址的,这个地址可以是一段子程序(函数)的起始地址,也可以是一个变量的起始地址,也就是函数名和变量都可以看做一个符号。

除了目标文件,链接过程还有库文件,最常见的库就是运行时库,它是支持运行程序的基本函数集合。链接过程主要包括了地址和空间分配,符号决议和重定位。

地址和空间分配过程实际在编译的最后一步:从汇编文件编译成目标文件过程已经完成了一部分,各个目标文件模块内部的变量、函数以本文件为基准分配地址,即文件内的偏移地址。除了引用的外部符号,模块内的符号都有明确的地址。编译器也会根据变量的类型为其分配相应的空间。链接过程的地址和空间分配,可以看做是从目标文件到可执行文件映射过程,目标文件的地址在可执行文件中应该是怎样的地址;目标文件没有确定地址的外部符号在可执行文件中确定了下来。

符号决议也叫符号绑定、名称绑定、名称决议,甚至还有叫做地址绑定、指令绑定的。决议和绑定有细微的差别,决议倾向于静态链接,绑定更倾向于动态链接。

重定位的过程,主要是对外部符号的地址进行重定位,各个目标文件和库文件的外部符号在链接之前地址没有确定,编译过程把外部符号的地址暂时搁置,链接过程才确定下外部符号的地址,并在执行文件中保存。

以简单的例子讲述链接过程。例如两个模块main.cfunc.cmain.c中要使用func.c中的函数foo(),main.c模块中每一处调用foo的时候都要知道foo这个符号的地址,但是模块单独编译,编译main.c的时候不知道foo的地址,编译器暂时搁置,到链接时再去确定。连接器在链接的时候会根据所引用的符号foo,自动去相应的func.o模块中查找foo的地址,然后将main.o模块中所有引用到foo的指令重新修正,是得获取真正的foo函数的地址。

链接过程详细描述。

上面简单描述了链接过程,真正要了解链接过程,需要知道目标文件的格式——目标文件到底有哪些东西。

目标文件从结构上讲,它已经是编译后的可执行文件格式,是按照可执行文件格式(ELF)存储的。ELF文件的开头是一个“文件头”,文件头描述了整个文件的属性,包括1 是否可执行,2是静态链接还是动态链接,3入口地址,4目标硬件,5目标操作系统和6段表偏移等信息。段表描述了文件中各个段在文件中的偏移位置及段的属性。文件头后面就是各个段的内容。图2更直观的描述ELF文件的格式。

对于链接器来说,整个链接过程中,就是将几个输入目标文件加工合并成一个输出文件。上面提到的链接有三个步骤:空间与地址分配、符号决议和重定位。

空间地址分配可以分为两种,按序叠加和相似段合并。按序叠加十分简单,就是将所有的目标文件按照顺序依次叠加到一起成为一个输出文件。输出文件的段数等于所有文件段数的总和。这种方法得到的最后输出文件可能有成千上万个段,每个段由于地址和空间对齐的需要,造成很多内存碎片,十分浪费空间。如图3表示:

相似段合并是将性质相同的段合并到一起,这种方法可以有效的避免内存碎片的问题,节省空间,如图4所示。地址和空间分配,包含第一输出的可执行文件中的空间,第二是装载后的虚拟地址中的虚拟地址空间。链接过程一般采用两步链接的方法。

第一步空间与地址分配,扫描所有的输入文件,获得它们的各个段的长度、属性和位置,并将输入目标文件中的符号表中所有的符号定义和符号引用收集起来,统一放到一个全局符号表。链接器将能够获得所有输入文件的段长度,并且将它们合并,计算出输出文件中各个段合并后的长度与位置,并建立映射关系。

第二步 符号解析与重定位,使用第一步收集到的所有信息,读取输入文件中段的数据、重定位信息,并且进行符号解析与重定位 、调整代码中的地址等。该步骤是链接过程的核心,特别是重定位过程。

上面主要讲述了静态链接过程。动态链接以后的文章介绍。