第四章 静态链接
连接过程的本质就是多个不同的目标文件“粘合”到一起,为了使不同的目标文件可以相互粘合,这些目标文件必须有固定的规则才行,可以将符号看链接过程的粘合剂
-
可重定位文件(.o文件):
-
可重定位文件是编译过程的中间产物,它包含了程序的机器码和符号表,但尚未确定最终的运行时地址。
-
在可重定位文件中,所有的地址都是相对于文件开头的偏移量,而不是绝对地址。这意味着文件本身并没有分配虚拟地址空间,因为它还没有准备好被执行。
-
系统在可重定位文件存在时不会为其分配虚拟地址空间,因为可重定位文件不是直接执行的目标。
-
-
可执行文件:
-
可执行文件是链接过程的产物,它将多个可重定位文件(以及可能的其他资源)合并成一个单一的、可以直接由操作系统加载和执行的文件。
-
在链接过程中,链接器会确定可执行文件中各个段(如代码段、数据段等)的位置和大小,并为它们分配虚拟地址空间。这些虚拟地址空间是逻辑上的地址,用于在程序运行时映射到物理内存(或虚拟内存)中的相应位置。
-
/* a.c */ extern int shared; int main(){ int a = 100; swap(&a,&shared);
/* b.c */ int shared = 1; void swap(int* a,int* b){ *a ^= *b ^= *a ^= *b;//这是位操作符异或, 二进制的数学运算。这是一种不需要临时变量就可以交换ab的方法
使用gcc将上述两个程序编译成目标文件a.o,b.o:
$ gcc -c a.c b.c
4.1 空间与地址分配
ld a.o b.o -e main -o ab -lc //将a.o和b.o静态链接成可执行文件ab
扫描所有的输入目标文件,并且获得它们的各个段的长度、属性和位置,并且将输入目标文件中的符号表中所有的符号定义和符号引用收集起来,统一放到一个全局符号表。这一步中,链接器将能够获得所有输入目标文件的段长度,并且将它们合并,计算出输出文件中各个段合并后的长度与位置,并建立映射关系(程序头表)。
有两种分配方式:
4.1.1 按序叠加 一个最简单的方案就是将输入的目标文件按照次序叠加起来,如下图所示:
按序叠加的优点是做法很简单,直接将各个目标文件依次合并;缺点是在有很多输入文件的情况下,输出文件将会有很多零散的段,每个段都需要有一定的地址和空间对齐要求,会造成内存空间大量的内部碎片,非常浪费空间。
4.1.2 相似段合并 一个更实际的方法是将相同性质的段合并到一起,将所有输入文件的“.text”合并到输出文件的“.text”段,接着是“.data”段、“.bss”段等,如下图所示:
需要注意的是,“.bss”段在目标文件和可执行文件中并不占用文件的空间,但是它在装载时占用地址空间。所以链接器在合并各个段的同时,也将“.bss”合并,并且分配虚拟空间。现在的链接器空间分配的策略基本上都采用上述方法中的第二种,使用这种方法的链接器一般都采用一种叫两步链接(Two-pass Linking) 的方法。整个链接过程分两步。
4.2 符号解析与重定位
4.2.1 符号解析
具体步骤如下
-
收集符号信息::链接器首先扫描所有的输入目标文件,收集它们各自符号表中的所有符号定义和符号引用信息,并将这些信息统一放入一个全局符号表中。
-
解析未定义符号:全局符号表中会包含一些标记为UND(未定义)的符号,这些符号在当前目标文件中被引用但未被定义。链接器的任务是找到这些未定义符号在其他目标文件或库中的定义,并确定它们的实际地址。
-
确定符号地址:一旦找到了未定义符号的定义,链接器就会根据这些定义来更新程序中所有引用该符号的位置的地址。
在相似段合并后,链接器会为每一个符号计算其正确的虚拟地址。这一计算过程涉及为符号添加一个偏移量,该偏移量等于符号所在段在输出文件中的虚拟地址加上该符号在其原始段内的偏移。通过这种方式,链接器确保每个符号都能够被调整到其在最终可执行文件中的正确虚拟地址。
-
分配虚拟内存地址:在链接过程中,链接器还会为可执行文件中的各个段分配虚拟内存地址(VMA)。这些地址是程序在运行时占用的虚拟地址空间中的位置。对于全局符号来说,它们的地址就是它们在各自段中的偏移量加上该段的起始虚拟地址。
$ readelf -s a.o Num: Value Size Type Bind Vis Ndx Name ... 4: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND shared 5: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND swap
$ readelf -s ab //符号表 11: 0000000000401083 79 FUNC GLOBAL DEFAULT 12 swap 12: 0000000000404020 4 OBJECT GLOBAL DEFAULT 16 shared ...
$ objdump -h ab //段表 Idx Name Size VMA LMA File off Algn 24 .data 00000014 0000000000004000 0000000000004000 00003000 2**3 CONTENTS, ALLOC, LOAD, DATA
4.2.2 重定位
反汇编结果中对 "swap","shared" 的引用语句如下
$ objdump -d a.o 0000000000000000 <main>: 26: 48 8d 15 00 00 00 00 lea 0x0(%rip),%rdx # 2d <main+0x2d> //`%rip`是指令指针寄存器,`0x0(%rip)`实际上是一个相对于指令指针寄存器存储的值的偏移量,这里偏移量为0。 2d: 48 89 d6 mov %rdx,%rsi ... 33: e8 00 00 00 00 call 38 <main+0x38> 38: b8 00 00 00 00 mov $0x0,%eax
$ objdump -d ab 0000000000401030 <main>: 401056: 48 8d 15 c3 2f 00 00 lea 0x2fc3(%rip),%rdx # 404020 <shared> ... 401063: e8 1b 00 00 00 call 401083 <swap> 401068: b8 00 00 00 00 mov $0x0,%eax 0000000000401083 <swap>: //00401068 + 000001b = 00401083
重定位表如下:
$ objdump -r a.o a.o: file format elf64-x86-64 RELOCATION RECORDS FOR [.text]: OFFSET TYPE VALUE 0000000000000029 R_X86_64_PC32 shared-0x0000000000000004 0000000000000034 R_X86_64_PLT32 swap-0x0000000000000004 000000000000004d R_X86_64_PLT32 __stack_chk_fail-0x0000000000000004 RELOCATION RECORDS FOR [.eh_frame]: OFFSET TYPE VALUE 0000000000000020 R_X86_64_PC32 .text
-
OFFSET:表示在文件中需要修改的位置的偏移量,从节的开始处计算。
-
TYPE:描述了重定位的类型,即如何修改
OFFSET
指定的位置。 -
VALUE:提供了重定位所需的具体值或符号,用于计算最终的地址或值。
例如,
shared-0x0000000000000004
表示需要引用的符号是shared
,并且需要从该符号的地址中减去4来得到最终的值。
重定位表(Relocation Table)的结构专门用来保存这些与重定位相关的信息,其中每个要被重定位的地方叫一个重定位入口。
-
若代码段”.text“有需要重定位的地方,则会有一个“.rel.text”段
-
若数据段”.data“有需要重定位的地方,则会有一个“.rel.data”段
typedef struct { Elf64_Addr r_offset; /* Address */ Elf64_Xword r_info; /* Relocation type and symbol index */ } Elf64_Rel;
-
r_offset:这是一个
Elf64_Addr
类型的字段,它表示了一个偏移量,这个偏移量是相对于节(section)开始的。它指定了需要被重定位的位置,即在哪里需要修改地址或数据。 -
r_info:字段是一个
Elf64_Xword
类型的复合字段,它包含了重定位类型和符号索引两部分信息,r_info
的高位部分(表示重定位类型,而低位部分则表示符号索引。符号索引用于指向符号表中的一个条目,而重定位类型则决定了如何根据该符号来计算最终的地址或值。