之前写过一篇博客讲述了链接的来由:https://mp.csdn.net/mdeditor/102058918#
这篇博客则打算讲讲链接的核心工作:符号解析和重定位。看这篇博客之前可能需要一点基础,大家可以先看看我上面的链接的文章,再来看这篇。
我上一篇博客讲过那些是符号,也简述了为什么这些是符号。这里我们再来讲一下之前未提及的一些东西。
符号解析
什么是符号解析?
csapp书上的定义为:将每个引用与它输入的可重定位目标文件的符号表中的一个确定的符号定义关联起来。即一个符号对应唯一的一个定义
符号解析分为局部符号解析和全局符号解析,局部的不用说。全局符号解析因为多个目标文件可能会定义相同的名字的全局符号。此时,链接器有两种可能处理方法,报错,和选出一个定义并抛弃其它定义。
再补充一个概念就能知道链接器怎么处理了:
- 不同的符号是有强弱之分的:
强符号:函数和初始化的全局变量
弱符号:未初始化的全局变量
举个例子
// 文件 main1.c
int a = 5; // 强符号,已初始化
p1() { ... } // 强符号,函数
// 文件 main.c
int a; // 弱符号,未初始化
p2() { ... } // 强符号,函数
链接器在处理强弱符号同名情况的时候遵守以下规则:
- 不能出现多个同名的强符号,不然就会出现链接错误
- 如果有同名的强符号和弱符号,选择强符号。就是说弱符号的定义失效的意思,我们可以直接把弱符号定义代码划掉不看
- 如果有多个弱符号,随便选择一个。所以多个弱符号其实都是指向同一个地址。
这些规则很好理解,但是对于第三个规则有一个很隐蔽的坑值得了解一下
// 文件 p1.c
int x;
int y;
p1() { ... }
// -----------------------------------------
// 文件 p2.c
double x;
p2() { ... }
我们看这个例子,这里 p1 和 p2 中定义的变量都是弱符号,我们对 p2 中的 x 进行写入时,会影响到 p1 中的 y。因为两个 x 实际上引用的是同一个地址,而 double 的字节数是 int 的两倍。
再结合 **全局变量、静态全局变量和静态局部变量都存放在内存的静态存储区域,局部变量存放在内存的栈区。**静态储存区向下生长,栈向上生长,堆向下生长
我们就易见x,会影响到y,正好x也是y的两倍大小。
讲这么多其实落到实处就是尽量避免使用全局变量,但是有时这是不可避免的所以也有措施:
- 使用静态变量
- 定义全局变量的时候初始化
- 注意使用 extern 关键字
重定位
链接器完成了符号解析这一步,其实就给所有引用的符号找到了相应的定义。链接器就知道它输入目标模块中的代码节和数据节的确切大小。现在就可以开始重定位步骤了。重定位由两步组成:
1、重定位节和符号定义:在这一步中,链接器将所有相同类型的节合并为同一类型的新的节。例如,来自所有输入模块的 .data 节被全部合并成一个节,这个节称为输出的可执行目标文件的 .data节。然后,链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。当然引用的地址都还没有确定,这个工作我们交给下一步完成。
2、重定位节中的符号引用:在这一步中,链接器修改每个符号的引用,使得它们指向正确的运行时地址,分为重定位相对地址修改和重定位绝对地址修改。要执行这一步,链接器依赖于可重定位目标模块中称为 重定位条目 的数据结构。值得一提,请将符号表和重定位条目区分。
重定位条目如下,只有最终位置未知的引用才生成重定位条目。符号表在我上一篇博客有讲,所有符号都会在符号表中。
typedef struct
{
long offset;
long type:32,
symbol:32;
long addend;
}Elf64_Rela
重定位的算法需要较长篇幅,大家可以参考csapp480到482页,这里仅带过。
1.相对地址重定位
公式为:符号地址(重定位第一步得到)-引用地址(通过节的起始地址+重定位条目中的offset得到)+修正值(即重定位条目的addend)
2.绝对地址重定位
符号地址直接填入
完成这两步后,我们就得到了一个可执行目标文件。