目标文件有三种形式:
1. 可重定位目标文件
2. 可执行目标文件
3. 共享目标文件
编译器和汇编器生成可重定位目标文件/共享目标文件,连接器生成可执行目标文件。
在这里我们首先介绍可重定位目标文件。
可重定位目标文件:包含二进制代码和数据,可以在编译时与其他可重定位目标文件合并起来,创建一个可执行目标文件。
可重定目标文件格式:
注意:
bss段 在目标文件中不占实际的空间,它仅仅只是一个占位符。目标文件格式区分初始化和未初始化变量是为了效率,在目标文件中,未初始化变量不需要占据任何实际的磁盘空间。
.symatb段 不包含局部变量的条目。
符号和符号表
符号:
每个可重定位目标模块 M,都有一个符号表,它包含 M 所定义和引用的符号的信息。有三种不同的符号:
1. 由 M 定义并能被其他模块引用的全局符号。(非静态的 C函数以及 不带 C static 属性的全局变量)
2. 由其他模块定义并能被模块 M 引用的全局符号。(外部符号,对应于定义在其他模块中的 C 函数和变量)
3. 只能被模块 M 定义和引用的本地符号。(带 static 属性的 C函数和全局变量)
符号表:
符号表由汇编器构造,输出到汇编语言 .s 文件中的符号。
.symtab 段中包含 ELF 符号表,这张符号表包含一个条目的数组。
ELF 符号表条目:
typedef strcut{
int name; //字符串表偏移,指向符号以 null 结尾的字符串名字
int value; // 符号地址 距定义目标的段的起始位置的偏移
int size; // 目标大小
char type:4 // 表示要么是数据,要么是函数
binding:4; // 表示符号是本地/全局的
char reserved; // Unused
char section; // 一个到段头部表的索引,
// 有三个特殊的伪节,他们在段头部表中是没有条目的
// ABS 代表不该被重定位的符号
// UNDEF 代表未被定义的符号
// COMMON 代表还未被分配位置的未初始化数据目标
};
在这里我们给出一个简单函数
#include <stdio.h>
int buf[2] = {1, 2};
int main( void ){
swap();
return 0;
}
我们通过 gcc 编译器生成对应的 .o 文件,然后使用 readelf -Ws 来查看该 .o文件的符号表。
开始的 8个条目可以暂时忽略,它们是连接器内部使用的本地符号。
其中 Ndx = 1 表示 .text段,Ndx = 3 表示 .data 段。
从第九个条目开始,我们可以看到一个关于全局符号 buf 定义的条目,它是一个位于.data 段中偏移量为 0 处的大小为 8 个字节的目标。
其后跟随着的是全局符号 main 的定义,是位于 .text段中偏移为 0 处的 21 字节函数。
最后一个条目来自对外部符号 swap 的引用。并且是未定义的。
符号解析:
链接器解析符号是将每个引用与它输入的可重定位目标文件的符号表中的一个确定的符号定义联系起来。
编译器只允许每个模块中每个本地符号只有一个定义,编译器还确保静态本地变量,它们也会有本地链接器符号,拥有唯一的名字。
当编译器遇到一个不是在当前模块中定义的符号时,它会假设该符号是在其他某个模块中定义的,生成一个连接器符号表条目,并把它交给链接器处理。如果链接器在它的任何输出模块中都找不到这个被引用的符号,它就输出一条错误信息并终止。