我理解在典型的ELF二进制文件中,函数通过过程链接表(PLT)调用。函数的PLT条目通常包含跳转到全局偏移表(GOT)条目。该条目将首先引用一些代码将实际的函数地址加载到GOT中,并在第一次调用(延迟绑定)之后包含实际的函数地址。
确切地说,在懒惰地将GOT入口点绑定回PLT之前,先将其绑定到GOT之后的指令中。这些指令通常会跳到PLT的头上,从那里调用一些绑定例程,然后更新GOT条目。
现在我想知道为什么有两个间接的方向(调用PLT,然后从GOT跳转到一个地址),而不是仅仅保留PLT和直接从GOT调用地址。看起来这可以节省一次跳跃和完整的PLT。当然,您仍然需要一些调用绑定例程的代码,但这可以在PLT之外。
我遗漏了什么吗?额外的PLT的目的是什么?
Update:如注释中所建议的,我创建了一些(伪)代码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的替代方案中,懒散绑定之前的情况如下:(我将代码保存在“延迟绑定表”中,类似于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 | |
| | ... | |
| +------------------+ |
+------------------------+发布于 2017-03-28 19:27:38
问题是用call printf@PLT替换call [printf@GOTPLT]需要编译器知道函数printf存在于共享库中,而不是静态库中(甚至在普通的对象文件中)。链接器可以将call printf转换为call printf@PLT,将jmp printf转换为jmp printf@PLT,甚至将mov eax, printf转换为mov eax, printf@PLT,因为它所做的一切都是将基于符号printf的重新定位更改为基于符号printf@PLT的重新定位。链接器不能将call printf转换为call [printf@GOTPLT],因为它无法从重新定位中知道是调用还是JMP指令,还是其他什么东西。它不知道是否是呼叫指令,也不知道是否应该将操作码从直接呼叫更改为间接呼叫。
但是,即使有一种特殊的重定位类型表明该指令是调用,您仍然存在这样的问题:直接调用指令长5字节,而间接调用指令长6字节。编译器必须发出像nop; call printf@CALL这样的代码,以便为链接器提供插入所需的额外字节的空间,并且它必须对任何全局函数的所有调用都这样做。这可能会导致净性能损失,因为所有额外的,实际上并不是必要的NOP指令。
另一个问题是,在32位x86目标上,PLT条目在运行时被重新定位.PLT中的间接jmp [xxx@GOTPLT]指令不像直接调用和JMP指令那样使用相对寻址,而且由于xxx@GOTPLT的地址取决于图像被加载到内存中的位置,因此需要固定指令以使用正确的地址。通过将所有这些间接JMP指令组合在一个.plt部分中,意味着需要修改的虚拟内存页面的数量要小得多。修改后的每个4K页都不能再与其他进程共享,当需要修改的指令分散在内存中时,需要将图像的更大部分不共享。
请注意,后面这个问题只是共享库和在32位x86目标上定位独立可执行文件的问题。传统的可执行文件无法重新定位,因此不需要修复@GOTPLT引用,而在64位x86目标上,RIP相对寻址用于访问@GOTPLT条目。
由于最后一点,GCC (6.1或更高版本)的新版本支持-fno-plt标志。在64位x86目标上,此选项将导致编译器生成call printf@GOTPCREL[rip]指令而不是call printf指令。但是,对于没有在同一编译单元中定义的函数的任何调用,它似乎都会这样做。这是它不知道的任何函数都没有在共享库中定义。这意味着间接跳转也将用于调用在其他对象文件或静态库中定义的函数。在32位x86目标上,-fno-plt选项将被忽略,除非编译位置独立的代码(-fpic或-fpie),从而导致发出call printf@GOT[ebx]指令。除了产生不必要的间接跳转之外,这还需要为GOT指针分配寄存器,尽管大多数函数都需要分配它。
最后,Windows可以通过使用"dllimport“属性在头文件中声明符号来实现您的建议,这表明它们存在于DLL中。这样,编译器就知道是在调用函数时生成直接调用指令还是间接调用指令。这样做的缺点是,符号必须存在于DLL中,因此,如果使用此属性,则无法在编译后决定使用静态库进行链接。
还请阅读Drepper的https://www.akkadia.org/drepper/dsohowto.pdf文章,它很好地解释了这一点(对于Linux)。
发布于 2017-03-27 14:46:47
现在我想知道为什么有两个方向(打电话到PLT,然后从GOT跳到一个地址),
首先有两个调用,但只有一个间接调用(对PLT存根的调用是直接的)。
而不是仅仅保留PLT和直接从GOT打电话地址。
在不需要延迟绑定的情况下,可以使用-fno-plt来绕过PLT。
但是,如果您想保留它,您需要一些存根代码来查看符号是否已被解析并相应地分支。现在,为了方便分支预测,这个存根代码必须对每个被调用的符号和瞧重复,您重新发明了PLT。
https://stackoverflow.com/questions/43048932
复制相似问题