之前介紹過(guò)靜態(tài)鏈接,動(dòng)態(tài)鏈接相對(duì)于靜態(tài)鏈接稍微要麻煩一些。總體來(lái)說(shuō),兩者的過(guò)程都復(fù)雜,步驟太多,涉及到重定位,符號(hào)修正,地址修正等等?!獜?fù)雜
動(dòng)態(tài)鏈接
靜態(tài)鏈接在計(jì)算機(jī)早期還是比較流行的,但是到了后面,其缺點(diǎn)也非常明顯。比如浪費(fèi)內(nèi)存和磁盤空間,更新模塊困難等。
舉個(gè)例子,每個(gè)程序內(nèi)部除了都保留了printf()、scanf()等這樣的公共函數(shù)庫(kù),還有相當(dāng)一部分的其他函數(shù)庫(kù)及輔助數(shù)據(jù)結(jié)構(gòu)都會(huì)包含在其中。現(xiàn)在Linux中,一個(gè)程序用到C語(yǔ)言靜態(tài)庫(kù)至少1MB以上,那么100個(gè)程序就會(huì)浪費(fèi)掉100MB空間。
如下圖,Program1、Program2都包含了Lib.o這個(gè)模塊,所以在連接輸出可執(zhí)行文件Program1、Program2的時(shí)候就會(huì)有兩個(gè)相同的副本。

除了浪費(fèi)空間,動(dòng)態(tài)更新也很麻煩。比如在iOS中,你用到了第三方的一個(gè)SDK,如果SDK出現(xiàn)了什么Bug,只有等著SDK廠商修復(fù)完bug,將新版的SDK給用戶,用戶再去更新自己的APP,才能修復(fù)線上問(wèn)題。這套流程對(duì)于一般的公司來(lái)講代價(jià)是非常大的,時(shí)間周期太長(zhǎng)。
動(dòng)態(tài)鏈接的出現(xiàn)解決了上面的問(wèn)題。將程序模塊相互獨(dú)立的分隔開(kāi)來(lái),形成獨(dú)立的文件,不再將它們靜態(tài)地鏈接到一起。簡(jiǎn)單而言就是對(duì)那些組成程序目標(biāo)文件的鏈接,等到程序運(yùn)行時(shí)才進(jìn)行鏈接,也就是把鏈接的過(guò)程推遲到運(yùn)行時(shí)才進(jìn)行,這就是動(dòng)態(tài)鏈接的基本思想。
如上面的例子,假如現(xiàn)在保留了Program1.o、Program2.o和Lib.o,當(dāng)運(yùn)行Program1這個(gè)程序的時(shí)候,系統(tǒng)首先加載Program1.o,當(dāng)系統(tǒng)發(fā)現(xiàn)Program1.o依賴Lib.o的時(shí)候,那么系統(tǒng)再去加載Lib.o,如果還依賴其他目標(biāo)文件,則同樣以類似于懶加載的方式去加載其他目標(biāo)文件。
當(dāng)所有的目標(biāo)文件加載完之后,依賴關(guān)系也得到了滿足,則系統(tǒng)才開(kāi)始進(jìn)行鏈接,這個(gè)鏈接過(guò)程和現(xiàn)在鏈接非常相似。之前介紹過(guò)靜態(tài)鏈接的過(guò)程,包含符號(hào)解析,重定向等。完成這些之后,系統(tǒng)再把控制權(quán)交過(guò)Program1.o的執(zhí)行入口,開(kāi)始執(zhí)行。如果這個(gè)時(shí)候Program2需要運(yùn)行,則會(huì)發(fā)現(xiàn)系統(tǒng)中已經(jīng)存在了Lib.o的副本所以就不需要重新加載Lib.o,直接將Lib.o鏈接起來(lái)就可以了。

根據(jù)前面介紹的,這樣的方式不僅僅減少了內(nèi)存、磁盤空間的浪費(fèi),還減少了物理頁(yè)面的換入換出,也可以增加CPU緩存的命中率,因?yàn)椴煌M(jìn)程的數(shù)據(jù)和指令偶讀集中在了一個(gè)共享模塊上。
至于更新也就更加簡(jiǎn)單了,只需要簡(jiǎn)單的將舊的目標(biāo)文件覆蓋掉。無(wú)需從先將程序鏈接一遍,下次程序運(yùn)行的時(shí)候,新的目標(biāo)文件就會(huì)自動(dòng)裝載到內(nèi)存中。
擴(kuò)展性及兼容性
動(dòng)態(tài)鏈接還有一個(gè)特點(diǎn)就是讓程序可以動(dòng)態(tài)的選擇加載程序模塊,有點(diǎn)像插件的含義。只要規(guī)定了好了程序的幾口,那么只需要安裝這個(gè)接口來(lái)編寫動(dòng)態(tài)鏈接文件,就可以實(shí)現(xiàn)那動(dòng)態(tài)的添加,擴(kuò)展程序的功能?!绻鹖OS中不上架AppStore是可以實(shí)現(xiàn)動(dòng)態(tài)更新的,比如用企業(yè)證書發(fā)布。
其次兼容性也是動(dòng)態(tài)鏈接的一個(gè)優(yōu)點(diǎn)。動(dòng)態(tài)鏈接相當(dāng)于在程序和操作系統(tǒng)之間增加了一個(gè)中間層,從而消除了程序?qū)Σ煌脚_(tái)之間的依賴關(guān)系。
動(dòng)態(tài)鏈接基本實(shí)現(xiàn)
基本思想上面介紹過(guò),就是把程序按照各個(gè)模塊劃分為相對(duì)獨(dú)立的模塊,在程序運(yùn)行的時(shí)候才將他們鏈接在一起形成完成的程序。
動(dòng)態(tài)鏈接需要得到操作系統(tǒng)的支持,因?yàn)樵趧?dòng)態(tài)鏈接的情況下,進(jìn)程的虛擬地址空間分布會(huì)比靜態(tài)鏈接情況更為復(fù)雜。比如一些存儲(chǔ)管理、內(nèi)存共享、進(jìn)程線程等機(jī)制都會(huì)相對(duì)于靜態(tài)鏈接不同。
使用動(dòng)態(tài)鏈接庫(kù)的情況下,程序被分為程序主模塊和動(dòng)態(tài)鏈接庫(kù),實(shí)際上它們都可以看成程序的一個(gè)模塊,都包含了程序指令和數(shù)據(jù)。比如C語(yǔ)言的運(yùn)行庫(kù)glibc就是以動(dòng)態(tài)鏈接形式保存在lib下面:
-> # ls | grep libc
libc-2.17.so
libcap-ng.so.0
libcap-ng.so.0.0.0
libcap.so.2
libcap.so.2.22
libcidn-2.17.so
libcidn.so.1
libcrack.so.2
libcrack.so.2.9.0
libcrypt-2.17.so
libcrypt.so.1
libc.so.6
系統(tǒng)只保留了一份libc.so。而所有用c語(yǔ)言編寫的、動(dòng)態(tài)鏈接程序都可以在運(yùn)行時(shí)使用它。當(dāng)程序被裝載的時(shí)候,系統(tǒng)的動(dòng)態(tài)鏈接器會(huì)將程序所有用到的動(dòng)態(tài)鏈接庫(kù)裝載到進(jìn)程的地址空間。
動(dòng)態(tài)鏈接的例子
用上面的Program1.c、Program2.c及Lib.c作為例子
Lib.c
#include <stdio.h>
void foobar(int i) {
printf("Printint from Lib.so %d\n", i);
}
Lib.h
#ifndef LIB_H
#define LIB_H
void foobar(int i);
#endif
Program1.c
#include "Lib.h"
int main()
{
foobar(1);
return 0;
}
Program2.c
#include "Lib.h"
int main()
{
foobar(2);
return 0;
}
現(xiàn)在將Lib.c編譯為一個(gè).so文件
gcc -fPIC -shared -o Lib.so Lib.c
-share表示生成.so文件,這樣就會(huì)在當(dāng)前目錄下生成Lib.so文件。其中包含了foobar()函數(shù),然后分別編譯連接Program1.c和Program2.c。
-> # gcc -o Program1 Program1.c ./Lib.so
-> # gcc -o Program2 Program2.c ./Lib.so
一共有如下文件
ls
Lib.c Lib.h Lib.so Program1 Program1.c Program2 Program2.c
現(xiàn)在從Program1的角度來(lái)看整個(gè)編譯及連接過(guò)程如下:

執(zhí)行一下Program1、Program2
-> # ./Program1
Printint from Lib.so 1
-> # ./Program2
Printint from Lib.so 2
-> #
注意上面的圖,在鏈接器這步,Lib.o并沒(méi)有被鏈接進(jìn)來(lái),鏈接的輸入目標(biāo)文件只有Program1.o,當(dāng)時(shí)從可執(zhí)行文件執(zhí)行的結(jié)果來(lái)看,確實(shí)Lib.so參與了。
當(dāng)程序模塊Program1.c被編譯為Program1.o的時(shí)候,編譯器還不知道foobar函數(shù)地址,靜態(tài)鏈接也講過(guò),對(duì)于弱符號(hào),如果鏈接器必須確定所引用的函數(shù),那么鏈接器會(huì)根據(jù)鏈接的規(guī)則將foobr函數(shù)重定位。如果foorbar定義在一個(gè)動(dòng)態(tài)共享庫(kù)中,那么鏈接器會(huì)將這個(gè)符號(hào)引用標(biāo)記為一個(gè)動(dòng)態(tài)鏈接符號(hào),不對(duì)他進(jìn)行重定位,而是把重定位的實(shí)際留到裝載的時(shí)候再進(jìn)行。
Lib.so中保存了完整的符號(hào)信息,鏈接器在解析符號(hào)時(shí)就知道,foorbar是一個(gè)定義在Lib.so的動(dòng)態(tài)符號(hào),從而對(duì)foobar特殊處理使得它成為一個(gè)對(duì)動(dòng)態(tài)符號(hào)的引用。
動(dòng)態(tài)鏈接程序運(yùn)行時(shí)地址空間分布
靜態(tài)鏈接而言,整個(gè)進(jìn)程只有一個(gè)可執(zhí)行文件被映射,之前介紹過(guò)靜態(tài)的內(nèi)存分布。動(dòng)態(tài)鏈接而言除了可執(zhí)行文件外還有其他共享目標(biāo)文件。
以Program1為例。在其中加入sleep函數(shù)防止一運(yùn)行程序就結(jié)束了。
#include "Lib.h"
int main()
{
foobar(1);
sleep(-1);
return 0;
}
直接看打印的結(jié)果
-> # ./Program1 &
[2] 7847
Printint from Lib.so 1
-> # cat /proc/7847/maps
00400000-00401000 r-xp 00000000 fd:00 608267 /root/CodeDir/dym_linckTest/Program1
00600000-00601000 r--p 00000000 fd:00 608267 /root/CodeDir/dym_linckTest/Program1
00601000-00602000 rw-p 00001000 fd:00 608267 /root/CodeDir/dym_linckTest/Program1
7f5eaac3d000-7f5eaadf5000 r-xp 00000000 fd:00 17332206 /usr/lib64/libc-2.17.so
7f5eaadf5000-7f5eaaff5000 ---p 001b8000 fd:00 17332206 /usr/lib64/libc-2.17.so
7f5eaaff5000-7f5eaaff9000 r--p 001b8000 fd:00 17332206 /usr/lib64/libc-2.17.so
7f5eaaff9000-7f5eaaffb000 rw-p 001bc000 fd:00 17332206 /usr/lib64/libc-2.17.so
7f5eaaffb000-7f5eab000000 rw-p 00000000 00:00 0
7f5eab000000-7f5eab001000 r-xp 00000000 fd:00 608263 /root/CodeDir/dym_linckTest/Lib.so
7f5eab001000-7f5eab200000 ---p 00001000 fd:00 608263 /root/CodeDir/dym_linckTest/Lib.so
7f5eab200000-7f5eab201000 r--p 00000000 fd:00 608263 /root/CodeDir/dym_linckTest/Lib.so
7f5eab201000-7f5eab202000 rw-p 00001000 fd:00 608263 /root/CodeDir/dym_linckTest/Lib.so
7f5eab202000-7f5eab223000 r-xp 00000000 fd:00 17332199 /usr/lib64/ld-2.17.so
7f5eab416000-7f5eab419000 rw-p 00000000 00:00 0
7f5eab421000-7f5eab423000 rw-p 00000000 00:00 0
7f5eab423000-7f5eab424000 r--p 00021000 fd:00 17332199 /usr/lib64/ld-2.17.so
7f5eab424000-7f5eab425000 rw-p 00022000 fd:00 17332199 /usr/lib64/ld-2.17.so
7f5eab425000-7f5eab426000 rw-p 00000000 00:00 0
7fff0c0fe000-7fff0c11f000 rw-p 00000000 00:00 0 [stack]
7fff0c16e000-7fff0c170000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
整個(gè)進(jìn)程調(diào)度虛擬地址空間多出了幾個(gè)文件的映射。Lib.so與Program1一樣,被操作系統(tǒng)已同樣的方式映射到虛擬地址空間,知識(shí)占據(jù)的虛擬地址范圍不同。
其中還用到了C語(yǔ)言運(yùn)行庫(kù)libc-2.17.so,還有一個(gè)非常重要的共享對(duì)象ld-2.17.so,其實(shí)ld-2.17.so就是Linux下的動(dòng)態(tài)鏈接器。動(dòng)態(tài)鏈接器和普通的共享對(duì)象一樣被映射到了進(jìn)程的地址空間,系統(tǒng)開(kāi)始運(yùn)行程序之前,會(huì)把控制權(quán)給動(dòng)態(tài)鏈接器,由動(dòng)態(tài)鏈接器完成鏈接工作,之后再把控制權(quán)給Program1
可以使用readelf -l Lib.so查看Lib.so的裝載屬性
readelf -l Lib.so
Elf 文件類型為 DYN (共享目標(biāo)文件)
入口點(diǎn) 0x5e0
共有 7 個(gè)程序頭,開(kāi)始于偏移量64
程序頭:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x000000000000078c 0x000000000000078c R E 200000
LOAD 0x0000000000000df8 0x0000000000200df8 0x0000000000200df8
0x0000000000000238 0x0000000000000240 RW 200000
DYNAMIC 0x0000000000000e18 0x0000000000200e18 0x0000000000200e18
0x00000000000001c0 0x00000000000001c0 RW 8
NOTE 0x00000000000001c8 0x00000000000001c8 0x00000000000001c8
0x0000000000000024 0x0000000000000024 R 4
GNU_EH_FRAME 0x000000000000070c 0x000000000000070c 0x000000000000070c
0x000000000000001c 0x000000000000001c R 4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 10
GNU_RELRO 0x0000000000000df8 0x0000000000200df8 0x0000000000200df8
0x0000000000000208 0x0000000000000208 R 1
Section to Segment mapping:
段節(jié)...
00 .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame
01 .init_array .fini_array .jcr .data.rel.ro .dynamic .got .got.plt .bss
02 .dynamic
03 .note.gnu.build-id
04 .eh_frame_hdr
05
06 .init_array .fini_array .jcr .data.rel.ro .dynamic .got
可以看到除了文件類型和可執(zhí)行文件不同與裝載地址從0x0000 0000開(kāi)始之外,其余基本上都一樣。很明顯這個(gè)裝載地址是無(wú)效地址。共享對(duì)象最終的裝載地址在編譯時(shí)是不確定的,而是在裝載的時(shí)候,裝載器更加當(dāng)前地址空間的空閑狀體,動(dòng)態(tài)分配一塊足夠大小的虛擬地址空間給相應(yīng)的共享對(duì)象。
地址無(wú)關(guān)代碼
地址無(wú)關(guān)代碼是為了解決指令部分在多個(gè)進(jìn)程之間共享問(wèn)題。重定位解決的是動(dòng)態(tài)模塊中有絕對(duì)地址引用的問(wèn)題。
舊方案
- 共享對(duì)象在被裝載時(shí),如何確定它在進(jìn)程虛擬空間的地址?
相對(duì)于動(dòng)態(tài)庫(kù)共享還有一種叫做靜態(tài)共享庫(kù),靜態(tài)共享庫(kù)和靜態(tài)庫(kù)有很明顯的區(qū)別。靜態(tài)庫(kù)是在鏈接的時(shí)候就確定了符號(hào)地址,而靜態(tài)共享庫(kù)是吧程序各個(gè)模塊統(tǒng)一交給操作系統(tǒng)來(lái)管理,操作系統(tǒng)在某個(gè)特定的地址劃分出一個(gè)地址塊,為已知的模塊預(yù)留足夠的空間。
靜態(tài)共享庫(kù)有很多問(wèn)題,比如地址沖突;還有就是升級(jí)之后共享庫(kù)必須保持共享庫(kù)中的全局函數(shù)和變量地址不變,一旦在鏈接的時(shí)候綁定了這些地址,更改之后就需要重新鏈接整個(gè)程序。
新方案——裝載時(shí)重定位
為了讓共享對(duì)象在任意地址裝載,所以對(duì)所有絕對(duì)地址的引用不做重定位,而是把這步推遲到裝載的時(shí)候再完成,比如一旦模塊的裝載地址確定了也就是目標(biāo)地址確定,那么系統(tǒng)對(duì)程序所有的絕對(duì)地址引用進(jìn)行重定位,來(lái)實(shí)現(xiàn)任意地址裝載。
比如前面的例子foorbar相對(duì)于代碼段的其實(shí)位置是0x100,當(dāng)模塊被裝載到0x10000000時(shí),假設(shè)代碼段在模塊最開(kāi)始的位置,則foobar的地址就是0x10000100。這個(gè)時(shí)候遍歷所有模塊中的重定位表,把所有對(duì)foorbar的地址引用都重定位為0x10000100
靜態(tài)鏈接的重定位叫做鏈接時(shí)重定位,而上面這種方式叫做裝載時(shí)重定位。
雖然能夠解決動(dòng)態(tài)模塊中有絕對(duì)地址引用的情況,還是沒(méi)能解決上面的多個(gè)模塊依賴一個(gè)共享庫(kù)的指令,變量地址的問(wèn)題(如何共用?)。動(dòng)態(tài)鏈接模塊被裝載映射到虛擬空間,指令部分大部分都是進(jìn)程之間共享的,因?yàn)橹噶畋恢囟ㄎ缓髮?duì)每個(gè)進(jìn)程來(lái)講是不同的?!獑?wèn)題?
動(dòng)態(tài)鏈接庫(kù)中可以修改數(shù)據(jù)的部分對(duì)于不同進(jìn)程來(lái)講是由多個(gè)副本的,所以可以用裝載時(shí)重定位的方法來(lái)解決。
Linux和GCC支持這種裝載時(shí)重定位。GCC有兩個(gè)參數(shù)
-shared表示輸出的共享對(duì)象就是使用裝載時(shí)重定位的方式.
地址無(wú)關(guān)代碼的實(shí)現(xiàn)
上面在生成可執(zhí)行文件Program1的時(shí)候除了shared參數(shù)還用到了-fPIC。fPIC就是表示生成地址無(wú)關(guān)代碼的。
上面雖然裝載時(shí)重定位解決了動(dòng)態(tài)模塊中有絕對(duì)地址引用的情況,但是指令部分還是無(wú)法在多個(gè)進(jìn)程之間共享。
歸根結(jié)底希望程序模塊中共享的指令部分在裝載時(shí)不需要因?yàn)檠b載地址改變而改變。
思路就是把這些指令按照需要被修改的部分剝離處理,需要修改數(shù)據(jù)部分放在一起,不修改的指令放一起。這里指令部分就保持不變了,數(shù)據(jù)部分就可以在每個(gè)進(jìn)程中有一個(gè)副本。這就是地址無(wú)關(guān)代碼(PIC Positon-independent Code)。
按照兩個(gè)維度分析共享對(duì)象模塊。一個(gè)是地址引用方式,分為模塊內(nèi)、模塊外;一種是引用方式,分為指令引用和數(shù)據(jù)訪問(wèn)。一共就4中情況。
模塊間的指令引用和數(shù)據(jù)訪問(wèn)會(huì)通過(guò)一張全局偏移表(GOT Global Offset Table)來(lái)實(shí)現(xiàn),這個(gè)表里面建立了指向這些變量或者指令的指針數(shù)組。需要用到這些變量的時(shí)候,從這個(gè)表中去查找



模塊內(nèi)的指令引用和訪問(wèn)數(shù)據(jù)則會(huì)根據(jù)偏移計(jì)算出來(lái)。

遺留問(wèn)題
共享模塊全局變量
定義在模塊內(nèi)的全局變量?當(dāng)一個(gè)模塊醫(yī)用了一個(gè)定義在全局變量的時(shí)候,編譯器無(wú)法判斷這個(gè)變量在定義同一模塊還是定義在另一個(gè)共享對(duì)象之中。
如果是可執(zhí)行文件的一部分,則程序主模塊就不是地址五官代碼,不會(huì)使用PIC機(jī)制。在連接的時(shí)候就會(huì)重定位。具體過(guò)程之前介紹過(guò),會(huì)在bbs段創(chuàng)建一個(gè)改變量的副本。
如果是共享庫(kù),還是按照剛才那種方式,因?yàn)?code>bbs段在共享庫(kù)中只有一份副本,因?yàn)槭侨止蚕恚敲匆粋€(gè)變量同時(shí)存在多個(gè)位置,肯定不行的。
解決辦法就是將所有使用這個(gè)變量的指令都指向位于可執(zhí)行文件中的那個(gè)副本。ELF共享庫(kù)在編譯時(shí),默認(rèn)把定義在模塊內(nèi)部的全局變量當(dāng)作定義在其他模塊的全局變量,如果某個(gè)全局變量在可執(zhí)行文件中有副本,那么動(dòng)聽(tīng)?zhēng)炀蜁?huì)把GOT中相應(yīng)地址指向該副本。這樣變量在運(yùn)行時(shí)就只有一個(gè)實(shí)例了。
總結(jié)一下:如果變量在共享模塊中被初始化,則動(dòng)態(tài)鏈接器會(huì)將改初始化值復(fù)制到程序主模塊中的變量副本;如果變量在主模塊中沒(méi)有副本,那么GOT中就直接指向模塊內(nèi)部的改變量副本。
特別注意共享庫(kù)的數(shù)據(jù)段在每個(gè)進(jìn)程中都有獨(dú)立的副本,所以不同進(jìn)程之間的全局共享變量不會(huì)彼此影響。
數(shù)據(jù)段地址無(wú)關(guān)性
始終記住,數(shù)據(jù)端在每個(gè)進(jìn)程中都有一份獨(dú)立的副本,所以并不會(huì)擔(dān)心因?yàn)檫M(jìn)程而改變。可以選擇在裝載時(shí)重定位的方法來(lái)解決數(shù)據(jù)端中絕對(duì)地址應(yīng)用問(wèn)題。
對(duì)于共享對(duì)象來(lái)講,如果數(shù)據(jù)段有絕對(duì)地址的引用,那么鏈接器就會(huì)產(chǎn)生一個(gè)重定位表。這個(gè)重定位表里面包含了R_386_RELATIVE類型的重定位入口。當(dāng)在裝載的時(shí)候發(fā)現(xiàn)該共吸納過(guò)對(duì)象有這樣的重定位入口,動(dòng)態(tài)鏈接器就會(huì)對(duì)該共享對(duì)象進(jìn)行重定位。
其實(shí)代碼段也可以使用這種裝載重定位,但是這樣就不是地址無(wú)關(guān)了,就會(huì)造成多個(gè)副本,不能多個(gè)進(jìn)程之間共享,于是就失去了節(jié)省內(nèi)存的特點(diǎn),因?yàn)闆](méi)訪問(wèn)全局變量和函數(shù)的時(shí)候都需要做一次計(jì)算當(dāng)期那地址及簡(jiǎn)介地址尋址的過(guò)程。。所以說(shuō)地址無(wú)關(guān)根本目的是在于共享,在于節(jié)省內(nèi)存。
延遲綁定
動(dòng)態(tài)鏈接鏈接很多優(yōu)勢(shì),但是相對(duì)之下性能比靜態(tài)庫(kù)要差一些。
主要原因是動(dòng)態(tài)鏈接對(duì)群架和靜態(tài)數(shù)據(jù)的訪問(wèn)都需要復(fù)雜的GOT定位,然后間接尋址,對(duì)于模塊間的調(diào)用也要先GOT定位,再進(jìn)行跳轉(zhuǎn),所以程序運(yùn)行的速度回慢一些。而且在啟動(dòng)的時(shí)候動(dòng)態(tài)鏈接器需要進(jìn)行一次鏈接工作進(jìn)行符號(hào)查找及重定位,所以啟動(dòng)速度也會(huì)慢下倆。
優(yōu)化方式:用懶加載的方式,也就是函數(shù)第一次用到才進(jìn)行綁定(符號(hào)查找,重定位等),如果沒(méi)有用到則不進(jìn)行綁定。
ELF使用PLT(Procedure Linkage Table)的方式實(shí)現(xiàn),是一寫很精巧的匯編指令實(shí)現(xiàn)。也是通過(guò)一個(gè)表來(lái)保存需要跳轉(zhuǎn)的信息。
PLT基本原理:ELF將GOT拆為兩個(gè)表.got和.got.plt。其中.got用來(lái)保存全局變量的引用地址,.got.ptl用來(lái)保存函數(shù)引用的地址,所有對(duì)于外部函數(shù)的引用全部放到.got.plt中。.got.plt的前三項(xiàng)有特殊含義。
- 第一項(xiàng)保存的是
.dynamic段的地址,這個(gè)段秒速了本模塊動(dòng)態(tài)鏈接的相關(guān)信息 - 第二項(xiàng)保存的是本模塊的ID
- 第三項(xiàng)保存的是
_dl_runtime_reslove的地址
第二項(xiàng)和第三項(xiàng)由動(dòng)態(tài)鏈接器在裝載共享模塊的時(shí)候初始化。

動(dòng)態(tài)鏈接相關(guān)結(jié)構(gòu)
在動(dòng)態(tài)鏈接情況下,操作系統(tǒng)不能再裝載完可執(zhí)行文件之后就把控制權(quán)交給可執(zhí)行文件,因?yàn)榭蓤?zhí)行文件可能依賴很多共享對(duì)象,里面的很多外部符號(hào)還是無(wú)效地址,還沒(méi)有跟相應(yīng)的共享對(duì)象實(shí)際位置鏈接起來(lái)。在映射玩可執(zhí)行文件之后,操作系統(tǒng)會(huì)先啟動(dòng)一個(gè)動(dòng)態(tài)鏈接器。
操作系統(tǒng)將控制權(quán)交給動(dòng)態(tài)鏈接器的入口,開(kāi)始執(zhí)行一系列自身的初始化操作,然后根據(jù)當(dāng)前的環(huán)境參數(shù)對(duì)可執(zhí)行文件進(jìn)行動(dòng)態(tài)鏈接工作,所有鏈接操作完成之后,動(dòng)態(tài)鏈接器將控制權(quán)限轉(zhuǎn)交給可執(zhí)行文件的入口地址,程序開(kāi)始執(zhí)行。
interp段
動(dòng)態(tài)鏈接器不是由系統(tǒng)配置的,而是由ELF文件自己決定的,在動(dòng)態(tài)鏈接的ELF可執(zhí)行文件中,有一個(gè)專門的段.interp段(解釋器)。保存的就是動(dòng)態(tài)鏈接器的路徑。
.dynamic段
這個(gè)段里面保存了動(dòng)態(tài)鏈接器的基本所需要的信息,比如依賴于哪些共享對(duì)象,動(dòng)態(tài)連接符號(hào)表的位置,動(dòng)態(tài)鏈接重定位表的位置,共享對(duì)象初始化代碼的地址段。
typedef struct {
Elf32_Sword d_tag;
union {
Elf32_Word d_val;
Elf32_Addr d_ptr;
} d_un;
} Elf32_Dyn;
extern Elf32_Dyn _DYNAMIC[];
字段的說(shuō)明

動(dòng)態(tài)符號(hào)表(dynsym)
類比靜態(tài)鏈接中的符號(hào)表.symtab,里面保持了所有關(guān)與改目標(biāo)文件的符號(hào)定義和引用。動(dòng)態(tài)鏈接的符號(hào)表和靜態(tài)鏈接非常相似。
這個(gè)段的段名叫.dynsym,簡(jiǎn)稱動(dòng)態(tài)符號(hào)表。只保存了與動(dòng)態(tài)鏈接相關(guān)的符號(hào)。很多動(dòng)態(tài)鏈接模塊同時(shí)又dynsym和symtab兩個(gè)表,后者包含了所有符號(hào)包含了dynsym中的符號(hào)。同樣也有一些輔助表,比如字符串表.strtab,這里叫做.dynstr
動(dòng)態(tài)鏈接重定位表
動(dòng)態(tài)鏈接的可執(zhí)行文件使用PIC方法,雖然其代碼段不需要重定位(因?yàn)榈刂窡o(wú)關(guān)),但是數(shù)據(jù)端還是包含了絕對(duì)地址的引用,因?yàn)榇a段中絕對(duì)地址相關(guān)部分被分離了出來(lái),編程了GOT(全局偏移表),而GOT實(shí)際上是數(shù)據(jù)端的一部分,除了GOT,數(shù)據(jù)端還可以能包含絕對(duì)地址引用。
重定位相關(guān)數(shù)據(jù)結(jié)構(gòu)
和靜態(tài)鏈接類似,動(dòng)態(tài)鏈接重定位表分為.rel.dyn和.rel.plt他們分別相當(dāng)于.rel.text和.rel.data。.rel.dyn是對(duì)數(shù)據(jù)的修真,位于.got段,.rel.plt是對(duì)函數(shù)的修正位于.got.plt段。
堆棧信息初始化
進(jìn)程初始化的時(shí)候,堆棧里面保持了關(guān)于進(jìn)程執(zhí)行的環(huán)境何明亮行參數(shù) 等信息,還保持了動(dòng)態(tài)鏈接所需要的一些輔助信息數(shù)據(jù),輔助信息的格式是一個(gè)結(jié)構(gòu)體數(shù)組。

其中的字段含義


輔助信息數(shù)組位于環(huán)境變量指針的后面。假設(shè)操作系統(tǒng)傳遞給動(dòng)態(tài)鏈接器的輔助信息有4個(gè)

那么堆棧信息如下

裝載共享對(duì)象
動(dòng)態(tài)鏈接基本上分為3步:
- 顯示啟動(dòng)鏈接器本生
- 裝載所需要的共享對(duì)象
- 重定位和初始化
動(dòng)態(tài)鏈接器本身也是個(gè)共享對(duì)象,如果按照上面的邏輯,需要一個(gè)另一個(gè)動(dòng)態(tài)鏈接庫(kù)來(lái)鏈接這個(gè)庫(kù)。為了解決這個(gè)問(wèn)題動(dòng)態(tài)鏈接庫(kù)比較特殊,首先動(dòng)態(tài)鏈接器本身不會(huì)依賴其他任何共享對(duì)象,其次他的全局和靜態(tài)變量的重定位工作由自身完成。——具有一定限制條件的啟動(dòng)代碼往往被稱作為自舉(BootStrap)
裝載共享對(duì)象
完成自舉以后,動(dòng)態(tài)鏈接器將可執(zhí)行文件和鏈接器本身的符號(hào)表都合并到一個(gè)全局符號(hào)表中。然后連機(jī)器開(kāi)始尋找可執(zhí)行文件所依賴的共享對(duì)象。
在.dynamic段中有一種類型入口為DT_NEEDED,它所指出是可執(zhí)行文件所依賴的共享對(duì)象。然后將這些共享對(duì)象的名字放入到一個(gè)裝載集合中。然后在鏈接器在一個(gè)一個(gè)取出里面的共享對(duì)門名字,找到對(duì)應(yīng)的文件,讀取ELF文件頭和.dynamic段內(nèi)容,將數(shù)據(jù)端、代碼段映射到進(jìn)程空間。如果還有依賴的共享庫(kù),則一個(gè)一個(gè)遍歷,重復(fù)這個(gè)過(guò)程。
整個(gè)過(guò)程可以看做是一個(gè)廣度優(yōu)先的遍歷過(guò)程。
符號(hào)優(yōu)先級(jí)
如果兩個(gè)共享對(duì)象都定義了同一符號(hào),會(huì)出現(xiàn)什么情況?如果在靜態(tài)庫(kù)中如果有相同的符號(hào)根本就不會(huì)鏈接成功。
在Linux中的動(dòng)態(tài)鏈接器,它定義了一個(gè)規(guī)則,當(dāng)一個(gè)符號(hào)需要加入全局符號(hào)表時(shí),如果相同名字的符號(hào)已經(jīng)存在,則后加入的符號(hào)會(huì)被忽略。從動(dòng)態(tài)鏈接器加載的順序可以看到,是按照廣度優(yōu)先的順序就行裝載的.
由于存在這種直接忽略符號(hào)的現(xiàn)象,所以當(dāng)程序使用大量的共享對(duì)象對(duì)象的時(shí)候,需要非常小心重名的問(wèn)題。名字相同的符號(hào),卻執(zhí)行了不同的功能,那么程序運(yùn)行出現(xiàn)莫名其妙的問(wèn)題。
總結(jié)
總算是動(dòng)態(tài)鏈接部分介紹完了。有好多好多細(xì)節(jié)的地方?jīng)]介紹,比如根據(jù)一個(gè)實(shí)例來(lái)計(jì)算符號(hào)偏移地址。感覺(jué)內(nèi)容太多了。這里只是把動(dòng)態(tài)鏈接主要部分介紹了下。
動(dòng)態(tài)鏈接相對(duì)于靜態(tài)鏈接過(guò)程不同之處有很多。重點(diǎn)提一下符號(hào)重定位。動(dòng)態(tài)鏈接中對(duì)于不變的代碼段是通過(guò)抵制無(wú)關(guān)代碼實(shí)現(xiàn)的(PLT),里面涉及到一個(gè)全局偏移表(GOT),里面記錄指令對(duì)應(yīng)的地址。而對(duì)于需要改變的數(shù)據(jù)段是通過(guò)重定向來(lái)實(shí)現(xiàn)的,根據(jù)重定向表實(shí)現(xiàn),這點(diǎn)和靜態(tài)鏈接類似。