全局偏移表(GOT)和過程鏈接表(PLT)
ELF 格式的共享庫使用 PIC 技術使代碼和數(shù)據的引用與地址無關,程序可以被加載到地址空間的任意位置。PIC 在代碼中的跳轉和分支指令不使用絕對地址。PIC 在 ELF 可執(zhí)行映像的數(shù)據段中建立一個存放所有全局變量指針的全局偏移量表 GOT
對于模塊外部引用的全局變量和全局函數(shù),用 GOT 表的表項內容作為地址來間接尋址;對于本模塊內的靜態(tài)變量和靜態(tài)函數(shù),用 GOT 表的首地址作為一個基準,用相對于該基準的偏移量來引用,因為不論程序被加載到何種地址空間,模塊內的靜態(tài)變量和靜態(tài)函數(shù)與 GOT 的距離是固定的,并且在鏈接階段就可知曉其距離的大小。這樣,PIC 使用 GOT 來引用變量和函數(shù)的絕對地址,把位置獨立的引用重定向到絕對位置。
對于 PIC 代碼,代碼段內不存在重定位項,實際的重定位項只是在數(shù)據段的 GOT 表內。共享目標文件中的重定位類型有 R_386_RELATIVE、R_386_GLOB_DAT 和 R_386_JMP_SLOT,用于在動態(tài)鏈接器加載映射共享庫或者模塊運行的時候對指針類型的靜態(tài)數(shù)據、全局變量符號地址和全局函數(shù)符號地址進行重定位。
1.2 PLT 表
過程鏈接表用于把位置獨立的函數(shù)調用重定向到絕對位置。通過 PLT 動態(tài)鏈接的程序支持惰性綁定模式。每個動態(tài)鏈接的程序和共享庫都有一個 PLT,PLT 表的每一項都是一小段代碼,對應于本運行模塊要引用的一個全局函數(shù)。程序對某個函數(shù)的訪問都被調整為對 PLT 入口的訪問。
每個 PLT 入口項對應一個 GOT 項,執(zhí)行函數(shù)實際上就是跳轉到相應 GOT 項存儲的地址,該 GOT 項初始值為 PLTn項中的 push 指令地址(即 jmp 的下一條指令,所以第 1 次跳轉沒有任何作用),待符號解析完成后存放符號的真正地址。動態(tài)鏈接器在裝載映射共享庫時在 GOT 里設置 2 個特殊值:在 GOT+4( 即 GOT[1]) 設置動態(tài)庫映射信息數(shù)據結構link_map 地址;在 GOT+8(即 GOT[2])設置動態(tài)鏈接器符號解
析函數(shù)的地址_dl_runtime_resolve。
PLT 的第 1 個入口 PLT0 是一段訪問動態(tài)鏈接器的特殊代碼。程序對 PLT 入口的第 1 次訪問都轉到了 PLT0,最后跳入 GOT[2]存儲的地址執(zhí)行符號解析函數(shù)。待完成符號解析后,將符號的實際地址存入相應的 GOT 項,這樣以后調用函數(shù)時可直接跳到實際的函數(shù)地址,不必再執(zhí)行符號解析函數(shù)
操作系統(tǒng)運行程序時,首先將解釋器程序即動態(tài)鏈接器ld.so 映射到一個合適的地址,然后啟動 ld.so。ld.so 先完成自己的初始化工作,再從可執(zhí)行文件的動態(tài)庫依賴表中指定的路徑名查找所需要的庫,將其加載映射到內存。
Linux用一個全局的庫映射信息結構 struct link_map鏈表來管理和控制所有動態(tài)庫的加載,動態(tài)庫的加載過程實際上是映射庫文件到內存中,并填充庫映射信息結構添加到鏈表中的過程。結構 struct link_map 描述共享目標文件的加載映射信息,是動態(tài)鏈接器在運行時內部使用的一個結構,通過它保持對已裝載的庫和庫中符號的跟蹤。
link_map 使用雙向鏈接中間件“l_next”和“l_prev”鏈接進程中所有加載的共享庫。當動態(tài)鏈接器需要去查找符號的時候,可以向前或向后遍歷這個鏈表,通過訪問鏈表上的每一個庫去搜索需要查找的符號。Link_map 鏈表的入口由每個可執(zhí)行映像的全局偏移表的第 2 個入口(GOT[1])指向,查找符號時先從 GOT[1]讀取 link_map 結點地址,然后沿著link-map 結點進行搜索。
動態(tài)庫的加載映射過程主要分 3 步:
(1) 動態(tài)鏈接器調用 __mmap 函數(shù)對動態(tài)庫的所有PT_LOAD 可加載段進行整體映射:
l_map_start=(ElfW(Addr))__mmap ((void *)0, maplength, prot,
MAP_COPY | MAP_FILE, fd, mapoff);
返回值 l_map_start 是實際映射的虛擬地址,和段結構成員 p_vaddr 指定的虛擬地址不一定相同,這對于位置無關代碼不會產生影響。但是對于數(shù)據段和 link_map 結構中其它相關的位置描述信息還要進行修正。共享庫映射的內存位置關系如圖 1,l_addr 是實際映射地址和原來指定的映射地址的差值,用于其它位置信息的修正,即簡單地將原來指定的虛擬地址加上 l_addr 就可以得到實際加載的虛擬地址
(2)共享文件映射完畢,動態(tài)鏈接器處理共享庫的PT_DYNAMIC 動態(tài)段,將各項動態(tài)鏈接信息主要是哈希表、符號表、字符串表、重定位表、PLT 重定位項表等地址填寫到 link_map 的 l_info 數(shù)組結構中。l_info 是 link_map 最重要的字段之一,幾乎所有與動態(tài)鏈接管理相關的內容都與 l_info數(shù)組有關。動態(tài)鏈接器還要加載處理當前共享庫的所有依賴庫。
(3)由于實際的映射地址和指定的虛擬地址有可能不同,因此還要對動態(tài)庫及其依賴庫進行重定位。設置動態(tài)庫的第1 個和第 2 個 GOT 表項:
Elf32_Addr *got =
(Elf32_Addr *) lmap->l_info[DT_PLTGOT].d_un.d_ptr;
got[1]=lmap;
got[2]=&_dl_runtime_resolve;
對動態(tài)庫的所有重定位項進行重定位,在重定位項指定的偏移地址處加上修正值 l_addr。動態(tài)項 DT_REL 給出了重定位表的地址,DT_RELSZ 給出重定位表項的數(shù)目。
映射完畢后,動態(tài)鏈接器調用共享庫(包括所有相關的依賴庫)自備的初始化函數(shù)進行初始化。
程序連接表(Procedure Linkage Table)可以使被感染的文件調用外部的函數(shù)。這要比修改LD_PRELOAD環(huán)境變量實現(xiàn)調用的重定向優(yōu)越的多,首先不牽扯到環(huán)境變量的修改
程序連接表(PLT)
在ELF文件中,全局偏移表(Global Offset Table,GOT)能夠把位置無關的地址定位到絕對地址,程序連接表也有類似的作用,它能夠把位置無關的函數(shù)調用定向到絕對地址。連接編輯器(link editor)不能解決程序從一個可執(zhí)行文件或者共享庫目標到另外一個的執(zhí)行轉移。結果,連接編輯器只能把包含程序轉移控制的一些入口安排到程序連接表 (PLT)中。在system V體系中,程序連接表位于共享正文中,但是它們使用私有全局偏移表(private global offset table)中的地址。動態(tài)連接器(例如:ld-2.2.2.so)會決定目標的絕對地址并且修改全局偏移表在內存中的影象。因而,動態(tài)連接器能夠重定向這些入口,而勿需破壞程序正文的位置無關性和共享特性??蓤?zhí)行文件和共享目標文件有各自的程序連接表。
elf的動態(tài)連接庫是內存位置無關的,就是說你可以把這個庫加載到內存的任何位置都沒有影響。這就叫做position independent。在編譯內存位置無關的動態(tài)連接庫時,要給編譯器加上 -fpic選項,讓編譯器產生的目標文件是內存位置無關的還會盡量減少對變量引用時使用絕對地址。把庫編譯成內存位置無關會帶來一些花費,編譯器會保留一個寄存器來指向全局偏移量表(global offset table (or GOT for short)),這就會導致編譯器在優(yōu)化代碼時少了一個寄存器可以使用,但是在最壞的情況下這種性能的減少只有3%,在其他情況下是大大小于3%的。