引言
我们都知道一个程序从我们的一个文本文件转化成一个可执行目标文件要经历预处理,编译,汇编和链接四个过程,理解这其中的过程有不仅利于我们写出一个更加健壮,高效的程序 ,而且能在错误时快速定位,成为一个大牛程序员(向之奋斗)
首先我们要清楚链接这个过程究竟干了些什么,我们知道到链接这一步时链接器拿到的是一组可重定向的目标文件,注意是一组,因为我们的程序不仅仅是一个文件,大多数情况下包含多个文件,而且绝大多数情况下其中还包含了库(静态库与动态库),所以链接的任务其实就是把这些个文件变成一个整体,让其中引用和定义一一对应,再把这些一节一节的信息(稍后解释为什么用一节)放到一个可执行目标文件中。这就是我们链接器要完成的工作,接下来我们具体解析其中的细节。
先提出一个概念 首先我们要清楚链接存在于三个阶段 编译时,加载时与运行时,这三个阶段所发生的链接并不相同,编译时就是简单的将每个引用与之定义所对应的内存地址相连,加载时与运行时的链接是为了应用动态链接库。我们逐个的来了解一下其中的细节
1.静态链接
当到了链接这个步骤的时候 我们应该得到的是一组后缀为 .o的可重定位文件,我们的任务就是把各个文件中的引用与定义相连,链接器此时间段做了两件事情
- 符号解析 把每一个符号引用与符号定义相关联
- 重定位 每一个进程都是程序的内存映像,重定位阶段将为程序分配运行时地址,以便在运行时由加载器加载(这个过程不发生从磁盘到内存的任何数据拷贝 这是虚拟内存的优点之一)
符号解析
要清楚符号解析首先我们得清楚什么是符号与符号表 其实符号就是我们的变量名称,函数名称,而符号表就是每个可重定位文件中符号的一个集合
- 全局符号 此模块中定义 其他模块引用 非静态C函数与全局变量
- 外部符号 此模块中引用 其他模块定义 非静态C函数与全局变量
- 局部符号 此模块中定义 此模块中引用 局部变量
看起来好像有点绕 其实仔细一想非常清楚 就是定义与引用是否在一个集合的问题而已
接下来说说符号解析 其实符号解析就是上文所说 把每一个符号引用与符号定义相关联 这个过程要是仅仅发生于局部变量还好,但是当存在全局变量时就不那么简单了,编译器所采取的策略是 维护一个未定义的符号的集合 每扫描一个可重定位文件就在上一个集合中取出已有定义的 同时加入有引用无定义的 当扫描完所有的可重定位文件若集合不为空 则报错,这其中很有意思的一点就是对于全局同名符号也就是全局同名变量的解析 是怎么做到的呢? 我们首先来看一张图
这是使用READELF工具打开的一个可重定位文件的程序头 我们可以看到其中的数据是成节的
为了解释我们的问题 我们先来介绍这几个节的信息
- text 代码段
- data 已初始化的全局变量和静态变量
- bss 未初始化的静态变量 初始化为零的全局变量和静态变量
- comment 未初始化的全局变量
- symtab 符号表
我们问题的答案就藏在两个看似有些奇怪的节中 bss与comment中 他们之间非常的相似,为什么要大费周章分成两个节来存储呢 原因就在于我们对同名全局符号的解析过程
- 强符号 函数和已初始化的全局符号
- 弱符号 未初始化的全局符号
- 同名强符号只能有一个
- 强符号与弱符号同名选择强符号
- 弱符号随机选择(反正值都是零)
概念看起来可能有点枯燥 我们来看段代码
A.c
#include<stdio.h>
int x;
int y = 15;
void Judge_x(void)
{
x = 10;
}
#include"A.c"
#include<stdio.h>
void f(void);
int x = 15;
int y;
int main()
{
Judge_x();
printf("%d %d\n",x,y);
return 0;
}
请简单的浏览这两段代码 并思考所要打印的值
答案是 10 15
你可能会觉得有些诧异 但回想我们刚才提到的全局符号的解析过程 再回想强符号优先级大于弱符号 这个输出就不难理解了
我们还有一个问题尚未解决 就是所提到到的comment与bss节 ,为什么要分成两节存储呢 现在看来其实它们一个是强符号 一个是弱符号,这两个数据节分开的原因也就清楚了 就是为了解析全局同名符号 值得一提的时comment其实是一个伪节 仅在可重定位文件中存在
其实这个过程就是静态库链接的过程,想要了解静态库的
重定位
当我们进行完符号解析这个步骤的时候 我们已经使得所有的符号引用与定义相连接 换句话说 我们已经确定了要生成的的可执行文件相对应的的数据模块的大小了 所以我们要做的其实就是给相对应的数据块分配其运行时的内存 然后把符号引用与运行时的地址相连接 这就构成了我们的重定位 综上 重定位主要可分为这两个步骤
- 重定位节和符号定义
- 重定位节中的符号引用
重定位节和符号定义
这一个步骤所做的事情其实就是把我们一组可重定位文件中类型相同的节进行合并 使得每个类型的节只有一个(想想一个进程的内存图),去掉不再需要的节(伪节,重定位节等),然后对其分配运行时地址 这样 我们每一个符号都就有了一个其独一无二的运行时地址了
重定位节中的符号引用
这个阶段要做的事情就是把符号引用与正确的地址相连 这离不开一个重要的数据结构 可重定位条目 这个数据结构在编译时产生 帮助链接器在生成可执行文件时修改符号引用 这里涉及到了如何去修改,我们来看看可重定位条目的数据结构
typedef struct
{
Elf64_Addr r_offset;
Uint64_t r_info;
int64_t r_addend;
}Elf64_Rela;
到这里 我们就完成了文件的链接过程(不涉及动态链接库的情况下)
我们来看看一个可执行文件的文件头
我们可以看到这其中有七个标志位 分别是
- Offest 偏移量
- filesiz 目标文件段的大小
- align 对齐要求
- memsz 内存中段的大小
- flags 权限
- 剩下两个就是虚拟地址与物理地址
这些段就是加载器在执行时向页表中加载的东西 这里我认为有三点比较有意思 第一 在加载阶段几乎没有从磁盘到内存中的数据复制 这也就是虚拟内存的优点之一 也从侧面看到了虚拟内存是成片分布的 第二 就是第一个load中 filesiz与memsz为什么不一样大呢?答案就在这里 第三为什么会有align对齐这种奇怪的东西 且对齐均是二的幂 我认为这是联系链接与虚拟内存较为重要的地方
链接的基本过程到这里就结束了 动态链接也是一个值得说道的东西 它使得多个程序对于同一个函数使用同一块地址空间成为可能 加载时链接与运行时链接也与动态链接库息息相关 静态库与动态链接库