为什么需要PLT存根开头的“jmp”?

Why is the `jmp` at the start of the PLT stub needed?

提问人:Ofek Shilon 提问时间:8/27/2023 最后编辑:Peter CordesOfek Shilon 更新时间:8/28/2023 访问量:81

问:

在 SystemV ABI 中指定 PLT 用法(并在实践中实现)的方式是示意性的,如下所示:

# A call from somewhere in code is into a PLT slot
# (In reality not a direct call, in x64 typically an rip-relative one)
0x500:   
          call 0x1000   
...

0x1000:
   .PLT1: jmp [0x2000]  # the slot for f in the binary's GOT
          pushq $index_f
          jmp .PLT0
...
0x2000: 
# initially jumps back to .PLT to call the lazy-binding routine:
   .GOT1: 0x1005
# but after that is called:
          0x3000   # the address of the real implementation of f
...
0x3000:
     f:  ....

我的问题是:

PLT 插槽中的第 1 个不是多余的吗?这不能通过间接调用 GOT 来代替吗?例如:jmp

0x500:   
          call [0x2000]
...

0x1000:
   .PLT1: pushq $index_f
          jmp .PLT0
...
0x2000: 
# initially jumps back to .PLT to call the lazy-binding routine:
   .GOT1: 0x1005
# but after that is called:
          0x3000   # the address of the real implementation of f
...
0x3000:
     f:  ....

这可能会带来边际性能优势 - 但我问的原因是链接器/精灵社区最近在 16 字节的 PLT 插槽中提出额外的字节以容纳英特尔 IBT(搜索失败,并导致额外的间接。12.plt.sec)

Assembly linker elf dynamic-linking got

评论

0赞 Jester 8/27/2023
您必须跳转到实际函数,而不是调用它。但是,如果解析器查看返回地址以确定它是哪个函数,则可以将解析 + 替换为 a。pushjmpcall
0赞 Ofek Shilon 8/27/2023
@Jester (1) JMP目标不是等价的吗?(2) 不能替换为 ,因为在解析后解析器会调用,并且您希望它返回到原始调用站点。call+jmpcallpush+jmpcallfret
0赞 Jester 8/27/2023
1) 在原始调用方中,PLT 应该只是 2) 如果解析器弹出返回地址并使用它来确定它是哪个函数,则可以。此外,解析器也不会调用,它会跳转到它(或者如果它调用了,那么它会在之后调用)。calljmpfret
0赞 Ofek Shilon 8/27/2023
@Jester 请注意,在我的假设方案中,调用是对 GOT 中地址的间接调用,而不是对 PLT 的调用。我仍然不明白为什么需要jmp。
1赞 Ofek Shilon 8/27/2023
@Jester我也这么认为,但在 SO (stackoverflow.com/questions/76243294/...) 上了解到今天不需要这样做。当 func 地址被占用时,函数会提前绑定,并且代码会从(已解析的)got 插槽中获取该地址。(我认为那里的 SystemV ABI 规范已经过时了)

答:

2赞 Chris Dodd 8/28/2023 #1

基本问题是编译器正在生成原始调用(在 0x500 处),此时,编译器不知道此符号最终是否会出现在此动态对象中。因此,它会生成一个简单的调用(直接的、相对于 PC)的调用,因为对于动态对象中本地调用的常见情况来说,这是最有效的。

直到链接器运行,我们才知道这是另一个 dynmic 对象中的符号,还是此对象中的全局可见符号(可能被覆盖)或本地函数调用。对于后一种情况,它只会使其成为直接调用,但对于前一种情况,它将为符号创建一个 PLT 条目,并使调用转到 PLT 条目。

您的建议将节省跳转,但需要在编译时知道每次调用是否需要 PLT 条目,或者需要在链接时根据是否需要 PLT 在直接调用和间接调用之间切换。在 x86 上,直接调用和间接调用的大小不同,因此能够更改将非常棘手。

评论

0赞 Ofek Shilon 8/28/2023
默认情况下,即使对于同一库中的函数,也会通过 PLT 生成来自共享库的调用。具有“默认”可见性的符号是可插接的,并且插补只能在运行时发生。
1赞 Peter Cordes 8/28/2023
gcc -fno-plt将有同样的问题(对于在链接器输入中找到的符号的不必要的间接)。它通过“放松”以使用虚拟地址大小前缀定向呼叫来解决,该前缀对其执行方式没有影响。(但是,指令需要在机器代码中占用相同的空间而不插入 .对于“轻松”的呼叫,有一种特殊的重新定位类型。(示例:无法从汇编 (yasm) 代码在 64 位 Linux 上调用 C 标准库函数 - NASM 使用非宽松 :/)call [rip+rel32]a32 call rel32nop
1赞 Peter Cordes 8/28/2023
@OfekShilon:但是大多数时候你不希望这样,甚至在使用 或 with 编译时(即使默认情况下 GCC 也会直接调用其他函数),GCC 也不知道未定义的符号是在另一个对象中找到还是仅在共享对象中找到。GCC 通过让链接器重写调用以在需要时通过 PLT 来处理此问题(传统),或者让链接器放松到 ( without , or visibility=hidden)。或者看看我之前的评论:放松.-fPIE-fno-pie -no-pie.o.so-fno-piecall foo@pltcall foo-fPIE-fno-pltcall [rip+rel32]