链接笔记


经过这些扫描,语法分析,语义分析,源代码优化,代码生成和目标代码优化,编译器忙活了这么多个步骤后,源代码终于被编译成了目标代码,但这个目标代码有一个问题:index和array的地址还没有确定。如果我们把目标代码使用汇编器编译成真正能够在机器上执行的指令,那么index和array的地址应该从哪得到?如果index和array定义在跟上面的源代码同一个编译单元里面,那么编译器可以为index和array分配空间,确定他们的地址,那如果是定义在其他的程序模块中呢“?
事实上,定义其他模型的全局变量和函数在最终运行时的绝对地址都要在最终链接的时候才能确定。所以现代的编译器可以将一个源代码编译成一个为链接的目标文件,然后由链接器最终将这些目标文件链接起来形成可执行文件。让我们走进链接的世界。

链接器历史

以前人们编写程序时,将所有源代码都写在同一个文件中,发展到后来一个程序源代码的文件长达数百万行,以至于人类已经没有能力维护这个程序了,于是人们开始寻找新的方法,一场新的软件开发革命爆发。
计算机的程序开发并非从一开始就有着这么复杂的自动化编译,链接过程,原始的链接概念远在高级程序语言发明之前就已经存在了,在最开始的时候,程序员先把一个程序在纸上写好,当然当时没有很高级的语言,用的都是机器语,甚至连汇编语言都没有,当程序须要被运行时,程序员人工将他写的程序写入到存储设备下,最原始的存储设备之一就是纸带,即在纸带上打相应的孔。
假设有一个计算机,它的每条指令是1个字节,也就是8位,我们假设有一种跳转指令,它的高4位是0001,表示这是一条跳转指令,低4位存放的是跳转目的地的绝对地址。至于0和1怎么映射到纸带上,比如我们可以规定纸带每行有8个孔位,每个孔位代表一位,穿孔表示0,未穿孔表示1。
现在问题来了,程序并不是一写好就永远不变化的,它可能会经常被修改,比如我们在第1条指令之后,第5条指令之前插入一条或多条指令,那么第5条指令及后面的指令的位置就会相应的往后移动,原先第一条指令的低4位的数字将需要相应的调整。在这个过程中,程序员需要人工重新计算每个子程序或跳转的目标地址,当程序修改的时候,这些位置都要重新计算,十分繁琐又耗时,并且很容易出错,这种重新计算各个目标的地址过程被叫做重定位。
如果我们有多条纸带的程序,这些程序之间可能会有类似的跨纸带之间的跳转,这种程序经常经常修改导致目标地址变化在程序拥有多个模块的时候更为严重,人工绑定进行指令的修改以确保所有的跳转目标地址都正确,在程序规模越来越大以后变得越来越复杂和繁琐。
于是,先驱者发明了汇编语言,汇编语言使用接近人类的各种符号和标记来帮助记忆,比如采用两个或三个字母的缩写,记住"jmp"比记住0001XXXX是跳转指令容易的多,汇编语言还可以使用符号来标记位置,比如一个符号“divide”表示一个除法子程序的起始地址,比记住从某个位置开始的第几条指令是除法指令方便的多。比如前面纸带程序中,我们把刚开始第5条指令开始的子程序命名为“foo”,那么第一条指令的汇编就是:
jmp foo
当然人们可以使用这种符号命名子程序或跳转目标后,不管这个“foo”之前插入或者减少了多少指令导致"foo"目标指令发生了什么变化,汇编器在每次汇编程序的时候会重新计算"foo"这个符号的地址,然后把所有引用到“foo”的指令修正到这个正确的地址。整个过程不需要人工参与,程序员终于摆脱了这种低级的繁琐的调整地址的工作。符号这个概念随着汇编语言的普及迅速被使用,它用来表示一个地址,这个地址可能是一段子程序(后来发展成函数)的起始地址,也可以是一个变量的起始地址。
有了汇编语言后,生产力大大地提高了,软件的规模也开始庞大起来,这是程序的代码量也快速的膨胀,导致人们要开始考虑将不同功能的代码以一定的方式组织起来,使得更加容易阅读和理解,以便日后修改和重复使用,于是,人们开始将代码按照功能或者性质划分,分别形成不同的功能模块,比如在C语言中,最小的单位是变量和函数,若干个变量和函数组成一个模块,存放在一个".c"的源代码文件中,然后这些源代码文件按照目录结构来组织。
在现代软件开发过程中,软件的规模往往都很大,如果都放在一个模块肯定无法想象,所以现代的大型软件往往拥有成千上万个模块,这些模块之间相互依赖又相互独立,这种按照层次化及模型化存储和组织源代码有很多好处,比如代码更容易阅读,理解,重用,每个模块可以单独开发,编译,测试,改变部分程序不需要编译整个过程。
在一个程序被分割成多个模块后,这些模块之间最后如何组成形成一个单一的程序是须解决的问题,模块之间如何组合的问题可以归结为模块之间如何通信的问题,最常见的属于静态语言的C/C++模块之间通信有两种方法,一种是模块间的函数调用,另外一种是模块间的变量访问,函数访问须知道目标函数的地址,变量访问也须知道目标变量的地址,所以这两种方式都可以归结于一种方式,那就是模块间符号的引用,模块间依靠符号来通信类似于拼图版,定义符号的模板多出一块区域,引用该符号的模块刚好少了那一块区域,两者一拼接刚好完美组合,这个模块的拼接过程就叫做:链接。
这种基于符号的模块化的一个直接结果是链接过程在整个程序开发中变得十分重要和突出。

模块拼接-静态链接

程序设计的模块化是人们一直在追求的目标,因为当一个系统非常复杂的时候,我们不得不将一个复杂的系统逐步分割成小的系统以达到各个突破的目的。一个复杂的软件也如此,人们把每个源代码模块独立地编译,然后按照须要将它们“组装”起来,这个组装模块的过程就是链接,链接的主要内容就是各个模块之间相互引用的部分都处理好,使得各个模块之间能够正确地衔接,从原理上讲,它的工作无非就是把一些指令对其他符号地址的引用加以修改,链接过程主要包括了地址和空间分配,符号决议和重定位等这些步骤。
在这里插入图片描述
最基本的静态链接如图,每个模块的源代码文件(如.c)文件经过编译器编译成目标文件(Object File,一般扩展名为.o或.obj,目标文件和库一起链接形成最终可执行文件。而最常见的库就是运行时库,它是支持程序运行的基本函数的集合。库其实是一组目标文件的包,就是一些常用的代码编译成目标文件后打包存放。
我们认为对于Object文件没有一个很合适的中文名称,把它叫做中间目标文件比较合适,简称为目标文件,很多时候我们又把目标文件称为模块。
现代的编译和链接过程也并非想象中的那么复杂,它还是一个比较容易理解的概念。比如我们在程序模块main.c中使用另外一个模块func.c中的函数foo()。我们在main.c模块中每一处调用foo的时候都必须确切知道foo这个函数的地址,但是由于每个模块都是单独编译的,在编译器编译main.c的时候它并不知道foo函数的地址,所以它暂时把这些调用foo的指令的目标搁置,等待最后链接的时候由链接器去将这些指令的目标地址修正。当func.c模块被重新编译,foo函数的地址有可能改变时,我们在main.c中所有使用到foo的地址的指令将要全部重新调整。使用链接器后,可以直接引用其他模板的函数和全局变量而不需要知道他们的地址,因为链接器在链接的时候,会根据你所引用的符号foo,自动去相应的func.c模块查找foo的地址,然后将main.c模块中所有引用到foo的指令重新修改,让它们的目标地址为真正的foo函数的地址,这就是静态链表的最基本的过程。
在链接过程中,对其他定义在目标文件中的函数调用的指令须要被重新调整,对使用其他定义在其他目标文件的变量来说,也存在同样的问题,让我们结合具体的CPU指令来了解这个过程,假设我们有个全局变量叫做var,它在目标文件A里面,我们在目标文件B里面要访问这个全局变量,比如我们在目标B里面有这么一条指令:
movl $0x2a,var
这条指令就是给这个var变量赋值0x2a,相当于C语言里面的语句var=42.然后我们编译目标文件B,得到这条指令机器码,如图
在这里插入图片描述
由于在编译目标文件B的时候,编译器并不知道变量var的目标地址,所以编译器在没打法确定地址的情况下,将这条mov指令的目标地址置为0,等待链接器在将文件A和B链接起来的时候再将其修正,我们假设A和B链接后,变量var的地址确定下来为0x1000,那么链接器将会把这个指令的目标地址修改为0x100000.这个地址修正的过程也被叫做重定位,每个要被修改的地方叫一个重定位入口。重定位所做的就是给程序中每个这样的绝对地址引用的位置“打补丁”,使它们指向正确的地址。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值