在 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 槽位中的第一个
jmp
不是多余的吗?这不能通过间接调用 GOT 来实现吗?例如:
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: ....
这可能会带来边际性能优势 - 但我问的原因是链接器/elf 社区最近争相在 16 字节 PLT 插槽中提供额外字节以容纳 intel IBT(搜索失败,并导致额外的
.plt.sec
间接寻址。1, 2)
基本问题是编译器正在生成原始调用(位于 0x500),此时编译器不知道该符号最终是否会出现在该动态对象中。因此它生成一个简单的调用(直接的,相对于 PC 的),因为这对于动态对象内本地调用的常见情况是最有效的。
直到链接器运行我们才知道这是否是另一个动态对象中的符号或该对象中的全局可见符号(可能被覆盖)或本地函数调用。对于后一种情况,它只会使其成为直接调用,但对于前一种情况,它将为符号创建一个 PLT 条目,并使调用转到 PLT 符号。
您的建议将节省跳转,但需要在编译时知道每个调用是否需要 PLT 条目,或者需要根据是否需要 PLT 在链接时在直接调用和间接调用之间切换。在 x86 上,直接和间接调用的大小不同,因此能够更改将非常棘手。