動态連結的步驟基本上分為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
statically linked
Q2:動态連結器本身得是PIC的嗎?
A: 是動态連結可以是PIC的也可以不是,但使用PIC往往更簡單一些,一方面是PIC可以使得代碼段共享,另一方面則是ld.so自舉過程比較複雜,如果代碼段不是PIC的,則不僅需要對.data段還需要對.code段進行重定位。