# 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 函数引用了两个全局符号:arraysum。为每个引用,汇编器产生一个重定位条目,显示在引用的后面一行上。这些重定位条目告诉链接器对 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 执行以下的步骤:

  1. 将 PC 压入栈中
  2. 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 的重定位引用的值是什么?