符号解析
在上一篇文章中,我们熟悉了可重定位文件和可执行文件。我们继续学习链接操作的具体步骤——「符号解析」
回顾一下之前的知识
程序到可执行文件一共有四个操作:
- 预处理
- 编译
- 汇编
- 链接
经过汇编以后已经生成了二进制的文件。但是我们为了好看,还是用汇编代码代替。
再回顾一下链接的步骤
- 确定符号引用关系(符号解析)
- 合并相关 .o 文件(重定位)
- 确定每个符号的地址(重定位)
- 在指令中填入新的地址(重定位)
在这一篇博客中,我们主要负责符号解析的理解
符号解析到底做了什么
确定符号引用关系,将每个模块中引用的符号与某个目标模块的定义符号建立关联
看到上面的 P0.o
和 P1.o
中的箭头了吗。符号解析就是去干箭头干的活。
也就是说,每个定义符号在代码段(函数)和数据段(变量)都分配了存储空间,将引用符号与定义符号建立关联后,就可以在重定位时将引用符号的地址重定位为相关联的定义符号的地址
为了能建立这样的联系,定义了一个叫做「符号表」(symbol table)的东西
所以符号解析的整体过程如下:
- 程序中有定义和引用的符号(包括函数和变量)
- 编译器将定义的符号放在符号表中
- 符号表是一个结构数组
- 每个表项包含符号名、长度位置等信息
- 链接器将每个符号的引用都与一个确定的符号定义建立关联
下面将使用一个具体的例子介绍符号的类型
链接符号的类型
// main.c
int buf[2] = {1, 2};
void swap();
int main() {
swap();
return 0;
}
// swap.c
extern int buf[];
int *bufp0 = &buf[0];
static int *bufp1;
void swap() {
int temp;
bufp1 = &buf[1];
temp = *bufp0;
*bufp0 = *bufp1;
*bufp1 = temp;
}
有三种符号类型
-
Global symbols(模块内部定义的全局符号)
-
由模块 m 定义并能被其他模块引用的符号。例如,非 static C 函数和非 static 的 C 全局变量
如 main 全局变量 buf
-
-
External symbols(外部定义的全局符号)
-
由其他模块定义并能够被 m 引用的全局符号
如,main.c 中的函数名 swap
-
-
Local symbols(本模块的局部符号)
-
仅由模块 m 定义和引用的本地符号。例如,在模块中定义的带 static 的 C 函数和全局变量
如,swap.c 中的 static 变量名 bufp1
-
注意!
函数中的局部变量比如 temp 是存在栈中的。编译器不管那玩意
如何查看目标文件中的符号
// main.c
int buf[2] = {1, 2};
void swap();
int main() {
swap();
return 0;
}
就拿这个例子来说
首先通过命令gcc -c main.c -o main.o
生成可重定义的目标文件
再通过readelf -s main.o
看符号表:
符号表是什么
我们先来看看,每一个表项有哪些数据
typedef struct {
Elf32_Word st_name;
Elf32_Addr st_value;
Elf32_Word st_size;
unsigned char st_info;
unsigned char st_other;
Elf32_Half st_shndx;
} Elf32_Sym;
说说每个成员:
- st_name:在可重定位中有另外一个段(symbol string),里面记录了所有符号, st_name 就是索引
- st_value:相关符号的值,根据上下文,可能就是值,也可能是地址
- st_size:符号的大小。比如数据对象的大小是对象中包含的字节数
- st_info:该成员指定符号的类型和绑定属性
- st_other:该成员当前指定符号的可见性
- st_shndx:每个表项都是根据某个节定义的,所以这个成员记录了节的索引
我们再来看这个图:
- buf 是属于第 3(Ndx)节,OBJECT 对象,全局作用域,大小 8B。
- swap 不知道是哪个节的,不知道什么类型,没有定义的符号(UND),全局作用域
- main 第 1 节(.text)函数对象,大小 21 字节。
链接器对符号的解析规则
首先得知道这两个名词的意思:
- 「强符号」函数名和已初始化的全局变量名
- 「弱符号」未初始化的全局变量名
// p1.c
int val = 10; // 强符号
void p1(); // 弱符号
int val; // 弱符号
void p2() { // 强符号
...
}
之后我们再来看:多重定义符号的处理规则
- 强符号不能多次定义
- 弱一个符号被定义为一次强符号和多次若符号,则按强定义为准
- 弱有多个若符号定义,则任选其中一个
// main.c
int x = 10;
int p1(void);
int main() {
x = p1();
return x;
}
int x = 20;
int p1() {
return x;
}
- main 只有一次定义
- p1 有一次强定义,一次弱定义
- 而 x 有两次强定义,所以会出错!
静态库的符号解析的过程(重要)
链接器在工作的时候:
首先创建三个集合 E、U、D
- E:合并在一起的所有目标文件(还未重定位)
- U:没有解析的符号(定义符号和引用符号还没有被建立联系)
- D:定义符号的集合
现在来叙述全部过程
- 对每一个输入文件来说,首先判断是不是库文件。
-
如果不是库文件,就是目标文件 f。就能把目标文件放入 E 中,根据 f 中未解析符号和定义符号判断后分别放入 U、D 中
比如说,f 中有一个未解析符号 k,如果 D 中存在对它的定义,那么就可以建立联系。如果没有就放入 U 中。
- 如果是库文件,会试图把所有 U 中的符号与库文件中的符号匹配,匹配上了就从 U 放入 D 中。并把匹配上的模块放入 E 中。一直重复直到 U D 不再变化。库文件剩下的内容直接就不管了。
- 如果往 D 中放入了一个已经存在的符号或者扫描完所有文件后 U 还是非空,则链接器会停止并报错。否则执行重定位