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_PC32 sum-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 - 0x4callq 指令的长度是 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 上迭代执行
假设每个 s 是一个字节数组,每个重定位条目 r 是一个类型为 Elf64_Rela 的结构
假设当算法运行时,链接器已经为每个节(用 ADDR(s) 表示)和每个符号都选择了运行时地址(用 ADDR(r.symbol) 表示)
ADDR(s) 表示 节的地址
ADDR(r.symbol) 表示 符号的运行时地址
第 3 行计算的是需要被重定位的 4 字节引用的数组 s 中的地址,也就是这个符号在节中的相对位置
如果这个引用使用的是 PC 相对寻址,那么它就用第 5 ~ 9 行来重定位
如果该引用使用的是绝对寻址,它就通过第 11 ~ 13 行来重定位

Link to original

现在,假设链接器已经确定 ADDR(s) = ADDR(.text) = 0x4004d0ADDR(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 执行以下的步骤:
1)将 PC 压入栈中
2)PC ← PC + 0x5 = 0x4004e3 + 0x5 = 0x4004e8
因此,要执行的下一条指令就是 sum 例程的第一条指令,这当然就是我们想要的!

Link to original

重定位绝对引用

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 上迭代执行
假设每个 s 是一个字节数组,每个重定位条目 r 是一个类型为 Elf64_Rela 的结构
假设当算法运行时,链接器已经为每个节(用 ADDR(s) 表示)和每个符号都选择了运行时地址(用 ADDR(r.symbol) 表示)
ADDR(s) 表示 节的地址
ADDR(r.symbol) 表示 符号的运行时地址
第 3 行计算的是需要被重定位的 4 字节引用的数组 s 中的地址,也就是这个符号在节中的相对位置
如果这个引用使用的是 PC 相对寻址,那么它就用第 5 ~ 9 行来重定位
如果该引用使用的是绝对寻址,它就通过第 11 ~ 13 行来重定位

Link to original

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 = &array

Note

请注意:我们算出了*refptr = 0x601018
但是最最终得到的可执行目标文件中,它以小端序存储,也就是得到了
bf 18 10 60 00
其中bf是 1 字节操作码 Oxbf,后面就是小端序存储的运行时绝对地址

Link to original