# 7.7.2 重定位符号引用
图 7-10 展示了链接器的重定位算法的伪代码。第 1 行和第 2 行在每个节 s 以及与每个节相关联的重定位条目 r 上迭代执行。为了使描述具体化,假设每个节 s 是一个字节数组,每个重定位条目 r 是一个类型为 Elf64_Rela 的结构,如图 7-9 中的定义。另外,还假设当算法运行时,链接器已经为每个节(用 ADDR(s) 表示)和每个符号都选择了运行时地址(用 ADDR(r.symbol) 表示)。第 3 行计算的是需要被重定位的 4 字节引用的数组 s 中的地址。如果这个引用使用的是 PC 相对寻址,那么它就用第 5~9 行来重定位。如果该引用使用的是绝对寻址,它就通过第 11~13 行来重定位。
1 foreach section s {
2 foreach relocation entry r {
3 refptr = s + r.offset; /* ptr to reference to be relocated */
4
5 /* Relocate a PC-relative reference */
6 if (r.type == R_X86_64_PC32) {
7 refaddr = ADDR(s) + r.offset; /* ref's run-time address */
8 *refptr = (unsigned) (ADDR(r.symbol) + r.addend - refaddr);
9 }
10
11 /* Relocate an absolute reference */
12 if (r.type == R_X86_64_32)
13 *refptr = (unsigned) (ADDR(r.symbol) + r.addend);
14 }
15 }
图 7-10 重定位算法
让我们来看看链接器如何用这个算法来重定位图 7-1 示例程序中的引用。图 7-11 给出了(用 objdump -dx main.o 产生的)GNU OBJDUMP 工具产生的 main.o 的反汇编代码。
1 0000000000000000 <main>:
2 0: 48 83 ec 08 sub $0x8,%rsp
3 4: be 02 00 00 00 mov $0x2,%esi
4 9: bf 00 00 00 00 mov $0x0,%edi %edi = &array
5 a: R_X86_64_32 array Relocation entry
6 e: e8 00 00 00 00 callq 13 <main+0x13> sum()
7 f: R_X86_64_PC32 sum-0x4 Relocation entry
8 13: 48 83 c4 08 add $0x8,%rsp
9 17: c3 retq
图 7-11 main.o 的代码和重定位条目。原始 C 代码在图 7-1 中
main 函数引用了两个全局符号:array 和 sum。为每个引用,汇编器产生一个重定位条目,显示在引用的后面一行上。这些重定位条目告诉链接器对 sum 的引用要使用 32 位 PC 相对地址进行重定位,而对 array 的引用要使用 32 位绝对地址进行重定位。接下来两节会详细介绍链接器是如何重定位这些引用的。
# 1. 重定位 PC 相对引用
图 7-11 的第 6 行中,函数 main 调用 sum 函数,sum 函数是在模块 sum.o 中定义的。call 指令开始于节偏移 0xe 的地方,包括 1 字节的操作码 0xe8,后面跟着的是对目标 sum 的 32 位 PC 相对引用的占位符。
回想一下,重定位条目和指令实际上存放在目标文件的不同节中。为了方便,OBJDUMP 工具把它们显示在一起。
相应的重定位条目 r 由 4 个字段组成:
r.offset = 0xf
r.symbol = sum
r.type = R_X86_64_PC32
r.addend = -4
这些字段告诉链接器修改开始于偏移量 0xf 处的 32 位 PC 相对引用,这样在运行时它会指向 sum 例程。现在,假设链接器已经确定
ADDR(s) = ADDR(.text) = 0x4004d0
和
ADDR(r.symbol) = ADDR(sum) = 0x4004e8
使用图 7-10 中的算法,链接器首先计算出引用的运行时地址(第 7 行):
refaddr = ADDR(s) + r.offset
= 0x4004d0 + 0xf
= 0x4004df
然后,更新该引用,使得它在运行时指向 sum 程序(第 8 行):
*refptr = (unsigned) (ADDR(r.symbol) + r.addend - refaddr)
= (unsigned) (0x4004e8 + (-4) - 0x4004df)
= (unsigned) (0x5)
在得到的可执行目标文件中,call 指令有如下的重定位的形式:
4004de: e8 05 00 00 00 callq 4004e8 <sum> sum()
在运行时,call 指令将存放在地址 0x4004de 处。当 CPU 执行 call 指令时,PC 的值为 0x4004e3,即紧随在 call 指令之后的指令的地址。为了执行这条指令,CPU 执行以下的步骤:
- 将 PC 压入栈中
PC ← PC + 0x5 = 0x4004e3 + 0x5 = 0x4004e8
因此,要执行的下一条指令就是 sum 例程的第一条指令,这当然就是我们想要的!
# 2. 重定位绝对引用
重定位绝对引用相当简单。例如,图 7-11 的第 4 行中,mov 指令将 array 的地址(一个 32 位立即数值)复制到寄存器 %edi 中。mov 指令开始于节偏移量 0x9 的位置,包括 1 字节操作码 0xbf,后面跟着对 array 的 32 位绝对引用的占位符。
对应的占位符条目 r 包括 4 个字段:
r.offset = 0xa
r.symbol = array
r.type = R_X86_64_32
r.addend = 0
这些字段告诉链接器要修改从偏移量 0xa 开始的绝对引用,这样在运行时它将会指向 array 的第一个字节。现在,假设链接器已经确定
ADDR(r.symbol) = ADDR(array) = 0x601018
链接器使用图 7-10 中算法的第 13 行修改了引用:
*refptr = (unsigned) (ADDR(r.symbol) + r.addend)
= (unsigned) (0x601018 + 0)
= (unsigned) (0x601018)
在得到的可执行目标文件中,该引用有下面的重定位形式:
4004d9: bf 18 10 60 00 mov $0x601018,%edi %edi = &array
综合到一起,图 7-12 给出了最终可执行目标文件中已重定位的 .text 节和 .data 节。在加载的时候,加载器会把这些节中的字节直接复制到内存,不再进行任何修改地执行这些指令。
1 00000000004004d0 <main>:
2 4004d0: 48 83 ec 08 sub $0x8,%rsp
3 4004d4: be 02 00 00 00 mov $0x2,%esi
4 4004d9: bf 18 10 60 00 mov $0x601018,%edi %edi = &array
5 4004de: e8 05 00 00 00 callq 4004e8 <sum> sum()
6 4004e3: 48 83 c4 08 add $0x8,%rsp
7 4004e7: c3 retq
8 00000000004004e8 <sum>:
9 4004e8: b8 00 00 00 00 mov $0x0,%eax
10 4004ed: ba 00 00 00 00 mov $0x0,%edx
11 4004f2: eb 09 jmp 4004fd <sum+0x15>
12 4004f4: 48 63 ca movslq %edx,%rcx
13 4004f7: 03 04 8f add (%rdi,%rcx,4),%eax
14 4004fa: 83 c2 01 add $0x1,%edx
15 4004fd: 39 f2 cmp %esi,%edx
16 4004ff: 7c f3 jl 4004f4 <sum+0xc>
17 400501: f3 c3 repz retq
a) 已重定位的 .text 节
1 0000000000601018 <array>:
2 601018: 01 00 00 00 02 00 00 00
b) 已重定位的 .data 节
图 7-12 可执行文件 prog 的已重定位的 .text 节和 .data 节。原始的 C 代码在图 7-1 中
练习题 7.4 本题是关于图 7-12a 中的已重定位程序的。
A. 第 5 行中对 sum 的重定位引用的十六进制地址是多少?
B. 第 5 行中对 sum 的重定位引用的十六进制值是多少?
练习题 7.5 考虑目标文件 m.o 中对 swap 函数的调用(图 7-5)。
9: e8 00 00 00 00 callq e <main+0xe> swap()
它的重定位条目如下:
r.offset = 0xa
r.symbol = swap
r.type = R_X86_64_PC32
r.addend = -4
现在假设链接器将 m.o 中的 .text 重定位到地址 0x4004d0,将 swap 重定位到地址 0x4004e8。那么 callq 指令中对 swap 的重定位引用的值是什么?