R_X86_64_PC32的重定位计算方式

R_X86_64_PC32的重定位计算方式

R_X86_64_PC32的重定位计算方式 #

重定位表的数据结构是这样的:

typedef struct {
  Elf64_Addr   r_offset; /* 重定位表项的偏移地址 */
  Elf64_Xword  r_info;   /* 重定位的类型以及重定位符号的索引 */
  Elf64_Sxword r_addend; /* 重定位过程中需要的辅助信息 */
} Elf64_Rela;

其中,r_info 的高 32bit 存放的是重定位符号在符号表的索引,r_info 的低 32bit 存放的是重定位的类型的索引。符号表就是.symtab 段,可以把它看成是一个字典,这个字典以整数为 key,以符号名为value。

重定位表中,类型为 R_X86_64_PC32,其重定位计算方式为:S + A – P。

这里的 S 表示完成链接后该符号的实际地址。在链接器将多个中间文件的段合并以后,每个符号就按先后顺序依次都会分配到一个地址,这就是它的最终地址 S。

A 表示 Addend 的值,它代表了占位符的长度。

P 表示要进行重定位位置的地址或偏移,可以通过 r_offset 的值获取到,这是引用符号的地方,也就是我们要回填地址的地方,简单说,它就是我们上文提到的用 0 填充的占位符的地址。

这里 S 与 P 所表示的地址都是文件合并之后最终的虚拟地址,由于我们无法获取链接器中间过程的文件,所以,我们需要通过查看链接完成后的可执行文件,来寻找这两个地址。

我们以 extern_var 的变量为例,具体跟踪一遍重定位的过程。

00000000004004ad <main>:
  4004ad:       55                      push   %rbp
  4004ae:       48 89 e5                mov    %rsp, %rbp
  4004b1:       48 83 ec 20             sub    $0x20, %rsp
  4004b5:       8b 05 75 0b 20 00       mov    0x200b75(%rip), %eax # 601030 <extern_var>
  4004bb:       89 45 e8                mov    %eax, -0x18(%rbp)

上边输出部分是对生成可执行文件的反汇编。根据 S、A、P 的定义,我们知道对于 extern_var 来讲:

S 是其最终符号的真实地址,如上汇编里边的注释所示 也就是上面注释的 0x601030 这个地址; A 是 Addend 的值,可以从重定位表里查到是 -4; P 是重定位 offset 的地址,这里是 0x4004b7。

根据公式,我们算出重定位处需要填写的值应该是 0x601030 + (-4) – 0x4004b7 = 0x200b75,也就是最终可执行文件中这条 mov 指令里的值。

系统为什么搞这么一套复杂的公式来计算出这么一个值呢?这个值的真正含义是什么?

针对这个问题,我们再从 CPU 的角度来看下这里的取值关系。从上面 main 函数的反编译的结果可以看到,我们最终对 extern_var 的访问生成的汇编是: mov 0x200b75(%rip), %eax 这是一条 PC 相对偏移的寻址方式。当 CPU 执行到这条指令的时候,%rip 的值存放的是下一条指令的地址,也就是 0x4004bb。这时候可以算出 0x4004bb + 0x200b75 = 0x601030,刚好是 extern_var 的实际地址。

经过正面分析这个重定位的值的作用后,这里我们再来理解一下 S+A-P 这个公式的作用。链接器有了整体的虚拟内存布局后,知道的信息是:需要重定位符号的地址 S 的值是 (0x601030),以及需要重定位的位置地址 P 的值是 (0x4004b7)。

这时候,链接器需要在指令中占位符的位置填一个值,让程序运行的时候能够找到 S。但程序运行到这条指令的时候,能够拿到的地址就只有 PC 的值,也就是下一条指令的地址 (0x4004bb)。你会发现,重定位地址的值跟下一条 pc 的值,相差的就是这个 Addend(-4),这个 Addend 实际上就是用来调整 P 的值和执行时 PC 的值之间的差异的,所以它刚好就是占位符的宽度。

Viewpoint #

From #

06 | 静态链接:变量与内存地址是如何映射的?