静态链接主要的过程如下
- 空间和地址分配
- 符号解析和重定位
- COMMON 块
- 静态库链接
- 链接过程控制
案例代码
// a.c
extern int shared;
int main()
{
int a = 100;
swap(&a, &shared);
}
// b.c
int shared = 1;
void swap(int *a, int *b)
{
*a ^= *b ^= *a ^= *b;
}
// 将上述两个文件进行编译会得到两个 .o 文件
gcc -c a.c b.c
空间和地址分配
链接器作用:将多个目标文件加工后合并成一个输出文件
问题:多个输入文件中的空间是如何分配到?
按序叠加
- 将后面的目标文件叠加到前面的目标文件中
- 存在的问题是会有很多零散的段,此时会比较浪费空间,因为每一个段都有地址和空间对齐要求
-
相似段合并
同一个类型的段放在一起
- 使用相似段合并到链接器一般都是使用两步链接的方法,即链接过程分成两步
- 空间和地址分配:扫描所有目标文件,获取每个段都长度,属性和位置,并且将输入的目标文件的符号表中的所有符号引用收集起来,统一放在一个全局符号表中
- 符号解析和重定位:使用第一步收集的信息,读取文件中段的数据、重定位信息,并且进行符号解析和重定位,调整代码中的地址等
下面使用链接器命令来链接两个目标文件
// -e main 表示将main函数作为程序的入口,-o ab 表示链接输出文件名为ab,默认为a.out
ld a.o b.o -e main -o ab
查看合并前后的虚拟地址
我们只看 .text 段,也可以看出来是进行段合并
问题:从上面可以看到,在合并后的可执行文件 ab 中,虚拟地址不是从 0 开始,实际开始地址是 00000000004000e8 那么为什么不是从0开始呢?
todo 这个问题在可执行文件的装载与进程中解决
符号地址的确定
- 经过前一步的虚拟地址分配,已经可以知道每一个段的大小和起始地址
- 段中每一个符号的相对位置都是确定的
- 计算符号的地址就是段基址+偏移量
链接器的重定位
首先反汇编 a.o
再看链接后的 ab 可执行文件
问题:链接器是如何知道哪些指令的地址是需要被调整的呢?
在 ELF 文件中,是有一个重定位表的结构专门用来保存这些与重定位相关的信息,在ELF 文件中重定位表是一个单独的段,叫做重定位段
查看重定位表