编译与链接 - 重定位
在源码中,我们使用名称来访问定义于其他源文件中的符号。再目标文件中,机器指令可不能通过名称来访问符号,对符号的访问需要被替换为绝对地址或者偏移量。目标文件中,这些符号的地址自然是未知的,在链接时,所有目标文件被合并起来,链接器能获取到所有目标文件中定义的所有符号。此时所有的符号的地址都已经确定,链接器会将指令中对符号的访问地址修改为真实的地址,这个过程就叫做重定位。
例子
下面结合例子来说明重定位的原理,例子如下:
// swap.c
void swap(int *a, int *b) {
int t = *a;
*a = *b;
*b = t;
}
// main.c
#include <stdio.h>
void swap(int *a, int *b);
int main() {
int a = 10, b = 20;
swap(&a, &b);
printf("a: %d b: %d\n", a, b);
return 0;
}
在 main 函数中,调用了 swap 和 printf 两个外部定义的函数,下面观察在 main.o 中,对应的函数调用指令。
$ gcc -c main.c
$ objdump -d main.o
main.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
8: c7 45 fc 0a 00 00 00 movl $0xa,-0x4(%rbp)
f: c7 45 f8 14 00 00 00 movl $0x14,-0x8(%rbp)
16: 48 8d 55 f8 lea -0x8(%rbp),%rdx
1a: 48 8d 45 fc lea -0x4(%rbp),%rax
1e: 48 89 d6 mov %rdx,%rsi
21: 48 89 c7 mov %rax,%rdi
24: e8 00 00 00 00 callq 29 <main+0x29>
29: 8b 55 f8 mov -0x8(%rbp),%edx
2c: 8b 45 fc mov -0x4(%rbp),%eax
2f: 89 c6 mov %eax,%esi
31: bf 00 00 00 00 mov $0x0,%edi
36: b8 00 00 00 00 mov $0x0,%eax
3b: e8 00 00 00 00 callq 40 <main+0x40>
40: b8 00 00 00 00 mov $0x0,%eax
45: c9 leaveq
46: c3 retq
在构造目标文件时,swap 和 printf 这两个函数的地址是未知的,在机器指令中,call 指令的操作数被设置为 0。
$ readelf -r main.o
Relocation section '.rela.text' at offset 0x220 contains 3 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000025 000a00000004 R_X86_64_PLT32 0000000000000000 swap - 4
000000000032 00050000000a R_X86_64_32 0000000000000000 .rodata + 0
00000000003c 000b00000004 R_X86_64_PLT32 0000000000000000 printf - 4
......
可以看到,在 .rela.text 中有三条重定位条目,这意味着在 .text 中有三个位置需要被重定位。通过 offset 字段,可以找到要重定位的位置:
Disassembly of section .text:
0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
8: c7 45 fc 0a 00 00 00 movl $0xa,-0x4(%rbp)
f: c7 45 f8 14 00 00 00 movl $0x14,-0x8(%rbp)
16: 48 8d 55 f8 lea -0x8(%rbp),%rdx
1a: 48 8d 45 fc lea -0x4(%rbp),%rax
1e: 48 89 d6 mov %rdx,%rsi
21: 48 89 c7 mov %rax,%rdi
24: e8 00 00 00 00 callq 29 <main+0x29>
^^^^^^^^^^^
29: 8b 55 f8 mov -0x8(%rbp),%edx
2c: 8b 45 fc mov -0x4(%rbp),%eax
2f: 89 c6 mov %eax,%esi
31: bf 00 00 00 00 mov $0x0,%edi
^^^^^^^^^^^
36: b8 00 00 00 00 mov $0x0,%eax
3b: e8 00 00 00 00 callq 40 <main+0x40>
^^^^^^^^^^^
40: b8 00 00 00 00 mov $0x0,%eax
45: c9 leaveq
46: c3 retq
不难看出,第一个位置需要被修改为 swap 函数的真实地址,第二个位置需要指向 .rodata 段中字符串 “a: %d b: %d\n” 的位置,第三个位置则需要被修改为 printf 函数的位置。
对链接后的可执行文件 main 的代码做反汇编,内容如下:
0000000000401195 <main>:
401195: 55 push %rbp
401196: 48 89 e5 mov %rsp,%rbp
401199: 48 83 ec 10 sub $0x10,%rsp
40119d: c7 45 fc 0a 00 00 00 movl $0xa,-0x4(%rbp)
4011a4: c7 45 f8 14 00 00 00 movl $0x14,-0x8(%rbp)
4011ab: 48 8d 55 f8 lea -0x8(%rbp),%rdx
4011af: 48 8d 45 fc lea -0x4(%rbp),%rax
4011b3: 48 89 d6 mov %rdx,%rsi
4011b6: 48 89 c7 mov %rax,%rdi
4011b9: e8 1e 00 00 00 callq 4011dc <swap>
4011be: 8b 55 f8 mov -0x8(%rbp),%edx
4011c1: 8b 45 fc mov -0x4(%rbp),%eax
4011c4: 89 c6 mov %eax,%esi
4011c6: bf 04 20 40 00 mov $0x402004,%edi
4011cb: b8 00 00 00 00 mov $0x0,%eax
4011d0: e8 6b fe ff ff callq 401040 <printf@plt>
4011d5: b8 00 00 00 00 mov $0x0,%eax
4011da: c9 leaveq
4011db: c3 retq
可以看到,前面提到的三个需要被重定位的位置均已被修改。
重定位原理
在编译过程中,编译器能收集到全部的外部定义的符号,并明确生成的机器指令中那些位置需要被修改。据此,编译器生成重定位条目,描述每一个需要被重定位的位置。不同的指令,其寻址方式不同,做重定位时,有的地址需要被修改为绝对地址,有的则需要被修改为偏移量。重定位条目中包含的多个字段,这些字段明确地给出了重定位的地址以及需要被改写的内容该如何计算。
链接阶段,所有符号的位置都已经固定下来了。链接器只需要读取重定位表,遍历其中的每一个重定位记录,并按描述执行重定位即可。
重定位记录
目标文件中那些符号需要重定位,如何做重定位,这些信息都存储在 ELF 文件的 .rela.* 节,可以使用 readelf -r 命令来查看重定位信息:
$ readelf -r main.o
Relocation section '.rela.text' at offset 0x220 contains 3 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000025 000a00000004 R_X86_64_PLT32 0000000000000000 swap - 4
000000000032 00050000000a R_X86_64_32 0000000000000000 .rodata + 0
00000000003c 000b00000004 R_X86_64_PLT32 0000000000000000 printf - 4
Relocation section '.rela.eh_frame' at offset 0x268 contains 1 entry:
Offset Info Type Sym. Value Sym. Name + Addend
000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text + 0
上面命令输出的每一行重定位信息都使用如下结构之一来存储:
typedef struct {
Elf64_Addr r_offset; /* Address */
Elf64_Xword r_info; /* Relocation type and symbol index */
} Elf64_Rel;
typedef struct {
Elf64_Addr r_offset; /* Address */
Elf64_Xword r_info; /* Relocation type and symbol index */
Elf64_Sxword r_addend; /* Addend */
} Elf64_Rela;
这两者的区别仅仅是 Elf64_Rela 中多了一个 r_addend 字段,对某个重定位 section,其中包含的只能是 Elf64_Rel 和 Elf64_Rela 中的一种,这两不会同时出现。该结构各字段说明如下:
r_offset
需要被重定位改写的地址偏移量,对于不同的 ELF 文件类型该字段的有不同的解读:
- 对可重定位目标文件(ET_REL)而言,
r_offset为 section 中的偏移量 - 对可执行文件(ET_EXEC,ET_DYN)而言,
r_offset为虚拟地址
r_info
该字段中包含两部分信息:指令访问的变量或函数在符号表中的下标、重定位类型
ELF64_R_SYM(info) ((info)>>32) // 高 32 位是符号表中的下标
ELF64_R_TYPE(info) ((Elf64_Word)(info)) // 低 32 位为重定位类型
r_addend
在计算符号重定位的位置时,该字段作为一个附加的偏移量。
重定位表
重定位记录存在于重定位表中,重定位表存在于某个以 .rela 或 .rel 开头的 section 中,表中存放的是 Elf64_Rela 或 Elf64_Rel 结构。重定位 section 与一个符号表关联,重定位 section 的 header 中 sh_link 字段指向与之关联的符号表。
重定位 section 的 header 中 r_info 字段中包含了与该重定位条目关联的符号的下标,sh_info 字段指向真正执行重定位的 section 的 header,通过 Elf64_Rela 中的 r_offset 字段可定位到重定位的位置。
下图描述了前述各个表之间的关系:

可以使用实际例子来验证以上结论:
$ readelf -S main.o
There are 13 section headers, starting at offset 0x2f8:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
0000000000000026 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 00000218
0000000000000048 0000000000000018 I 10 1 8
[ 3] .data PROGBITS 0000000000000000 00000066
0000000000000000 0000000000000000 WA 0 0 1
[ 4] .bss NOBITS 0000000000000000 00000066
0000000000000000 0000000000000000 WA 0 0 1
[ 5] .rodata PROGBITS 0000000000000000 00000066
000000000000000c 0000000000000000 A 0 0 1
[ 6] .comment PROGBITS 0000000000000000 00000072
0000000000000012 0000000000000001 MS 0 0 1
[ 7] .note.GNU-stack PROGBITS 0000000000000000 00000084
0000000000000000 0000000000000000 0 0 1
[ 8] .eh_frame PROGBITS 0000000000000000 00000088
0000000000000058 0000000000000000 A 0 0 8
[ 9] .rela.eh_frame RELA 0000000000000000 00000260
0000000000000030 0000000000000018 I 10 8 8
[10] .symtab SYMTAB 0000000000000000 000000e0
0000000000000120 0000000000000018 11 9 8
[11] .strtab STRTAB 0000000000000000 00000200
0000000000000018 0000000000000000 0 0 1
[12] .shstrtab STRTAB 0000000000000000 00000290
0000000000000061 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
.rela.text 中存放的是 .text 中的重定位记录,.rela.text 的 Link 字段为 10,对应的是符号表 .text,info 字段为 1,对应的是 .text。
重定位信息
Elf64_Rela 结构体中的 r_info 字段包含两部分信息,一个该重定位符号在符号表中的下标,第二部分信息是重定位类型。
重定位就是把指令中地址修改为真实地址,这里包含三部分信息:
- 需要修改的位置
- 需要修改多少个字节
- 需要修改为什么值
Elf64_Rela 结构体中的 r_offset、r_info、r_addend 三个字段就提供了这部分信息。首先 r_offset 告知需要修改的位置,r_info 中的重定位类型指明了需要修改多少字节,重定位类型结合 r_addend 指明需要被修改为什么值。
不同的重定位类型,需要修改的字节数不同,且重定位时被修改的值的计算方式不同。下表列出了需要修改的字节数(Field),以及最终目标值的计算方式(Calculation)。
在 x86_64 架构上包含如下重定位类型:
| Name | Value | Field | Calculation |
|---|---|---|---|
| R_X86_64_NONE | 0 | None | None |
| R_X86_64_64 | 1 | qword | S + A |
| R_X86_64_PC32 | 2 | dword | S + A – P |
| R_X86_64_GOT32 | 3 | dword | G + A |
| R_X86_64_PLT32 | 4 | dword | L + A – P |
| R_X86_64_COPY | 5 | None | Value is copied directly from shared object |
| R_X86_64_GLOB_DAT | 6 | qword | S |
| R_X86_64_JUMP_SLOT | 7 | qword | S |
| R_X86_64_RELATIVE | 8 | qword | B + A |
| R_X86_64_GOTPCREL | 9 | dword | G + GOT + A – P |
| R_X86_64_32 | 10 | dword | S + A |
| R_X86_64_32S | 11 | dword | S + A |
| R_X86_64_16 | 12 | word | S + A |
| R_X86_64_PC16 | 13 | word | S + A – P |
| R_X86_64_8 | 14 | word8 | S + A |
| R_X86_64_PC8 | 15 | word8 | S + A – P |
| R_X86_64_PC64 | 24 | qword | S + A – P |
| R_X86_64_GOTOFF64 | 25 | qword | S + A – GOT |
| R_X86_64_GOTPC32 | 26 | dword | GOT + A – P |
| R_X86_64_SIZE32 | 32 | dword | Z + A |
| R_X86_64_SIZE64 | 33 | qword | Z + A |
在 Field 一栏中,qword 表示 64 位,dword 表示 32 位,word 表示 16 位,word8 表示 8 位。Calculation 一栏中,包含由一些字母组成的计算公式,这些字母的含义如下:
| 符号 | 含义 | 备注 |
|---|---|---|
| A | The addend used to compute the value of the relocatable field. | Elf64_Rela 中的 Addend 字段的值 |
| B | The base address at which a shared object is loaded into memory during execution. Generally, a shared object file is built with a 0 base virtual address, but the execution address is different. | 动态库在内存中的起始虚拟地址 |
| G | The offset into the global offset table at which the address of the relocation entry’s symbol resides during execution. | 当前符号在 GOT 中的位置 |
| GOT | The address of the global offset table. | GOT 起始地址 |
| L | The section offset or address of the procedure linkage table entry for a symbol. | section 偏移量,或者该符号在 PLT 中的位置 |
| P | The section offset or address of the storage unit being relocated, computed using r_offset. | section 偏移量,或需要被重定位改写的内存地址 |
| S | The value of the symbol whose index resides in the relocation entry. | 重定位符号的值(符号表中该符号的 value 字段的值) |
案例分析
这里以前文中的程序为例,来分析重定位的计算流程。
// main.c
#include <stdio.h>
void swap(int *a, int *b);
int main() {
int a = 10, b = 20;
swap(&a, &b);
printf("a: %d b: %d\n", a, b);
return 0;
}
使用 objdump -d 可以看到汇编代码:
$ objdump -d main.o
main.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
8: c7 45 fc 0a 00 00 00 movl $0xa,-0x4(%rbp)
f: c7 45 f8 14 00 00 00 movl $0x14,-0x8(%rbp)
16: 48 8d 55 f8 lea -0x8(%rbp),%rdx
1a: 48 8d 45 fc lea -0x4(%rbp),%rax
1e: 48 89 d6 mov %rdx,%rsi
21: 48 89 c7 mov %rax,%rdi
24: e8 00 00 00 00 callq 29 <main+0x29>
^^^^^^^^^^^
29: 8b 55 f8 mov -0x8(%rbp),%edx
2c: 8b 45 fc mov -0x4(%rbp),%eax
2f: 89 c6 mov %eax,%esi
31: bf 00 00 00 00 mov $0x0,%edi
^^^^^^^^^^^
36: b8 00 00 00 00 mov $0x0,%eax
3b: e8 00 00 00 00 callq 40 <main+0x40>
^^^^^^^^^^^
40: b8 00 00 00 00 mov $0x0,%eax
45: c9 leaveq
46: c3 retq
其中画波浪线的地方需要被改写,下面是重定位表的内容:
$ readelf -r main.o
Relocation section '.rela.text' at offset 0x220 contains 3 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000025 000a00000004 R_X86_64_PLT32 0000000000000000 swap - 4
000000000032 00050000000a R_X86_64_32 0000000000000000 .rodata + 0
00000000003c 000b00000004 R_X86_64_PLT32 0000000000000000 printf - 4
这里三处需要重定位,其中涉及两种重定位类型,我们分别来看。
先看重定位类型为 R_X86_64_PLT32 的 swap,这个重定位类型下,重定位后的值的计算公式为:L + A – P
- L: swap 在 PLT 中的位置(后面介绍位置无关代码时会详细说明)
- A: -4
- P: call 指令的操作数的地址
L 实际上就是需要被调用的函数的地址,这里 call 指令采用相对偏移量来寻址,基址就是下一条指令的地址。因为 call 指令的操作数是 4 个字节,因此 P + 4 就是下条指令的地址。因此与 swap 的实际调用地址的偏移量为 L - (P + 4),前面的计算公式 L + A – P = L - (P - A) 中,而此处 A 的取值恰好就是 -4。
注意: 当你看到可执行文件 main 的反汇编指令时,会发现 swap 并未出现在 PLT 中,这是因为 swap 是被链接到可执行文件中,无需 PLT。
0000000000401195 <main>:
401195: 55 push %rbp
401196: 48 89 e5 mov %rsp,%rbp
401199: 48 83 ec 10 sub $0x10,%rsp
40119d: c7 45 fc 0a 00 00 00 movl $0xa,-0x4(%rbp)
4011a4: c7 45 f8 14 00 00 00 movl $0x14,-0x8(%rbp)
4011ab: 48 8d 55 f8 lea -0x8(%rbp),%rdx
4011af: 48 8d 45 fc lea -0x4(%rbp),%rax
4011b3: 48 89 d6 mov %rdx,%rsi
4011b6: 48 89 c7 mov %rax,%rdi
4011b9: e8 1e 00 00 00 callq 4011dc <swap>
^^^^^^^^^^^
4011be: 8b 55 f8 mov -0x8(%rbp),%edx
4011c1: 8b 45 fc mov -0x4(%rbp),%eax
4011c4: 89 c6 mov %eax,%esi
4011c6: bf 04 20 40 00 mov $0x402004,%edi
^^^^^^^^^^^
4011cb: b8 00 00 00 00 mov $0x0,%eax
4011d0: e8 6b fe ff ff callq 401040 <printf@plt>
^^^^^^^^^^^
4011d5: b8 00 00 00 00 mov $0x0,%eax
4011da: c9 leaveq
4011db: c3 retq
00000000004011dc <swap>:
4011dc: 55 push %rbp
4011dd: 48 89 e5 mov %rsp,%rbp
4011e0: 48 89 7d e8 mov %rdi,-0x18(%rbp)
4011e4: 48 89 75 e0 mov %rsi,-0x20(%rbp)
4011e8: 48 8b 45 e8 mov -0x18(%rbp),%rax
4011ec: 8b 00 mov (%rax),%eax
4011ee: 89 45 fc mov %eax,-0x4(%rbp)
4011f1: 48 8b 45 e0 mov -0x20(%rbp),%rax
4011f5: 8b 10 mov (%rax),%edx
4011f7: 48 8b 45 e8 mov -0x18(%rbp),%rax
4011fb: 89 10 mov %edx,(%rax)
4011fd: 48 8b 45 e0 mov -0x20(%rbp),%rax
401201: 8b 55 fc mov -0x4(%rbp),%edx
401204: 89 10 mov %edx,(%rax)
401206: 90 nop
401207: 5d pop %rbp
401208: c3 retq
401209: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
main 中对 swap 的调用是通过计算 swap 函数与 callq 后的一条指令的偏移量实现的。计算结果为 0x4011dc - 0x4011be = 0x1e,可以看到在 4011b9 处,callq 的操作数正是 0x1e。
下一个重定位需要修改 mov 指令的操作数,重定位类型为 R_X86_64_32,需要修改 2 个字节,重定位目标值计算公式为 S + A。
- S: 符号
.rodata的 value - A: 0
根据 info 字段的值 00050000000a,可以得知该符号在符号表中的下标为 5,查看符号表:
$ readelf -s main.o
Symbol table '.symtab' contains 12 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS main.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000000 0 SECTION LOCAL DEFAULT 7
7: 0000000000000000 0 SECTION LOCAL DEFAULT 8
8: 0000000000000000 0 SECTION LOCAL DEFAULT 6
9: 0000000000000000 71 FUNC GLOBAL DEFAULT 1 main
10: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND swap
11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf
下标为 5 的符号是一个 section,section 的 index 也是 5。查看 section 列表会发现对应的 section 为 .rodata
$ readelf -S main.o
There are 13 section headers, starting at offset 0x2e8:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
0000000000000047 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 00000220
0000000000000048 0000000000000018 I 10 1 8
[ 3] .data PROGBITS 0000000000000000 00000087
0000000000000000 0000000000000000 WA 0 0 1
[ 4] .bss NOBITS 0000000000000000 00000087
0000000000000000 0000000000000000 WA 0 0 1
[ 5] .rodata PROGBITS 0000000000000000 00000087
000000000000000f 0000000000000000 A 0 0 1
[ 6] .comment PROGBITS 0000000000000000 00000096
0000000000000012 0000000000000001 MS 0 0 1
[ 7] .note.GNU-stack PROGBITS 0000000000000000 000000a8
0000000000000000 0000000000000000 0 0 1
[ 8] .eh_frame PROGBITS 0000000000000000 000000a8
0000000000000038 0000000000000000 A 0 0 8
[ 9] .rela.eh_frame RELA 0000000000000000 00000268
0000000000000018 0000000000000018 I 10 8 8
[10] .symtab SYMTAB 0000000000000000 000000e0
0000000000000120 0000000000000018 11 9 8
[11] .strtab STRTAB 0000000000000000 00000200
0000000000000019 0000000000000000 0 0 1
[12] .shstrtab STRTAB 0000000000000000 00000280
0000000000000061 0000000000000000 0 0 1
回到重定位,重定位时 mov 指令的操作数需要被修改为 .rodata 的 value + addend,这里 addend 为 0。
main.o 中的 .rodata 在链接后会被合并到可执行文件的 .rodata 中。这里我们计算最终的地址时,需要关注的是 main 的符号表中 .rodata 的值。
查看 main 的 section 列表,得知 .rodata 的 index 为 16,再查看符号表:
$ readelf -s main
...
Symbol table '.symtab' contains 76 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000004002a8 0 SECTION LOCAL DEFAULT 1
2: 00000000004002dc 0 SECTION LOCAL DEFAULT 2
3: 0000000000400300 0 SECTION LOCAL DEFAULT 3
4: 0000000000400328 0 SECTION LOCAL DEFAULT 4
5: 0000000000400348 0 SECTION LOCAL DEFAULT 5
6: 00000000004003a8 0 SECTION LOCAL DEFAULT 6
7: 00000000004003e8 0 SECTION LOCAL DEFAULT 7
8: 00000000004003f0 0 SECTION LOCAL DEFAULT 8
9: 0000000000400410 0 SECTION LOCAL DEFAULT 9
10: 0000000000400428 0 SECTION LOCAL DEFAULT 10
11: 0000000000401000 0 SECTION LOCAL DEFAULT 11
12: 0000000000401030 0 SECTION LOCAL DEFAULT 12
13: 0000000000401060 0 SECTION LOCAL DEFAULT 13
14: 0000000000401070 0 SECTION LOCAL DEFAULT 14
15: 00000000004012c4 0 SECTION LOCAL DEFAULT 15
16: 0000000000402000 0 SECTION LOCAL DEFAULT 16
17: 0000000000402014 0 SECTION LOCAL DEFAULT 17
...
可以看到 .rodata 的 value 为 0000000000402000,最终计算出重定位目标值 S + A = 0x402000。
总结
在编译阶段,编译器以源文件作为一个单元来编译产生目标文件。因为目标文件需要被链接器链接为可执行文件,且目标文件中各个 section 在可执行文件中的位置是不确定的,目标文件中的变量和函数的地址也不确定。在编译阶段,实际上无法待访问符号最终的地址。链接器首先排定目标文件中的各个 section,这样各符号的地址就确定了,此后链接器开始改写指令中的访问地址,将其修改为运行时真正的地址,这个过程就叫做重定位。
在编译阶段,编译器会生成一个重定位表,表中的各个元素就是需要被重定位的条目。每一个条目就对应编译指令中需要被改写的一个位置。重定位条目中描述了重定位策略、待重定位改写的地址和重定位的符号信息。在链接阶段,链接器基于这些信息来执行重定位,让目标文件的指令中引用到正确的地址上。