静态链接小述拼装

一个程序被分割成很多个模块之后,这些模块之间最后如何组合形成一个单一的程序可归结为模块之间如何进行通信的问题,比如模块间的函数调用,模块间的变量访问;函数访问必须知道目标函数的地址,变量访问也必须知道目标变量的地址,所以归根结底还是模块间符号的引用;模块间依靠符号来通信

符号(Symbol)是表示一个地址,这个地址可能是一段子程序的起始地址,也可以是一个变量的起始地址

一个复杂的程序,将每个源代码模块独立地编译,然后按照需要将它们拼装起来,这个组装过程就是链接(Linking);主要任务就是把每个模块之间相互引用的部分都处理好,使得各个模块之间能够正确地衔接;链接器索要做的其实跟最早期的纸带程序员调整地址本质上是一样的;从原理上说,将一些指令对其他符号地址的引用加以修正,链接过程主要包括地址和空间分配(Address and Storage Allocation),符号决议(Symbol Resolution)和重定位(Relocation)等步骤

符号决议有时候被叫做符号绑定(Symbol Binding),名称绑定(Name Binding),名称决议(Name Resolution),甚至还有叫做地址绑定(Address Binding),指令绑定(Instruction Binding)等,但意思都一样,从细节来区分还是存在一定区别的,”决议“比较倾向于静态链接,而”绑定“倾向于动态连接,范围有所差异

每个模块的源代码(比如.c)文件经过编译器编译成目标文件(.o或者.obj),目标文件和库一起链接成最终可执行程序,而常见库就是运行时库(Runtime Library),它是支持程序运行的基本函数的集合;其实库也就是一组目标文件的包,一堆最常用的code编译成目标文件然后打包存放

比如程序模块main.c中使用了另一个模块fun.c里的函数foo(),那么在main.c模块中每一处调用foo的时候都必须确切地知道foo这个函数的地址,但是由于每个模块都是单独编译的,在编译器编译main.c的时候,它并不知道foo函数的地址,所以它暂时把这些调用foo的指令的目标地址搁置,等待最后链接的时候由链接器去将这些指令的目标地址修正;如果没有链接器,就必须手工将每个调用foo的指令进行修正,填入正确的foo函数地址;当fun.c模块被重新编译,foo函数的地址可能改变时,在main.c中所有用到foo地址的指令要全部重新调整,这就超级繁琐了;使用了链接器,可以直接引用其他模块的函数和全局变量而无需知道他们的地址,因为链接器在链接的时候,会根据你所引用的符号foo,自动取相应的fun.c模块查找foo的地址,然后将main.c模块中所有引用到foo的指令重新修正,让他们的目标地址为真正的foo函数地址,这就是静态链接最基本的过程和作用

可是我有个疑问:编译main.c不知道foo函数地址的时候,将这些foo的调用指令的目标地址搁置,要等到链接的时候?这个时候还能编译通过么?好吧,看来理解的还不够深刻

搜索了下,下面是Copy的一个例子:

在链接过程中,对其他定义在目标文件中的函数调用的指令要被重新调整,对使用其他定义在其他目标文件的变量来说,也存在同样的问题;结合具体的CPU指令来了解这个过程,假如有一个全局变量var,它在目标文件A里面,目标文件B里面要访问这个全局变量,比如在目标文件B里面有这条指令:movl        $0x2a,    var

        mov指令码                                   源常量

            C7 05           00 00 00 00       2a 00 00 00

                                   目标地址

这条指令将变量var赋值0x2a,相当于var = 42的意思,然后编译目标文件B

由于在编译目标文件B的时候,编译器并不知道变量var的目标地址,所以编译器在没法确定地址的情况下,将这条mov指令的目标地址置为0,等待链接器在将目标文件A和B连接起来的时候再将其修正;假如A和B链接后,变量var的地址确定下来是0x1000,那么链接器将会把这个指令的目标地址部分修改成0x10000,这个地址修正的过程也被叫做重定位(Relocation),每个要被修改的地方叫一个重定位入口(Relocation Entry),重定位所做的就是给程序中每个这样的绝对地址引用的位置“打补丁”,指他们指向正确的地址

好吧,看了这例子说明,就明白了!!

发表回复