天天看點

《程式員的自我修養》學習筆記(九)————動态連結(4):動态連結的步驟和實作1.動态連結器自舉2. 裝載共享對象3.符号的優先級4.重定位和初始化5.Linux動态連結器實作

         動态連結的步驟基本上分為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檔案解決共享對象依賴問題的,如果它也依賴于其它共享對象,那麼誰來幫它解決依賴問題?是以它本身必須不依賴于其它共享對象。

繼續閱讀