[英]Why does the PLT exist in addition to the GOT, instead of just using the GOT?
我了解在典型的ELF二進制文件中,通過過程鏈接表(PLT)調用函數。 函數的PLT條目通常包含到全局偏移表(GOT)條目的跳轉。 該條目將首先引用一些代碼以將實際功能地址加載到GOT中,並在首次調用(延遲綁定)之后包含實際功能地址。
確切地說,在延遲綁定GOT條目之前,請先將其指向PLT,再跳轉至GOT之后的說明。 這些指令通常會跳到PLT的開頭,從那里調用一些綁定例程,然后將更新GOT條目。
現在,我想知道為什么有兩種間接方式(調用PLT,然后從GOT跳轉到地址),而不是僅僅保留PLT並直接從GOT調用地址。 看起來這可以節省跳轉和完整的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 printf@PLT
call [printf@GOTPLT]
代替call printf@PLT
要求編譯器知道函數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]
因為它無法call [printf@GOTPLT]
定位中得知它是CALL還是JMP指令或完全是其他東西。 不知道它是否是CALL指令,就不知道是否應該將操作碼從直接CALL更改為間接CALL。
但是,即使存在指示該指令為CALL的特殊重定位類型,您仍然有一個問題,即直接調用指令的長度為5個字節,而間接調用指令的長度為6個字節。 編譯器將不得不發出類似nop; call printf@CALL
代碼nop; call printf@CALL
nop; call printf@CALL
給鏈接器空間插入所需的附加字節,並且對於任何對全局函數的所有調用都必須這樣做。 由於所有額外且實際上不是必需的NOP指令,最終可能會導致凈性能下降。
另一個問題是,在32位x86目標上,PLT條目在運行時被重定位。 PLT中的間接jmp [xxx@GOTPLT]
指令不使用直接CALL和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目標上,除非編譯與位置無關的代碼( -fpic
或-fpie
),否則它會導致call printf@GOT[ebx]
指令,否則-fno-plt
選項將被忽略。 除了產生不必要的間接跳轉之外,這還具有需要為GOT指針分配寄存器的缺點,盡管大多數功能無論如何都需要分配它。
最后,Windows可以通過在帶有“ dllimport”屬性的頭文件中聲明符號來表明您的建議,這些符號表明它們存在於DLL中。 這樣,編譯器知道在調用函數時是否生成直接或間接調用指令。 這樣做的缺點是符號必須存在於DLL中,因此,如果使用了此屬性,則無法在編譯后決定與靜態庫鏈接。
另請參閱Drepper的“ 如何編寫共享庫文件”,它在細節上對此進行了很好的解釋(對於Linux)。
現在,我想知道為什么有兩種間接方式(調用PLT,然后從GOT跳轉到一個地址),
首先,有兩個調用 ,但是只有一個間接調用(對PLT存根的調用是直接的 )。
而不只是保留PLT並直接從GOT調用地址。
如果不需要惰性綁定,可以使用-fno-plt
繞過PLT。
但是,如果要保留它,則需要一些存根代碼以查看符號是否已解析並相應地分支。 現在,為了促進分支預測,必須為每個被調用的符號和voila復制此存根代碼,您重新發明了PLT。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.