我知道在典型的ELF二进制文件中,函数通过过程链接表(PLT)被调用。函数的PLT条目通常包含一个跳转到全局偏移表(GOT)条目的指令。此条目将首先引用一些代码将实际函数地址加载到GOT中,并在第一次调用后包含实际函数地址(延迟绑定)。
确切地说,在进行延迟绑定之前,GOT条目指向PLT中的指令,跳转到GOT中的地址后。这些指令通常会跳转到PLT的开头,从那里调用一些绑定例程,然后更新GOT条目。
现在,我想知道为什么有两个间接引用(进入PLT,然后跳转到GOT中的地址),而不是直接从GOT中调用地址并省略PLT。看起来,这可以节省一个跳转和整个PLT。当然,你仍然需要一些调用绑定例程的代码,但这可以在PLT之外。
我是否遗漏了什么?额外的PLT的目的是什么?
更新: 如评论中所建议的,我创建了一些(伪)代码ASCII艺术作进一步解释。:
这就是我所理解的当前PLT方案在延迟绑定之前的情况:(PLT和printf
之间的一些间接引用由“...”表示。)
Program PLT printf
+---------------+ +------------------+ +-----+
| ... | | push [0x603008] |<---+ +-->| ... |
| call j_printf |--+ | jmp [0x603010] |----+--...--+ +-----+
| ... | | | ... | |
+---------------+ +-->| jmp [printf@GOT] |-+ |
| push 0xf |<+ |
| jmp 0x400da0 |----+
| ... |
+------------------+
...并且在进行惰性绑定之后:
Program PLT printf
+---------------+ +------------------+ +-----+
| ... | | push [0x603008] | +-->| ... |
| call j_printf |--+ | jmp [0x603010] | | +-----+
| ... | | | ... | |
+---------------+ +-->| jmp [printf@GOT] |--+
| push 0xf |
| jmp 0x400da0 |
| ... |
+------------------+
在我想象中的没有PLT的替代方案中,在延迟绑定之前的情况如下所示:(我将代码保持在“Lazy Binding Table”中,与PLT中的代码类似。它也可能看起来不同,但我不在意。)
Program Lazy Binding Table printf
+-------------------+ +------------------+ +-----+
| ... | | push [0x603008] |<-+ +-->| ... |
| call [printf@GOT] |--+ | jmp [0x603010] |--+--...--+ +-----+
| ... | | | ... | |
+-------------------+ +-->| push 0xf | |
| jmp 0x400da0 |--+
| ... |
+------------------+
现在进行了延迟绑定之后,将不再使用该表:
Program Lazy Binding Table printf
+-------------------+ +------------------+ +-----+
| ... | | push [0x603008] | +-->| ... |
| call [printf@GOT] |--+ | jmp [0x603010] | | +-----+
| ... | | | ... | |
+-------------------+ | | push 0xf | |
| | jmp 0x400da0 | |
| | ... | |
| +------------------+ |
+------------------------+
call myfunc @ GOTPCREL [rip]
变成call myfunc
,如果它确实可以直接链接到相同的库中。 (如果我没记错,它使用段覆盖前缀来填充call rel32
以填充6字节插槽)。 - Peter Cordes-fPIE
或非 PIE 可执行文件来说并不会发生,只有使用-fPIC
时才会发生。(相关:有选项可设置默认符号可见性来控制是否需要假定符号重定位。) - Peter Cordes