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 的值之间的差异的,所以它刚好就是占位符的宽度。