《程序員的自我修養》學習筆記(九)————動態鏈接(4):動態鏈接的步驟和實現

         動態鏈接的步驟基本上分爲3步:先是啓動動態鏈接器本身,然後裝載所有需要的共享對象,最後是重定位和初始化

1.動態鏈接器自舉

        動態鏈接器本身也是一個共享對象,但是事實上它有一些特殊性。對於普通共享對象文件來說,它的重定位工作由動態鏈接器來完成;它也可以依賴於其它共享對象,其中的被依賴的共享對象由動態鏈接器負責鏈接和裝載。動態鏈接器的特殊性:首先是,動態鏈接器本身不可以依賴於其它任何共享對象;其次是動態鏈接器本身所需要的全局和靜態變量的重定位工作由它本身完成。對於第一個條件我們可以人爲地控制,在編寫動態鏈接器時保證不使用任何系統庫、運行庫;對於第二個條件,動態鏈接器必須在啓動時有一段非常精巧的代碼可以完成這項工作而同時又不能用到全局和靜態變量。這種具有一定限制條件的啓動代碼往往被稱爲自舉(Bootstrap)。
        動態鏈接器入口地址即是自舉代碼的入口,當操作系統將進程控制權交給動態鏈接器時,動態鏈接器的自舉代碼即開始執行。自舉代碼首先會找到它自己的GOT。而GOT的第一個入口保存的即是”.dynamic”段的偏移地址,由此找到了動態鏈接器本身的”.dynamic”段。通過”.dynamic”中的信息,自舉代碼便可以獲得動態鏈接器本身的重定位表和符號表等,從而得到動態鏈接器本身的重定位入口,先將它們全部重定位。從這一步開始,動態鏈接器代碼中才可以開始使用自己的全局變量和靜態變量。實際上在動態鏈接器的自舉代碼中,除了不可以使用全局變量和靜態變量之外,甚至不能調用函數,即動態鏈接器本身的函數也不能調用。

2. 裝載共享對象

        完成基本自舉以後,動態鏈接器將可執行文件和鏈接器本身的符號表都合併到一個符號表當中,我們可以稱它爲全局符號表(Global Symbol Table)然後鏈接器開始尋找可執行文件所依賴的共享對象,”.dynamic”段中,有一種類型的入口是DT_NEEDED,它所指出的是該可執行文件(或共享對象)所依賴的共享對象。由此,鏈接器可以列出可執行文件所需要的所有共享對象,並將這些共享對象的名字放入到一個裝載集合中。然後鏈接器開始從集合裏取一個所需要的共享對象的名字,找到相應的文件後打開該文件,讀取相應的ELF文件頭和”.dynamic”段,然後將它相應的代碼段和數據段映射到進程空間中。如果這個ELF共享對象還依賴於其它共享對象,那麼將所依賴的共享對象的名字放到裝載集合中。如此循環直到所有依賴的共享對象都被裝載進來爲止,當然鏈接器可以有不同的裝載順序,如果我們把依賴關係看做一個圖的話,那麼這個裝載過程就是一個圖的遍歷過程,鏈接器可能會使用深度優先或者廣度優先或者其它的順序來遍歷整個圖,這取決於鏈接器,比較常見的算法一般都是廣度優先的。當一個新的共享對象被裝載進來的時候,它的符號表會被合併到全局符號表中,所以當所有的共享對象都被裝載進來的時候,全局符號表裏面將包含進程中所有的動態鏈接所需要的符號。

3.符號的優先級

         一個共享對象裏面的全局符號被另一個共享對象的同名全局符號覆蓋的現象又被稱爲共享對象的全局符號介入(Global Symbol Interpose)。關於全局符號介入這個問題,實際上Linux下的動態鏈接器是這樣處理的:它定義了一個規則,那就是當一個符號需要被加入全局符號表時,如果相同的符號名已經存在,則後加入的符號被忽略。由於存在這種重名符號被直接忽略的問題,當程序使用大量共享對象時應該非常小心符號的重名問題,如果兩個符號重名又執行不同的功能,那麼程序運行時可能會將所有該符號名的引用解析到第一個被加入全局符號表的使用該符號名的符號,從而導致程序莫名其妙的錯誤。

4.重定位和初始化

         當所有依賴的共享對象被裝載進來以後,鏈接器開始重新遍歷可執行文件和每個共享對象的重定位表,將它們的GOT/PLT中的每個須要重定位的位置進行修正。因爲此時動態鏈接器已經擁有了進程的全局符號表,所以這個修正過程也顯得比較容易,跟前面提到的地址重定位的原理基本相同。重定位完成之後,如果某個共享對象有”.init”段,那麼動態鏈接器會執行”.init”段中的代碼,用以實現共享對象特有的初始化過程,比如最常見的,共享對象中的C++的全局/靜態對象的構造就需要通過”.init”來初始化。相應地,共享對象中還可能有”.finit”段,當進程退出時會執行”.finit”段中的代碼,可以用來實現類似C++全局對象析構之類的操作。如果進行的可執行文件也有”.init”段,那麼動態鏈接器不會執行它,因爲可執行文件中的”.init”段和”.finit”段由程序初始化部分代碼負責執行。當完成了重定位和初始化之後,所有的準備工作就宣告完成了,所需要的共享對象也都已經裝載並且鏈接完成了,這時候動態鏈接器就如釋重負,將進程的控制權轉交給程序的入口並且開始執行。

5.Linux動態鏈接器實現

        內核在裝載完ELF可執行文件以後就返回到用戶空間,將控制權交給程序的入口。對於不同鏈接形式的ELF可執行文件,這個程序的入口是有區別的。對於靜態鏈接的可執行文件來說,程序的入口就是ELF文件頭裏面的e_entry指定的入口;對於動態鏈接的可執行文件來說,如果這時候把控制權交給e_entry指定的入口地址,那麼肯定是不行的,因爲可執行文件所依賴的共享庫還沒有被裝載,也沒有進行動態鏈接。所以對於動態鏈接的可執行文件,內核會分析它的動態鏈接器地址(在”.interp”段),將動態鏈接器映射至進程地址空間,然後把控制權交給動態鏈接器。
        在Linux下,可執行文件所需要的動態鏈接器的路徑幾乎都是”/lib/ld-linux.so.2”,其它的*nix操作系統可能會有不同的路徑。在Linux的系統中,/lib/ld-linux.so.2通常是一個軟鏈接。動態鏈接器是個非常特殊的共享對象,它不僅是個共享對象,還是個可執行的程序,可以直接在命令行下面運行。共享庫和可執行文件實際上沒什麼區別,除了文件頭的標誌位和擴展名有所不同之外,其它都是一樣的。Windows系統中的EXE和DLL也是類似的區別,DLL也可以被當作程序來運行,Windows提供了一個叫做rundll32.exe的工具可以把一個DLL當作可執行文件運行。
        動態鏈接器本身應該是靜態鏈接的,它不能依賴於其它共享對象,動態鏈接器本身是用來幫助其它ELF文件解決共享對象依賴問題的,如果它也依賴於其它共享對象,那麼誰來幫它解決依賴問題?所以它本身必須不依賴於其它共享對象。
 

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