前言
欢迎持续关注专栏:juejin.cn/column/7265...
上一篇文章中,我们介绍了复杂的编译过程,其中最后就抛出了一个问题,在编译完的汇编语言中,我们例子中的index
和array
的地址还不知道,这就涉及到了链接过程。这里来简单介绍一些关于链接的概念。
正文
C/C++程序在经过预编译和编译后,还需要汇编和链接过程,这里为了更好理解为什么要汇编和链接,我们从历史起源来看。
早期的程序
最开始的时候,那时候还没有磁盘、内存这种存储介质出现,人们把代码使用打孔纸带的方式存储,然后交由计算机处理,如下图:
假如纸带有如下程序:
erlang
地址 程序
0 0001 0100
1 ...
2 ...
3 ...
4 1000 0111
5 ...
6 ...
假如0001
表示是跳转指令,它要跳转的指令的地址是0100
,即地址为4(第5行)的指令,这里纸带使用打孔和未打孔来表示0和1,看起来没问题,思路是正确的。
但是问题来了,程序不是一成不变的,假如在第1行和第5行之间又增减了一部分指令,这时跳转指令所执行的地址0100
就需要改变了,而早期计算机处理这种情况的办法是人工调整这个地址,这个过程非常复杂又容易出错,而这个重新计算各个目标程序的地址以及跳转地址的过程就叫做重定位(Relocation)。
如果我们的程序越来越复杂,纸带也逐渐多了起来,每次人工计算目标程序的地址是无法容忍的,所以先驱者发明了汇编语言,它相比于机器语言是个很大的进步。
汇编语言使用接近人类的各种符号和标记来帮助记忆,比如前面的二进制指令0001 0100
,我们可以使用jump
来表示跳转指令,这就容易记住得多了;同时对于需要跳转的程序的地址,比如这里的0100
,我们一样可以使用一个符号来命名,比如命名为foo
,这时我们就可以使用汇编指令jump foo
来替代0001 0100
,汇编指令在可读性有了很大的进步。
当人们使用这种以符号命名的子程序时,当foo
前面插入或者减少了指令导致foo
地址发生变化时,汇编器可以在每次汇编程序时重新计算foo
这个符号的地址 ,从而把所有引用到foo
程序的指令都能修正到正确的地址。
这里也引入了一个重要概念,就是符号(Symbol) ,它用来表示一个地址,既可以是一段子程序的起始地址,也可以是一个变量的地址。
链接起源
既然我们了解了汇编语言产生的原因,以及符号的概念,现在来思考另一个问题。随着发展,软件的规模会越来越大,这时就需要考虑将不同功能的代码以一定的方式组织起来 ,比如C语言中,最小的单位是变量和函数,我们把若干个变量和函数都保存到一个.c
文件中,这就是一个模块。
这种做法很好,每个模块之间可以互相依赖又相对独立,好处也很多,比如容易阅读、理解和重用 ,每个模块还可以单独开发、编译、测试等。
但是将一个程序分割为多个模块后,如何把模块组合起来成为一个可执行程序就是一个必须要解决的问题。模块之间如何组合的问题,其实也就是模块之间如何通信的问题,比如C语言模块之间有2种通信方式:一种是模块之间的函数调用,另一种是模块之间的变量访问。
函数访问必须知道目标函数的地址,变量访问也需要知道目标变量的地址,而函数和变量在前面我们知道都可以用符号来表示 ,所以这2个问题就归纳为一个问题:那就是模块之间符号的引用。
模块之间依靠符号来通信就类似与拼图版 ,定义符号的模块多出一块区域,而引用该符号的模块刚好少那一块区域,而模块的拼接过程就是链接(Linking)。
静态链接
链接器所做的事其实就和最开始说的"程序员人工调整纸带地址"没有本质区别,它主要工作就是调整符号的地址 。链接过程主要包括了地址和空间分配(Address and Storage Allocation)、符号决议(Symbol Resolution)和重定位(Relocation)等这些步骤。
其中"决议"更倾向于静态链接,"绑定"更倾向于动态链接,所使用的范围是不一样的。
最基本的静态链接就是将多个源代码(如.c)文件编译后的目标文件(一般为.o)和库(Library)一起链接成可执行文件 。这里的库最常见的就是运行时库(Runtime Library),它是支持程序运行的基本函数集合 。库其实是一组目标文件的包,就是一些最常用的代码编译成目标文件后打包存放。
其实链接的过程并不复杂,比如我们在程序模块main.c
中使用了另一个模块fun.c
中的函数foo()
,我们在mian.c
模块中每一处调用foo
的时候都必须确切知道foo
函数的地址。但是由于每个模块都是单独编译的,在编译main.c
的时候,我们并不知道foo
函数的地址,所以它暂时会把这些调用foo
的指令的目标地址搁置,等待最后链接的时候再由链接器去将这些目标地址进行修正。
同时我们应该知道一点,由于我们是使用符号来表示一个函数地址,所以即使当fun.c
重新编译了,这时只需要重新链接即可,因为函数地址所表示的符号foo
没有变化。这就是静态链接的最基本过程和作用。
我们再以汇编指令举例,假如有一句C代码是var = 42
,其对应的汇编指令是:
javascript
movl $0x2a, var
这里我们知道就是把var
赋值为0x2a
,假如var
变量定义在其他模块,这时生成的机器码如下:
由于编译器并不知道变量var
的目标地址,所以编译器在没法确定地址的情况下,会将mov
指令的目标地址设置为0,等待链接器将目标文件链接进来的时候,再进行对其修正。
假设链接后,变量var
的地址被确认为0x12111
,那么链接器会将这个指令的目标地址修改为相应的值。这个地址修正的过程被叫做重定位(Relocation),每个需要被修正的地方叫一个重定位入口(Relocation Entry),而重定位所做的事就是给程序中每个不确定的目标地址"打补丁",使其指向正确的地址。
总结
本章简单介绍了链接的意义和概念,总结如下:
- 早期使用纸带编程,不方便阅读以及调整纸带位置都是非常困难且复杂的,所以发明了汇编语言,使用类似自然语言来表示各个指令。
- 同时使用符号来表示地址,包括函数和变量地址,这样便于阅读和理解。
- 随着程序变得复杂,程序被分模块编译,模块之间通过符号引用进行通信,这个过程就像拼图。
- 对于使用其他模块的变量和函数,在编译时,把这些地址搁置暂且为0,等待链接器对其进行修正,这个过程就是重定位。