ELF 定义了 32 种不同的重定位类型
我们只关心其中两种最基本的重定位类型
重定位PC相对引用
R_X86_64_PC32
重定位一个使用 32 位 PC 相对地址的引用
一个 PC 相对地址就是距程序计数器(PC)的当前运行时值的偏移量
当 CPU 执行一条使用 PC 相对寻址的指令时
它就将在指令中编码的 32 位值加上 PC 的当前运行时值,得到有效地址(如 call 指令的目标),PC 值通常是下一条指令在内存中的地址回顾重定位算法中的代码片段
0000000000000000 <main>: 0: 48 83 ec 08 sub $0x8,%rsp 4: be 02 00 00 00 mov $0x2,%esi 9: bf 00 00 00 00 mov $0x0,%edi %edi = &array a: R_X86_64_32 array Relocation entry e: e8 00 00 00 00 callq 13 <main+0x13> sum() f: R_X86_64_PC32 sum-0x4 Relocation entry 13: 48 83 c4 08 add $0x8,%rsp 17: c3 retq函数 main 调用 sum 函数,sum 函数是在模块 sum.o 中定义的
call 指令开始于节偏移 0xe 的地方’包括 1 字节的操作码 0xe8
后面跟着的是对目标 sum 的 32 位 PC 相对引用的占位符13 <main+0x13> sum()相应的重定位条目 r 由 4 个字段组成:
r.offset = 0xf //f: R_X86_64_PC32 sum-0x4 Relocation entry r.symbol = sum r.type = R_X86_64_PC32 r.addend = -4
r.offset由上面的反汇编代码给出f: R_X86_64_PC32 sum-0x4 Relocation entry,即因为采用了 R_X86_64_PC32sum-0x4这里减去的0x4应该是固定的某种规范
r.offset计算得出的是偏移量(重定位入口)Note
GPT的解释
在这个代码片段中,sum的可重定位条目r.offset为 0xf,这是因为在这个特定的上下文中,重定位条目R_X86_64_PC32被用于调用sum()函数的指令上。R_X86_64_PC32表示一个32位的PC相对地址,用于计算相对于指令下一条指令的地址的偏移。在这里,
callq 13 <main+0x13>指令的下一条指令是位于地址 0x13 的指令。因此,相对于callq指令的偏移是 0xf(十六进制),即0x13 - 0x4(callq指令的长度是 5 个字节,所以减去 0x4)。所以r.offset被设置为 0xf,以确保正确计算相对于sum()函数的地址。
这些字段告诉链接器修改开始于偏移量 0xf 处的 32 位 PC 相对引用,这样在运行时它会指向 sum 例程
现在,我们再回顾一下重定位符号引用中的算法伪代码实现
重定位符号引用
foreach section s { //对于每个节 foreach relocation entry r { //对于每个重定位条目,条目在节上 refptr = s + r.offset; /* ptr to reference to be relocated */ /* Relocate a PC-relative reference */ if (r.type == R_X86_64_PC32){ refaddr = ADDR(s) + r.offset; /* ref's run-time address */ *refptr = (unsigned) (ADDR(r.symbol) + r.addend - refaddr); } /* Relocate an absolute reference */ if (r.type ==R_X86_64_32) *refptr = (unsigned) (ADDR(r.symbol) + r.addend); } }第 1 行和第 2 行在每个节 s 以及与每个节相关联的重定位条目 r 上迭代执行
Link to original
假设每个节 s 是一个字节数组,每个重定位条目 r 是一个类型为 Elf64_Rela 的结构
假设当算法运行时,链接器已经为每个节(用 ADDR(s) 表示)和每个符号都选择了运行时地址(用 ADDR(r.symbol) 表示)
ADDR(s)表示节的地址
ADDR(r.symbol)表示符号的运行时地址
第 3 行计算的是需要被重定位的 4 字节引用的数组 s 中的地址,也就是这个符号在节中的相对位置
如果这个引用使用的是 PC 相对寻址,那么它就用第 5 ~ 9 行来重定位
如果该引用使用的是绝对寻址,它就通过第 11 ~ 13 行来重定位
现在,假设链接器已经确定ADDR(s) = ADDR(.text) = 0x4004d0和ADDR(r.symbol) = ADDR(sum) = 0x4004e8,这两个东西作为条件补充给出
使用图中的算法,链接器首先计算出引用的运行时地址(第 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 指令之后的指令的地址。为了执行这条指令,CPU 执行以下的步骤:
Link to original
1)将 PC 压入栈中
2)PC ← PC + 0x5 = 0x4004e3 + 0x5 = 0x4004e8
因此,要执行的下一条指令就是 sum 例程的第一条指令,这当然就是我们想要的!
重定位绝对引用
R_X86_64_32
重定位一个使用 32 位绝对地址的引用
通过绝对寻址,CPU 直接使用在指令中编码的 32 位值作为有效地址,不需要进一步修改回顾重定位算法中的代码片段
0000000000000000 <main>: 0: 48 83 ec 08 sub $0x8,%rsp 4: be 02 00 00 00 mov $0x2,%esi 9: bf 00 00 00 00 mov $0x0,%edi %edi = &array a: R_X86_64_32 array Relocation entry e: e8 00 00 00 00 callq 13 <main+0x13> sum() f: R_X86_64_PC32 sum-0x4 Relocation entry 13: 48 83 c4 08 add $0x8,%rsp 17: c3 retq再回顾一下重定位符号引用中的算法伪代码实现
重定位符号引用
foreach section s { //对于每个节 foreach relocation entry r { //对于每个重定位条目,条目在节上 refptr = s + r.offset; /* ptr to reference to be relocated */ /* Relocate a PC-relative reference */ if (r.type == R_X86_64_PC32){ refaddr = ADDR(s) + r.offset; /* ref's run-time address */ *refptr = (unsigned) (ADDR(r.symbol) + r.addend - refaddr); } /* Relocate an absolute reference */ if (r.type ==R_X86_64_32) *refptr = (unsigned) (ADDR(r.symbol) + r.addend); } }第 1 行和第 2 行在每个节 s 以及与每个节相关联的重定位条目 r 上迭代执行
Link to original
假设每个节 s 是一个字节数组,每个重定位条目 r 是一个类型为 Elf64_Rela 的结构
假设当算法运行时,链接器已经为每个节(用 ADDR(s) 表示)和每个符号都选择了运行时地址(用 ADDR(r.symbol) 表示)
ADDR(s)表示节的地址
ADDR(r.symbol)表示符号的运行时地址
第 3 行计算的是需要被重定位的 4 字节引用的数组 s 中的地址,也就是这个符号在节中的相对位置
如果这个引用使用的是 PC 相对寻址,那么它就用第 5 ~ 9 行来重定位
如果该引用使用的是绝对寻址,它就通过第 11 ~ 13 行来重定位
a: R_X86_64_32 array Relocation entry给出了r.offset = 0xa
第 4 行中,mov 指令将 array 的地址(一个 32 位立即数值)复制到寄存器%edi 中。mov 指令开始于节偏移量 0x9 的位置,包括 1 字节操作码 Oxbf,后面跟着对 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
链接器使用算法的第 13 行修改了引用:*refptr = (unsigned) (ADDR(r.symbol) + r.addend) = (unsigned) (0x601018 + 0) = (unsigned) (0x601018)在得到的可执行目标文件中,该引用有下面的重定位形式:
4004d9: bf 18 10 60 00 mov $0x601018,%edi %edi = &arrayLink to originalNote
请注意:我们算出了*refptr = 0x601018
但是最最终得到的可执行目标文件中,它以小端序存储,也就是得到了
bf 18 10 60 00
其中bf是 1 字节操作码 Oxbf,后面就是小端序存储的运行时绝对地址