因為靜態(tài)鏈接有缺點(diǎn):
1、靜態(tài)鏈接浪費(fèi)計算機(jī)內(nèi)存和磁盤空間。因為同一個庫的目標(biāo)文件會在不同的模塊中留有多個相同的副本。
2、只要某一模塊有更改,那么所有目標(biāo)文件就必須再靜態(tài)鏈接一次。
動態(tài)鏈接的基本思想是把程序各模塊彼此分隔開來形成獨(dú)立的文件,并把鏈接的過程推遲到運(yùn)行時再進(jìn)行。
動態(tài)鏈接解決了缺點(diǎn)1,因為它實(shí)現(xiàn)了目標(biāo)文件的共享,與此同時,這樣做還減少了物理頁面的換進(jìn)換出,增加了緩存的命中率。
它也解決了缺點(diǎn)2,因為個目標(biāo)文件彼此獨(dú)立,如果有更改只需要換掉那個發(fā)生更改的目標(biāo)文件即可。同時,它加強(qiáng)了模塊間的獨(dú)立性,是各模塊可以用不同的編程語言實(shí)現(xiàn),這個非常好!另外,開發(fā)、測試和維護(hù)也變得容易了。
插件是動態(tài)鏈接可擴(kuò)展性優(yōu)點(diǎn)的體現(xiàn)。
程序可以動態(tài)地加載合適的運(yùn)行庫以適應(yīng)不同的運(yùn)行環(huán)境,這是動態(tài)鏈接兼容性的體現(xiàn)。
當(dāng)然它也有缺點(diǎn),比如說早期Windows中出現(xiàn)過的DLL
Hell。
動態(tài)鏈接必須要有動態(tài)鏈接文件或者也可以叫做動態(tài)鏈接庫,程序和動態(tài)鏈接文件之間是通過動態(tài)鏈接器鏈接在一起的。
動態(tài)鏈接把鏈接過程從裝載前推遲到裝載時,這比靜態(tài)鏈接慢,性能會下降,但是這點(diǎn)性能的下降可以換來空間的節(jié)省和程序靈活性的提升,所以它是值得的。另外,動態(tài)鏈接過程也是可以優(yōu)化的。
在動態(tài)鏈接過程中,符號的引用被標(biāo)記為動態(tài),暫時不進(jìn)行重定位,這個過程留待裝載時進(jìn)行。
由靜態(tài)鏈接產(chǎn)生的可執(zhí)行文件只有一個文件需要映射到虛擬地址空間中去,該文件就是可執(zhí)行文件本身。而動態(tài)鏈接卻不一樣,它除了可執(zhí)行文件本身外,還包括由動態(tài)鏈接庫生成的共享目標(biāo)文件。還有動態(tài)鏈接器也會被映射到虛擬地址空間中。
共享對象的最終裝載地址在編譯時是不確定的,裝載器會根據(jù)虛擬地址空間的實(shí)際情況在裝載時給共享對象分配一塊合適的空間。
共享對象在裝載時如何確定其在虛擬內(nèi)存中的地址?
它不能在編譯時確定自己在進(jìn)程虛擬地址空間中的位置。
就是說動態(tài)鏈接文件可以被裝在到虛擬內(nèi)存空間中的任意位置上去,操作系統(tǒng)會根據(jù)實(shí)際的內(nèi)存情況來確定共享目標(biāo)文件的裝載位置,這個就叫做裝載時重定位。
雖說共享目標(biāo)文件在虛擬內(nèi)存中只有一份,但是由于各進(jìn)程是彼此獨(dú)立的,它們各自的操作也是彼此獨(dú)立的,所有它們對共享目標(biāo)文件中可修改數(shù)據(jù)部分的動作可能不一致,所以實(shí)際上共享目標(biāo)文件在各進(jìn)程中都有副本。
上面提到的副本是把動態(tài)鏈接文件的指令和數(shù)據(jù)部分都復(fù)制了一下,仔細(xì)想想這個完全沒有必要。因為指令是只讀的,數(shù)據(jù)是可讀可寫的,所以指令完全沒有必要也復(fù)制,當(dāng)然這里說的指令是指只讀的,其他的都算作數(shù)據(jù)好了。
所以要想做到只讓數(shù)據(jù)被復(fù)制,只需要把指令和數(shù)據(jù)分離開來就好了。只把數(shù)據(jù)的部分復(fù)制到各進(jìn)程中去可以解決共享對象中對絕對地址的重定位問題,這種技術(shù)叫地址無關(guān)代碼技術(shù)。
這種技術(shù)是通過把地址引用計數(shù)分成4種方式分別處理的,劃分的標(biāo)準(zhǔn)為模塊內(nèi)外和指令還是數(shù)據(jù)引用。
類型一模塊內(nèi)部調(diào)用或跳轉(zhuǎn)
這種情況無需重定位,因為都是模塊內(nèi)部與絕對地址無關(guān),只需要相對地址即可。
類型二模塊內(nèi)部數(shù)據(jù)訪問
同樣很簡單,雖然絕對地址是變動的,但是相對地址還是固定的,所以你只要得到一條指令的絕對地址,其他的也就知道了。
那么如何獲取某條指令的絕對地址就是一個問題了,而絕對地址存放在PC中,所以問題就轉(zhuǎn)變?yōu)槿绾潍@取PC值。
PC(Program Counter):程序計數(shù)器,是一個16位的計數(shù)器。用于存放和指示下一條要執(zhí)行的指令的地址。尋址范圍達(dá)216。PC有自動加1功能,以實(shí)現(xiàn)程序的順序執(zhí)行。PC沒有地址,是不可尋址的,無法用指令對它進(jìn)行讀寫。但在執(zhí)行轉(zhuǎn)移、調(diào)用、返回等指令時能自動改變其內(nèi)容,以改變程序的執(zhí)行順序。
關(guān)于這一點(diǎn),書中說得不直接。
書中這段匯編代碼與8086CPU的不太一樣,看起來有些費(fèi)勁。

書中講述的其實(shí)是黑體這一段,但是還涉及到紅色箭頭所指這一段。程序首先調(diào)用494處的函數(shù)把棧頂指針的內(nèi)容,而這個內(nèi)容就是執(zhí)行call之前的主調(diào)函數(shù)的發(fā)出調(diào)用語句的地址,存儲到ecx寄存器中,然后返回。說得簡單一點(diǎn)就是它用了call的性質(zhì)把要用到的絕對地址保存起來而已。
類型三模塊間的數(shù)據(jù)訪問
由于模塊間的目標(biāo)地址只有等到裝載時才能確定,所以ELF在自己的數(shù)據(jù)段中建立了一個指針數(shù)組,用來指向其他模塊的全局變量。這個指針數(shù)組叫做全局偏移表(Global
Offset Table,GOT)。
查找目標(biāo)地址的過程是先查找GOT,然后根究GOT表項查找目標(biāo)地址。
GOT中的各項數(shù)據(jù)是連接器在鏈接的時候查找各目標(biāo)地址后填充的。
GOT存放在數(shù)據(jù)段,可以被修改,每個進(jìn)程都有一個副本。
要想獲得目標(biāo)地址就必須首先獲得GOT的地址,GOT地址的獲取也是根據(jù)PC值再加上GOT相對于當(dāng)前指令的偏移量得到的,這個和類型二中的方法相同。得到GOT的絕對地址以后,再加上目標(biāo)地址相對于GOT的偏移量就可以得到目標(biāo)地址。
類型四模塊間調(diào)用,跳轉(zhuǎn)
類似于類型三的方法,只不過這回從變量變成了函數(shù)。
DSO(Dynamic Shared Object):動態(tài)共享目標(biāo)文件。
如果一個DSO經(jīng)過下面語句后,在TEXTREL(代碼段重定位地址表)中有輸出,那就代表該DSO不是PIC(地址無關(guān)代碼)的,因為真正的PICDSO是沒有TEXTREL的。
PIE(Position Independent Executable,地址無關(guān)可執(zhí)行文件):以地址無關(guān)代碼方式編譯的可執(zhí)行文件。
如何處理定義在模塊內(nèi)部的全局變量?
當(dāng)一個模塊A引用了一個定義在其他模塊B的全局變量C的時候。編譯器無法判斷C是在A中的其他目標(biāo)文件還是在另外一個共享對象之中,即無法判斷是否為模塊間調(diào)用。
解決方案:程序主模塊的代碼并不是地址無關(guān)的,可執(zhí)行文件在運(yùn)行時不進(jìn)行代碼重定位,那么全局變量地址的確定就應(yīng)該是在鏈接時完成的。在鏈接的過程中,鏈接器會在可執(zhí)行文件的BSS段中創(chuàng)建一個C的副本。這樣,同一個全局變量C在共享對象和可執(zhí)行文件中都有一個副本,那么程序在實(shí)際執(zhí)行過程中也不知道該用哪個,這會導(dǎo)致程序執(zhí)行失敗。
既然是歧義,那就消除歧義。就是說讓所有用C的地方都使用BSS中的C,可使用GOT方式實(shí)現(xiàn)之。
以上是針對代碼段而言的,本節(jié)來談?wù)剶?shù)據(jù)段的情況。
變量的地址會隨著共享對象的裝載而確定,但是共享對象的裝載地址是不確定的,所以變量的地址也是不確定的。
可用裝載重定位的方法解決此問題。
代碼段也可以使用裝載時重定位而不使用代碼無關(guān)技術(shù),但是此時它就不能被多進(jìn)程共享,也失去了節(jié)省空間的優(yōu)點(diǎn),但是它運(yùn)行速度快了。
延遲綁定,即函數(shù)被用到時才綁定,而不是說在程序開始執(zhí)行時就對所有的函數(shù)等進(jìn)行符號的解析和重定位。這樣做可以提升動態(tài)鏈接的速度。
PLT(Procedure Linkage Table):假設(shè)某共享目標(biāo)文件A要調(diào)用函數(shù)B,A必須要綁定函數(shù)B的地址,如何做到這一點(diǎn)?必須要有函數(shù)C,C要確定B具體在什么地方,哪個模塊的哪個函數(shù)B。
PLT并不是通過GOT來確定目標(biāo)地址的,而是通過PLT結(jié)構(gòu),PLT就像個數(shù)組。

這是實(shí)現(xiàn)延遲綁定的代碼,它把指令2的地址放到了bar@GOT中,所以語句1的效果就是跳轉(zhuǎn)到語句2上去。234合起來就是解析函數(shù)符號和重定位的過程。這個n是是該函數(shù)在PLT結(jié)構(gòu)中的下標(biāo),ID是模塊的ID。不同于一開始就把目標(biāo)函數(shù)的地址放入GOT表中,PLT是一開始把一個數(shù)字放入GOT中,所以在程序開始階段前者要花時間確定目標(biāo)函數(shù)的地址而后者僅僅是壓入一個數(shù)字,復(fù)雜度為O(1)。等真正用到這個函數(shù)的時候再根據(jù)n把目標(biāo)函數(shù)插入到GOT表中,這就實(shí)現(xiàn)了延遲綁定。
GOT其實(shí)分成2個——got和got.plt。got.plt就是上面提到的PLT,它的前三項具有特殊意義:
1、第一項dynamic保存了本模塊動態(tài)鏈接相關(guān)信息。
2、本模塊ID。
3、解析函數(shù)的地址。
其他相對應(yīng)的就是外部函數(shù)的引用了。
7.5動態(tài)鏈接相關(guān)結(jié)構(gòu)
在動態(tài)鏈接情況下,操作系統(tǒng)不會在可執(zhí)行文件裝載完畢后就把控制權(quán)交給該可執(zhí)行文件,因為還沒有和共享目標(biāo)文件鏈接起來。
這時操作系統(tǒng)會先啟動一個叫動態(tài)鏈接器的東西,動態(tài)鏈接器也是個共享目標(biāo)文件,它同樣被加載到虛擬地址空間中,然后操作系統(tǒng)把控制權(quán)轉(zhuǎn)交給動態(tài)鏈接器。
然后動態(tài)鏈接器就開始初始化,這之后開始動態(tài)鏈接,鏈接完成后再把控制權(quán)轉(zhuǎn)交給可執(zhí)行文件。
動態(tài)鏈接器的位置是由可執(zhí)行文件決定的。
ELF文件中有一個interp段,該段存放著動態(tài)鏈接器的路徑。
它保存了動態(tài)鏈接器所需要的基本信息,可以看做是動態(tài)鏈接下的ELF文件頭。
dynsym:動態(tài)符號表段,它保存了與動態(tài)鏈接相關(guān)的符號,它表示各模塊之間符號的導(dǎo)入導(dǎo)出關(guān)系。比如說,模塊A的中B函數(shù)被模塊C調(diào)用了,就是A導(dǎo)出了B,C導(dǎo)入了B。
dynsym有若干輔助表,比如動態(tài)符號字符串表,它用來在程序運(yùn)行時查找符號。
動態(tài)鏈接目標(biāo)文件中極有可能包含導(dǎo)入符號的引用,這些符號的絕對地址不到運(yùn)行時是不會確定的,只有在運(yùn)行時才能重定位。
該表就是為解決這個問題而存在的。
這個動態(tài)鏈接重定位表主要指2個——rel.dyn和rel.plt。前者負(fù)責(zé)對數(shù)據(jù)引用的修正,后者負(fù)責(zé)函數(shù)引用的修正。如果ELF文件不是以代碼無關(guān)PIC模式編譯的,對外部函數(shù)的引用還可能出現(xiàn)在rel.dyn中。
堆棧里面除了保存了進(jìn)程執(zhí)行環(huán)境和命令行參數(shù)以外,還保存了動態(tài)鏈接所需要的一些輔助信息數(shù)組。
輔助信息在進(jìn)程堆棧的位置如下圖所示:

這是堆棧的初始化圖,由該圖可以看出輔助信息數(shù)組位于環(huán)境指針的后面。
分三步:
1、啟動動態(tài)鏈接器。
2、裝載所需共享對象。
3、重定位和初始化。
動態(tài)鏈接器本身不依賴于任何共享對象。
動態(tài)鏈接器本身所需要的全局和靜態(tài)變量的重定位工作由自身完成,這決定連接器啟動代碼不能使用任何靜態(tài)和全局變量,同樣不能調(diào)用函數(shù),因為它們都會用到GOT/PLT,而這些都需要被重定位,但是此時實(shí)際沒有任何重定位。
自舉是指具有一定限制條件的啟動代碼,即,自舉代碼。
動態(tài)鏈接器的入口地址就是自舉代碼的地址。
動態(tài)鏈接器會把所有具有依賴關(guān)系的共享對象放入一個集合里面,這個集合是裝載集合。
一個共享對象中的全局符號會被另一個共享對象中的同名全局符號覆蓋掉,這種現(xiàn)象叫做全局符號接入。解決這個問題的方法是只要某個共享對象中出現(xiàn)的全局符號已經(jīng)在全局符號表中了那么其他共享對象中的同名符號就不再加以考慮了。
為了解決由全局符號介入導(dǎo)致的函數(shù)功能異常的問題,本模塊要把專屬于本模塊的符號編譯單元私有化。
動態(tài)鏈接器就是根據(jù)全局符號表進(jìn)行重定位。
這完成以后動態(tài)鏈接器就把控制權(quán)交給可執(zhí)行文件了。
靜態(tài)鏈接而成的可執(zhí)行文件的入口是ELF文件頭里的e_entry指定的入口,由于動態(tài)鏈接還要鏈接共享目標(biāo)文件所以控制權(quán)還要交給動態(tài)鏈接器。
動態(tài)鏈接器本身是靜態(tài)鏈接而成的,因為它不依賴于其他共享對象。
動態(tài)鏈接器本身是代碼無關(guān)的,因為如果不是的話,代碼段無法共享浪費(fèi)內(nèi)存,也會是本身初始化變得困難。
動態(tài)鏈接器作為一個共享對象,被裝載在0x00000000處。
它有時候被簡稱為運(yùn)行時加載。即程序在運(yùn)行時需要哪個模塊就加載哪個模塊,不需要哪個模塊就卸載哪個模塊。
一般的共享對象不需要進(jìn)行修改就可以進(jìn)行運(yùn)行時加載,這種共享對象叫做動態(tài)裝載庫。
動態(tài)庫和一般的共享對象唯一的區(qū)別就是,一般的共享對象是由動態(tài)鏈接器在程序啟動之前加載和鏈接的,而動態(tài)庫通過由動態(tài)鏈接器提供的API進(jìn)行操作的。
操作的步驟為打開動態(tài)庫、查找符號、錯誤處理、關(guān)閉動態(tài)庫。
dlopen函數(shù)負(fù)責(zé)完成這一功能,并將動態(tài)庫加載進(jìn)進(jìn)程的地址空間中。
P247~P248介紹了dlopen函數(shù)的步驟。
dynamic load symbol,它是運(yùn)行時裝載的核心,它的功能是在動態(tài)庫中查找相應(yīng)的符號。
先前講到的不同模塊的同名全局符號會被第一個同名符號覆蓋,這種優(yōu)先級方式叫做裝載序列。而所謂依賴序列是指對某個符號在某個共享對象中進(jìn)行查找還會涉及到在該共享對象所依賴的共享對象中進(jìn)行查找的過程。
dynamic load error用于判斷上次調(diào)用是否成功。
dynamic load close,將一個已經(jīng)加載的模塊卸載。