Linux加載啓動可執行程序的過程(二)解釋器完成動態鏈接

接着上一篇博客。前面的工作都是在內核完成的,接下來會回到用戶空間。

第一步,解釋器(也可以叫動態鏈接器)首先檢查可執行程序所依賴的共享庫,並在需要的時候對其進行加載。

ELF 文件有一個特別的節區: .dynamic,它存放了和動態鏈接相關的很多信息,例如動態鏈接器通過它找到該文件使用的動態鏈接庫。不過,該信息並未包含動態鏈接庫的絕對路徑,但解釋器通過 LD_LIBRARY_PATH 參數可以找到(它類似 Shell 解釋器中用於查找可執行文件的 PATH 環境變量,也是通過冒號分開指定了各個存放庫函數的路徑)該變量實際上也可以通過/etc/ld.so.conf 文件來指定,一行對應一個路徑名。爲了提高查找和加載動態鏈接庫的效率,系統啓動後會通過 ldconfig 工具創建一個庫的緩存 /etc/ld.so.cache 。如果用戶通過 /etc/ld.so.conf 加入了新的庫搜索路徑或者是把新庫加到某個原有的庫目錄下,最好是執行一下 ldconfig 以便刷新緩存。

找到動態鏈接庫後,就可以將其加載到內存中。

第二步,解釋器對程序的外部引用進行重定位,並告訴程序其引用的外部變量/函數的地址,此地址位於共享庫被加載在內存的區間內。動態鏈接還有一個延遲定位的特性,即只有在“真正”需要引用符號時才重定位,這對提高程序運行效率有極大幫助。(如果設置了 LD_BIND_NOW 環境變量,這個動作就會直接進行)

下面具體說明符號重定位的過程。

首先了解幾個概念。符號,也就是可執行程序代碼段中的變量名、函數名等。重定位是將符號引用與符號定義進行鏈接的過程,對符號的引用本質是對其在內存中具體地址的引用,所以本質上來說,符號重定位要解決的是當前編譯單元如何訪問「外部」符號這個問題。動態鏈接是在程序運行時對符號進行重定位,也叫運行時重定位(而靜態鏈接則是在編譯時進行,也叫鏈接時重定位)

現代操作系統中,二進制映像的代碼段不允許被修改,而數據段能被修改。

編寫如下代碼

通過gcc編譯成.o文件後,再通過objdump-d命令得到文件的彙編指令,如下所示

call指令的操作數是fc ff ff ff,翻譯成16進制數是0xfffffffc,看成有符號是-4。這裏應該存放printf函數的地址,但由於編譯階段無法知道printf函數的地址,所以預先放一個-4在這裏。所以程序爲了正確執行,需要在鏈接時對其地址進行修正。這裏的原理對靜態鏈接和動態鏈接來說都是一樣的。

但對於動態鏈接來說,有兩個不同的地方:

(1)因爲不允許對可執行文件的代碼段進行加載時符號重定位,因此如果可執行文件引用了動態庫中的數據符號,則在該可執行文件內對符號的重定位必須在鏈接階段完成,爲做到這一點,鏈接器在構建可執行文件的時候,會在當前可執行文件的數據段裏分配出相應的空間來作爲該符號真正的內存地址,等到運行時加載動態庫後,再在動態庫中對該符號的引用進行重定位:把對該符號的引用指向可執行文件數據段裏相應的區域。

(2)ELF 文件對調用動態庫中的函數採用了所謂的"延遲綁定"(lazy binding)策略, 只有當該函數在其第一次被調用發生時才最終被確認其真正的地址,因此我們不需要在調用動態庫函數的地方直接填上假的地址,而是使用了一些跳轉地址作爲替換,這樣一來連修改動態庫和可執行程序中的相應代碼都不需要進行了,當然延遲綁定的目的不是爲了這個,具體先不細說。

可執行程序對符號的訪問又分爲模塊內和模塊間的訪問,這裏只介紹模塊間的訪問,也就是訪問動態鏈接庫中的符號。

通過gcc生成test可執行文件,然後同樣用objdump-d得到可執行文件的彙編指令,如下所示

可以看到這裏的call指令指向了80482e0地址處,也即是PLT。

PLT就是程序鏈接表(Procedure Link Table),屬於代碼段。用於把位置獨立的函數調用重定向到絕對位置。每個動態鏈接的程序和共享庫都有一個PLT,PLT表的每一項都是一小段代碼,從對應的GOT表項中讀取目標函數地址。程序對某個函數的第一次訪問都被調整爲對 PLT入口也就是PLT0的訪問,也就是說所有的PLT首次執行時,最後都會跳轉到第一個PLT中執行。PLT0是一段訪問動態鏈接器的特殊代碼,是動態鏈接做符號解析和重定位的公共入口。這樣做的好處是不用每個PLT表都有重複的一份指令,可以減少PLT指令條數。

PLT表結構如下圖所示

可以看到,PLT會先執行jmp指令跳轉到某一個地址,而這個地址就對應的GOT表項。

GOT就是全局偏移表(Global Offset Table),屬於數據段。爲了能使得代碼段裏對數據及函數的引用與具體地址無關,只能再作一層跳轉,ELF 的做法是在動態庫的數據段中加一個表項,也就是GOT 。GOT表格中放的是數據全局符號的地址,該表項在動態庫被加載後由動態加載器進行初始化,動態庫內所有對數據全局符號的訪問都到該表中來取出相應的地址,即可做到與具體地址了,而該表作爲動態庫的一部分,訪問起來與訪問模塊內的數據是一樣的。

GOT表結構如下圖所示

GOT[0]對應本ELF動態段(.dynamic段)的裝載地址,GOT[1]對應本ELF的link_map數據結構描述符地址,GOT[2]:對應_dl_runtime_resolve動態鏈接器函數的地址。3個特殊項後面依次是每個動態庫函數的GOT表項

上面講到PLT通過jmp指令跳轉到GOT表中去取函數的真實地址,而符號所對應的表項開始是沒有這個地址的,而是存放了該PLT表項jmp指令的下一條指令地址,也就是push指令。回到了PLT表項對應的指令中繼續執行,最後一條jmp指令跳轉到了PLT0中執行。

PLT0對應的指令執行了下列過程:首先pushl把 804a004(GOT[1])這塊內存裏的qword入棧,這個qword是link_map的地址,根據這個地址可以找到動態庫的符號表。然後jmp跳轉到GOT表中的第三項,找到動態鏈接器的_dl_runtime_resolve函數地址,開始執行該函數。回想前面講到的內核中加載目標映像的過程,可執行文件在Linux內核通過exeve裝載完成之後,不直接執行,而是先跳到動態鏈接器(ld-linux-XXX)執行。在ld-linux-XXX裏將link_map地址、_dl_runtime_resolve地址寫到GOT表項內。所以在此時,該GOT表項的不爲空。(前面三個GOT表項都是這樣被寫入的)然後當程序加載其它動態庫的時候,會把動態庫的符號信息插入link_map


_dl_runtime_resolve函數得到動態鏈接庫中函數的地址後(該過程以後再分析),寫回到對應的GOT表項中。

這就是函數第一次被調用時執行的過程。以後每次被調用直接從GOT表中取到函數地址就可以了。

總的來說,動態重定位的過程可以由下圖表示

部分內容和圖片參考:https://blog.csdn.net/linyt/article/category/6267121

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章