参考资料:程序员的自我修养–链接、装载与库
通常使用的开发环境都是使用IDE,IDE将编译和链接的过程一步完成,这种编译和链接合并到一起的过程称为构建。就算使用gcc hello.c命令编译也包含了十分复杂的过程。
打个比方说有一个程序hello.c
#include<stdio.h>
int main()
{
printf("hello world\n");
return 0;
}
在linux下我们编译使用
gcc hello.c
./a.out
事实上,上述的过程分解成为4个步骤,分别是预处理(Preprossing)、编译(Compilation)、汇编(Assembly)、链接(Linking),gcc编译过程分解图如下:
1.预处理
首先是源代码文件hello.c和相关的头文件,如stdio.h等被预编译成一个.i文件,主要处理规则如下:
使用如下命令:
gcc -E hello.c -o hello.i
2.编译
编译器就是将高级语言翻译成机器语言的一个工具,编译过程就是将预处理完的文件进行一系列的词法分析、语法分析、语义分析以及优化后生成的汇编代码。编译过程的命令:
gcc -S hello.i -o hello.s
gcc -S hello.c -o hello.s
实际上gcc这个命令只是这些后台程序的包装,它会根据不同的参数要求去调用预编译编译程序cc1、汇编器as 、链接器ld。
编译过程涉及编译原理,我没咋学过,先跳过
3.汇编
汇编器是将汇编代码转换成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。
as hello.s -o hello.o
gcc -c hello.s -o hello.o
gcc -c hello.c -o hello.o
上面三条命令都可以
4. 链接
链接就是将一些静态库链接到一起,生成a.out这个目标文件
关于链接这里我们需要了解相关的概念
- 重定位 。下面是我看见的一段比较好理解的解释。
- 符号
汇编语言会使用接近人类语言的各种符号和标记来帮助记忆,比如说使用jmp表示跳转,比记住跳转的二进制数要容易的多
4.1静态链接
链接过程主要包括了地址和空间分配 、符号决议 和重定位 等步骤。
文件经过编译器生成目标文件。目标文件和库一起链接生成最终的可执行文件。
假设我们有两个目标文件即目标文件A和目标文件B
我们有个全局变量叫做var在目标文件A里面,但是我们在目标文件B里面有这么一条指令movl $0x2a,var
这条指令就是给这个var变量赋值0x2a,然后得到编译目标文件B。由于在编译目标文件B的时候,编译器并不知道变量var的目标地址,所以编译器在没法确定地址的情况下,将这条mov指令的目标地址置为0,等待链接器将目标文件A 和目标文件B连接起来再将其修正。我们假设A和B链接后,变量var的地址确定为0x10000,那么链接器将会把这个指令的目标地址部分修改成0x10000。这个地址修正的过程也被叫做重定位 ,每个要被修正的地方叫一个重定位入口 。重定位做的就是给程序中每个这样的绝对地址引用的位置"打补丁",使得他们指向正确的地址。
空间与地址分配
我们知道,可执行文件中的代码段和数据段都是由输入的目标文件中合并来的,那么在链接过程中有个问题就出现了:对于多个输入目标文件,链接器如何将它们的各个段合并到输出文件?或者说输出文件中的空间如何分配给输入文件???
1.按序叠加, 就是将各个目标文件依次合并,但是会造成内存空间的浪费
2.相似段合并 ,也就是将所有输入文件的.text段合并到输出文件的.text段,将.bss段合并到输出文件的.bss段,以此类推。
现在的链接器空间分配的策略都采用相似段合并的方法,使用这种方法的链接器一般都采用一种叫做两步链接的方法。
第一步 空间与地址分配 扫描所有的输入目标文件,并且获得他们的各个段的长度、属性和位置,并且将输入目标文件中的符号表中所有的符号定义和符号引用收集起来,统一放到一个全局符号表中。这一步中,链接器将能够获得所有输入到目标文件的段长度,并且将它们合并,计算出输出文件中各个段合并后的长度与位置,并建立映射关系。
第二步 符号解析与重定位 使用上面第一步中收集到的所有信息,读取输入文件中段的数据、重定位信息,并且进行符号解析与重定位、调整代码中的地址等。事实上第二步是链接过程的核心,特别是重定位过程。