Linux動態鏈接之四:動態鏈接的步驟

動態鏈接的步驟基本上分爲3步:
1.啓動動態鏈接器本身
2.裝載所需要的共享對象
3.重定位和初始化

1. 動態鏈接其ld.so自舉

動態鏈接器入口地址即是自舉代碼的入口,當OS將進程控制權交給動態鏈接器時,動態鏈接器的自舉代碼開始執行。自舉代碼執行邏輯:
1.找到動態鏈接器自己的GOT段,GOT中第一項即是.dynamic段的偏移地址,然後找到該動態鏈接器本身的.dynamic段;
2.通過.dynamic段中的信息,自舉代碼可以獲得動態鏈接器本身的重定位表和符號表等,即有.rel.dyn和.rel.plt,其中.rel.dyn是對數據引用的修正,修正的位置位於.got以及數據段,而.rel.plt(延遲綁定)是對函數引用的修正,所修正的位置位於.got.plt(考慮到動態鏈接器不和外部模塊交互,故plt機制對動態鏈接器不存在);找到這些重定位信息後,進行重定位,然後就可以將這些全局變量和靜態變量全部定位;
3.定位之後,動態鏈接器代碼便可以開始自由使用自己的全局變量和靜態變量。

動態鏈接器作爲第一個被加載進虛擬空間的共享對象,顯然不能依靠其他共享對象,此外由於動態鏈接器本身所需要的全局和靜態變量的重定位是需要自身獨立完成,這顯然需要動態鏈接器的啓動初始話部分要極爲精巧,這種不依賴外部對象完成啓動(如同電腦啓動的系統啓動程序)的啓動代碼都被稱爲自舉。

2. 裝載所需要的共享對象

這裏的共享對象是鏈式加載的,主要是根據主程序文件的.got.plt表中的.dynamic段中DT_NEEDED表項指示的依賴對象元素的文件名,依次加載,顯然新加載的共享對象還可能依賴別的對象,這便是涉及到廣度優先加載還是深度優先加載的選擇問題,一般常見的是採用廣度優先加載。動態鏈接器圖遍歷裝載ELF可執行文件需要的所有的.so對象,並每裝載一個新的.so,則將該.so對象名下的符號表合併到全局符號表。

既然有了符號表合併,顯然[符號決議]需要考慮了(可見性下推鏈),這便是全局符號表中的符號優先級問題。

全局符號介入(Global Symbol Interpose):一個共享對象裏面的全局符號被另一個共享對象的同名全局符號覆蓋的現象。這是因爲Linux下的ld.so是這樣處理全局符號表合併的:當一個符號需要被加入全局符號表時,如果相同的符號名已經存在,則後加入的符號被忽略。共享對象.so的裝載順序爲廣度優先。

由於存在這種重名符號被直接忽略的問題,當程序使用大量共享對象時應該非常小心符號的重名問題,如果兩個符號重名又執行不同的功能,那麼程序運行時可能會將所有該符號名的引用解析到第一個被加入全局符號表的使用該符號名的符號,從而導致程序莫名其妙的錯誤。

這樣需要介紹到其實模塊內部的全局函數的引用要和前面說過的全局變量一樣不能採用內部相對地址引用,全部採用.got.plt的GOT跳轉。這是因爲如果要允許全局符號的覆蓋,那麼假設模塊A內部存在全局bar()函數,模塊B也存在一個功能不一樣的bar()函數,並且先加載了模塊B,這時全局符號表中bar()函數符號唯一起效的便是模塊B中B::bar(),而若模塊A內存在對bar()函數的引用,這時如果採用相對地址引用則要麼是指引到A::bar(),這並不符合我們的設計預期,因爲我們希望在整個程序中只有B::bar()起效,要麼將模塊A中對全局函數bar()的引用改成模塊B中B::bar(),而這隻能採用GOT表跳轉實現。

因爲很多模塊都是默認函數是全局的外部可見的,這樣便會導致內部函數引用時存在一步不必要的GOT表跳轉,如果要加速函數調用效率,那麼這便是關鍵字static的作用,static顯示該文件內部的bar()函數爲文件內部可見且不被外部覆蓋,讓調用邏輯更爲清晰具體化,這時便可以採用近址調用的方式來實現函數調用,加快函數調用速度。

3.重定位和初始化

所有所需的共享對象.so一次性被裝進進程空間後,則開始一次性的重定位和初始化。鏈接器在完成共享對象裝載後,開始重新遍歷可執行文件和每個共享對象的重定位表,開始對應的重定位工作。參考此前GOT/PLT重定位操作,並且這時已經有了所有模塊的全局符號表合集,故而修正過程顯然不需要再次遍歷各個模塊以尋找各目標符號的地址了。

重定位完成後,這是如果某個共享對象存在.init初始化段,則動態鏈接器會依次執行各共享對象的.init段代碼,比如C++全局共享對象的初始化,相應的,在主程序結束後,還需要執行.finit段代碼完成自定義的清場動作。具體的過程,會額外通過一篇文章介紹。

而主程序文件的.init段和.finit段並非由動態鏈接器完成,而是有運行庫來實現封裝。當所有的初始化工作完成後,開始將控制權轉移到程序入口(運行庫的初始入口函數)。

幾個非常有意思的問題
Q1: 動態鏈接器本身是動態鏈接的還是靜態鏈接的?
A: 靜態鏈接的,因爲動態鏈接器得自舉,所以如果動態鏈接器也是動態鏈接的,那顯然是各死循環。

$ldd  /lib/ld-linux.so.2
   statically linked

Q2:動態鏈接器本身得是PIC的嗎?
A: 是動態鏈接可以是PIC的也可以不是,但使用PIC往往更簡單一些,一方面是PIC可以使得代碼段共享,另一方面則是ld.so自舉過程比較複雜,如果代碼段不是PIC的,則不僅需要對.data段還需要對.code段進行重定位。

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