符号表
符号表位于 ELF 文件中的 .symtab 节,其定义了可重定位目标模块中定义和引用的符号的信息
符号( Symbol )
这里说的符号,其实就是汇编语言中提的那个【标号 Label】。
每个符号对应一个函数 / 全局变量 / 静态(Static)变量
全局符号、外部符号、局部符号
- 全局符号:在本模块中定义,其他模块引用的符号。这些符号应该是非静态的 C 函数和全局变量;
- 外部符号:在其他模块中定义,在本模块引用的全局符号;
- 局部符号:在本模块定义,且只在本模块引用的符号。这样的符号在本模块内部随处可见,但不能在其他模块中引用,在 C 中需要用
static关键字声明。
在 C 中,一个 .c 文件就是一个模块,使用 static 关键字声明的函数、变量是该模块私有的,只能在该模块内部访问。没有使用该关键字声明的变量则所有模块都相互可见。应尽量用 static 来保护模块内的函数和变量。
链接器不关心本地非静态程序变量(即局部变量)的任何符号,这些符号都在运行时栈中才会被处理。但,特例是,带 static 关键字的局部变量除外——这些变量即使写在局部变量该出现的位置,也还会被在 .data 和 .bss 中定义和分配空间,符号表中也有唯一名字对应的本地链接器符号。
.symtab 中的条目
typedef struct {
int name; /* 字符串表中的节偏移,指向以 null 结尾的字符串名字 */
char type:4,/* 标记该符号的类型,比如是数据(OBJECT)或函数(FUNC)还是其他 */
binding:4;/* 表明该符号是本地的还是全局的 */
char reserved;
short section;
long value;
long size;
} Elf64_Symbol;
符号解析
链接器对符号的解析工作,就是将每个引用都和目标文件的符号表中的符号定义进行关联。
如果符号的定义和引用都在同一个模块,直接查找就可以(每个符号的名字都是唯一的);如果不在同一个文件,那么编译器会假设在当前编译的代码中找不到的符号定义在其他模块中——此时编译器会生成一个链接符号表条目,将该问题留给链接器解决。当链接器也没能找到被引用符号的定义时,就会报错
因此,未定义的符号能过编译(gcc -S)和汇编(gcc -c),过不了链接(ld)
未链接的目标文件中,对未定义函数的调用代码形如:callq 12 <main +0x12>,这句指令对应的机器码是这样的:
d: e8 00 00 00 00
12: b8 00 00 00 00
为什么是这样的呢?这是因为该目标文件还没有经过链接,其中机器码 e8 是指令 callq,e8 之后的参数(即目标地址)被填的全是 0 ,也就是留空了——这个留空的操作,就是汇编器留给链接器的。除了留空目标地址,汇编器还为链接器创建了“重定位记录(Relocation Record)”。
那么既然 callq 的地址为空,为什么反汇编出来的指令还是 callq 12 <main +0x12> 呢?
这是因为反汇编器也对这个事情心里有数,它知道这里还在等着链接器来重定位,因此先将 callq 的参数( offset )直接设置为下一条指令的地址( 即反汇编出来的指令中的 0x12,对应上面机器代码可见,正好是跳转指令的下一条指令。此处的 offset 还是 main+0x12,因为 main 还在 0x0 ) 该文件经过链接后,就会显示为正确的 offset
链接器对多重定义的处理
全局符号可以分为 strong 和 weak 符号,前者是函数和已初始化的变量,后者是未初始化的全局变量
对于多重定义的情况,链接器会按照这样的规则进行检查和处理:
- strong 符号不能重名,如果发现重名的 strong 符号,会报
multiple definition of xxx; - strong、weak 重名,则保留 strong 符号;
- 多个 weak 重名,在这些符号中随机选择一个。
其中,第 2、3 种情况通常只给一个 warn 然后不报错,然而这种不报错却最为致命。。这可能会引发一些诡异的问题,而且问题可能过了很久才会出现在距离错误代码很远的地方。举例而言,如果一个 64bit 的 double 把一个 32bit 的 int 给替换掉了,那就意味着这个变长了的空间也许把下一条指令或者数据盖掉了 32 位。。这种情况,程序炸了也会让人摸不到头脑,很难修正
静态库链接
先摆这么一张图来,静态库的构造和使用基本就是这么个过程。
静态库的后缀名通常为 .a ,意为 archive ,也就是归档(俗话叫打包)。
从上面的图也能看出来,静态库其实就是一大堆目标文件的集合。库的开发者把可能用到的各种代码分别编译和汇编成一个个小的对象文件,然后再用 AR 将他们打包在一起,就成了静态库。
动手看看系统的静态链接库
将 /usr/lib/x86_64-linux-gnu/libc.a 用 $ ar -x libc.a 解包,可以得到一堆的 .o ,这中间就有我们熟悉的 printf.o、scanf.o 等。如果使用 objdump 对这些目标文件进行反编译,可以发现每个文件都只有 .text 段
使用 ld 时,链接参数加 -static 就是静态链接,这样链接出来的程序直接加载进内存即可运行,无需在加载时再进行动态链接。ld 会判断代码中的引用,引用的是作为参数输入的静态库中的哪个可重定位对象的定义(符号),并将该对象代码拷贝过来(按需拷贝,用什么拷贝什么),再进行重定位
ld 的执行逻辑
ld 对于输入的参数逐个从左往右读,如果输入的文件是 .o , 就会等待合并,接着处理下一个文件;如果输入的是 .a ,则检查其中定义的 .o 哪个能和没找到符号定义的引用对上,如果对上了,那就把这个 .o 也加入到等待合并的文件中,如果对不上就继续处理下一个输入文件。
如果扫描完所有的输入文件后,仍然有没找到对应定义的符号引用,就会给出一个报错;如果一切顺利,就可以合并和重定位所有的 .o 文件,生成一个 .out
注意: 由于 ld 从左向右逐个读输入文件,所以包含某符号定义的文件,必须排列在含该符号引用的文件之后,否则即使包含符号的定义也会被忽略。如果存在“引用和符号所在文件交错存在”的情况,也可以在命令序列中多次重复文件。