You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何PLT存根开头的`jmp`指令是必需的?

关于SystemV ABI中PLT插槽第一条jmp指令的疑问

标准PLT实现(SystemV ABI规定)

# 代码段中的调用指向PLT插槽
# 实际是x64的RIP相对调用,并非直接call
0x500:   
          call 0x1000   
...

0x1000:
   .PLT1: jmp [0x2000]  # 跳转到对应函数的GOT槽位
          pushq $index_f
          jmp .PLT0
...
0x2000: 
# 初始状态下,GOT槽位指向PLT中push指令的地址(0x1005),触发延迟绑定
   .GOT1: 0x1005
# 延迟绑定完成后,GOT槽位被更新为函数f的真实地址
          0x3000   # 函数f的实际入口
...
0x3000:
     f:  ....

疑问:能否去掉PLT1的第一条jmp,改用直接间接调用GOT?

你提出的修改方案如下:

0x500:   
          call [0x2000]
...

0x1000:
   .PLT1: pushq $index_f
          jmp .PLT0
...
0x2000: 
# 初始指向PLT的push指令地址
   .GOT1: 0x1005
# 绑定后指向函数f的真实地址
          0x3000   # 函数f的实际入口
...
0x3000:
     f:  ....

从表面流程看,这个修改似乎能正常工作:第一次调用时,call [0x2000]会跳转到PLT1的pushq指令触发延迟绑定;绑定完成后,后续调用会直接跳到函数f的真实地址,还能节省一条jmp指令的开销。但现有实现不这么做,甚至社区为适配Intel IBT(间接分支追踪)新增.plt.sec间接层也没采用该方案,核心原因有以下几点:

1. ABI兼容性与生态依赖

SystemV ABI明确规定了PLT的结构,现有动态链接器、调试器、性能分析工具等全栈生态都基于这个标准实现。如果改为直接调用GOT,所有依赖PLT插槽结构的工具都会失效——比如调试器无法识别PLT调用栈、动态链接器的延迟绑定逻辑需要重写,会导致大量现有软件和工具无法兼容。

2. 延迟绑定的栈帧约定

动态链接器的延迟绑定例程(_dl_runtime_resolve)依赖固定的栈帧结构:原PLT流程中,call 0x1000会将调用处的返回地址压入栈,随后jmp [GOT]跳回PLT1的pushq指令,此时栈上的结构是「调用处返回地址 → 函数索引」,这是_dl_runtime_resolve预期的输入格式。虽然你的修改方案栈帧结构看似一致,但现有动态链接器的实现是基于原PLT的代码位置设计的,贸然修改可能触发未定义行为。

3. 代码重定位与PIE兼容性

在位置无关可执行(PIE)模式下,PLT代码本身是位置无关的,所有寻址都通过RIP相对偏移实现。原PLT的设计让PLT插槽成为调用的统一入口,避免了在代码段中直接生成大量指向GOT的重定位项,减少了链接器的工作量,同时提升了加载效率。如果改为直接调用GOT,代码段中每个调用点都需要生成独立的重定位项,会增加二进制体积和加载时间。

4. 调试与栈回溯的可读性

原PLT插槽是明确的中间层,调试器和性能分析工具可以识别PLT调用,在栈回溯时显示「调用处 → PLT插槽 → 目标函数」的完整流程,方便开发者定位动态链接函数的调用问题。如果直接调用GOT,栈回溯会直接从调用处跳到目标函数,丢失了PLT这个关键的中间信息,增加了调试难度。

关于Intel IBT适配的背景

近期社区尝试在16字节的PLT插槽中挤出空间添加endbr64指令(IBT要求的间接分支前置指令),但原PLT结构的指令长度总和已经接近16字节,无法兼容对齐和指令长度限制,最终只能新增.plt.sec间接层——这本质上是在兼容现有ABI的前提下做的妥协,而非想不到直接调用GOT的方案,核心还是要维护生态的兼容性。


内容的提问来源于stack exchange,提问作者Ofek Shilon

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 15:53:11