2017-03-27 134 views
4

我知道在典型的ELF二進制文件中,函數通過過程鏈接表(PLT)進行調用。函數的PLT條目通常包含跳轉到全局偏移表(GOT)條目。該條目將首先引用一些代碼將實際函數地址加載到GOT中,並在第一次調用(延遲綁定)後包含實際函數地址。爲什麼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 [[email protected]] |-+ | 
         | push 0xf   |<+ | 
         | jmp 0x400da0  |----+ 
         | ...    | 
         +------------------+ 

...之後延遲綁定:

Program    PLT      printf 
+---------------+  +------------------+  +-----+ 
| ...   |  | push [0x603008] | +-->| ... | 
| call j_printf |--+ | jmp [0x603010] | | +-----+ 
| ...   | | | ...    | | 
+---------------+ +-->| jmp [[email protected]] |--+ 
         | push 0xf   | 
         | jmp 0x400da0  | 
         | ...    | 
         +------------------+ 

在我的想象中的替代方案沒有PLT,結合應該是這樣的懶之前的情況:(我不停的代碼中的「延遲綁定表」類似,從PLT的一個。它也可以看不同的,我不在乎)

Program     Lazy Binding Table    printf 
+-------------------+  +------------------+    +-----+ 
| ...    |  | push [0x603008] |<-+  +-->| ... | 
| call [[email protected]] |--+ | jmp [0x603010] |--+--...--+ +-----+ 
| ...    | | | ...    | | 
+-------------------+ +-->| push 0xf   | | 
          | jmp 0x400da0  |--+ 
          | ...    | 
          +------------------+ 

現在後的延遲綁定,一個將不再使用該表:

Program     Lazy Binding Table  printf 
+-------------------+  +------------------+  +-----+ 
| ...    |  | push [0x603008] | +-->| ... | 
| call [[email protected]] |--+ | jmp [0x603010] | | +-----+ 
| ...    | | | ...    | | 
+-------------------+ | | push 0xf   | | 
         | | jmp 0x400da0  | | 
         | | ...    | | 
         | +------------------+ | 
         +------------------------+ 

回答

6

問題是將call [email protected]替換爲call [[email protected]]要求編譯器知道函數printf存在於共享庫中,而不是靜態庫(或者甚至僅在普通對象文件中)。因爲所有它做它改變基於符號printf到基於符號[email protected]搬遷安置接頭可以改變call printfcall [email protected]jmp printfjmp [email protected]甚至mov eax, printfmov eax, [email protected]。鏈接器不能將call printf更改爲call [[email protected]],因爲它不知道重定位是CALL還是JMP指令或其他內容。不知道它是否是CALL指令,它不知道它是否應該將操作碼從直接調用更改爲間接調用。

但是,即使存在一個特殊的重定位類型,表明該指令是CALL,仍然存在直接調用指令長度爲5個字節但間接調用指令長度爲6個字節的問題。編譯器必須發出如nop; call [email protected]這樣的代碼,以便鏈接器空間插入所需的附加字節,並且必須爲所有對全局函數的調用執行此操作。由於所有額外的,而不是實際必要的NOP指令,它可能最終成爲淨性能損失。

另一個問題是,在32位x86目標上,PLT條目在運行時重新定位。 PLT中的間接jmp [[email protected]]指令不像直接CALL和JMP指令那樣使用相對尋址,並且由於[email protected]的地址取決於映像在內存中的加載位置,所以需要修改指令以使用正確的地址。通過在一個.plt節中將所有這些間接JMP指令組合在一起意味着需要修改少得多的虛擬內存頁面。每個修改過的4K頁面不能再與其他進程共享,當需要修改的指令遍佈整個內存時,它需要一個非共享圖像的更大部分。

請注意,後面的問題只是32位x86目標上共享庫和位置無關的可執行文件的問題。傳統的可執行文件無法重新定位,因此無需修復@GOTPLT引用,而在64位x86目標上,則使用RIP相對尋址來訪問@GOTPLT條目。

由於最後一點GCC(6.1或更高版本)的新版本支持-fno-plt標誌。在64位x86目標上,此選項會導致編譯器生成call [email protected][rip]指令而不是call printf指令。但是,對於任何在同一個編譯單元中沒有定義的函數的調用,似乎都是這樣做的。這是它不知道的任何函數在共享庫中沒有定義。這意味着間接跳轉也可用於調用其他對象文件或靜態庫中定義的函數。在32位x86目標上,-fno-plt選項將被忽略,除非編譯位置獨立代碼(-fpic-fpie),否則會導致發出call [email protected][ebx]指令。除了產生不必要的間接跳轉之外,這也具有需要爲GOT指針分配寄存器的缺點,儘管大多數功能無論如何都需要它分配。

最後,Windows能夠通過使用「dllimport」屬性在頭文件中聲明符號來完成您的建議,表明它們存在於DLL中。這樣編譯器知道在調用函數時是否生成直接或間接的調用指令。這樣做的缺點是符號必須存在於一個DLL中,所以如果使用這個屬性,你不能在編譯之後再決定與靜態庫鏈接。

1

現在,我不知道爲什麼有兩種間接 (調用到PLT,然後跳轉到從GOT的地址),

首先有兩個電話,但只是一個間接尋址(呼叫PLT存根是直接)。

而不僅僅是保留PLT並直接從GOT呼叫地址。

如果您不需要延遲綁定,則可以使用跳過PLT的-fno-plt

但是如果你想保留它,你需要一些存根代碼來查看符號是否已被解析並相應地分支。現在,爲了便於分支預測,這個存根代碼必須爲每個被調用的符號複製,並且你重新發明了PLT。

+0

1.「_direct_」表示調用目標是靜態的,不能從內存中讀取?這當然是正確的,但除了呼叫之外,還有一個不必要的跳躍(總共一個呼叫和一個跳轉)。在現代x86上,無條件跳轉可能不是什麼大問題,但是對於所有體系結構而言,這可能並非如此,並且對代碼緩存局部性來說絕對沒有好處。 – F30

+0

2.我的「重新發明的」PLT與原來的相似,因爲它可能包含所有功能的綁定存根。但與我的重要區別在於,並非每個電話都必須從PLT轉到GOT(並返回一次)。相反,它直接進入GOT並回到「重新發明」的PLT進行第一次通話。 – F30

+1

@ F30「直接表示調用目標是靜態的,不能從內存中讀取」 - 不只是指它,這是直接調用的定義。他們肯定有他們的成本,但它(低得多)低於indirects,所以精確是很重要的。 – yugr

相關問題