Linux 為什么要?jiǎng)討B(tài)鏈接?與靜態(tài)鏈接的區(qū)別是什么?
掃描二維碼
隨時(shí)隨地手機(jī)看文章
在前面的文章中程序喵已經(jīng)介紹過(guò)靜態(tài)鏈接的原理,這篇文章我們來(lái)解密動(dòng)態(tài)鏈接。
老規(guī)矩,先拋出幾個(gè)問(wèn)題:
為什么要進(jìn)行動(dòng)態(tài)鏈接?
如何進(jìn)行動(dòng)態(tài)鏈接?
什么是地址無(wú)關(guān)代碼技術(shù)?
什么是延遲綁定技術(shù)?
如何在程序運(yùn)行過(guò)程中進(jìn)行顯式鏈接?
為什么要進(jìn)行動(dòng)態(tài)鏈接?
因?yàn)殪o態(tài)鏈接有缺點(diǎn):
浪費(fèi)內(nèi)存和磁盤空間:如下圖,
Program1和Program2分別包含Program1.o和Program2.o兩個(gè)模塊,他們都需要Lib.o模塊。靜態(tài)鏈接情況下,兩個(gè)目標(biāo)文件都用到Lib.o這個(gè)模塊,所以它們同時(shí)在鏈接輸出的可執(zhí)行文件Program1和program2中有副本,同時(shí)運(yùn)行時(shí),Lib.o在磁盤和內(nèi)存中有兩份副本,當(dāng)系統(tǒng)中有大量類似Lib.o的多個(gè)程序共享目標(biāo)文件時(shí),就會(huì)浪費(fèi)很大空間。
靜態(tài)鏈接對(duì)程序的更新部署和發(fā)布很不友好:假如一個(gè)模塊依賴20個(gè)模塊,當(dāng)20個(gè)模塊其中有一個(gè)模塊需要更新時(shí),需要將所有的模塊都找出來(lái)重新編譯出一個(gè)可執(zhí)行程序才可以更新成功,每次更新任何一個(gè)模塊,用戶就需要重新獲得一個(gè)非常大的程序,程序如果使用靜態(tài)鏈接,那么通過(guò)網(wǎng)絡(luò)來(lái)更新程序也會(huì)非常不便,一旦程序任何位置有一個(gè)小改動(dòng),都會(huì)導(dǎo)致整個(gè)程序重新下載。
為了解決靜態(tài)鏈接的缺點(diǎn),所以引入了動(dòng)態(tài)鏈接,動(dòng)態(tài)鏈接的內(nèi)存分布如圖,
多個(gè)程序依賴同一個(gè)共享目標(biāo)文件,這個(gè)共享目標(biāo)文件在磁盤和內(nèi)存中僅有一份,不會(huì)產(chǎn)生副本,簡(jiǎn)單來(lái)講就是不像靜態(tài)鏈接一樣對(duì)那些組成程序的目標(biāo)文件進(jìn)行鏈接,等到程序要運(yùn)行時(shí)才進(jìn)行鏈接,把鏈接這個(gè)過(guò)程推遲到運(yùn)行時(shí)才執(zhí)行。動(dòng)態(tài)鏈接的方式使得開發(fā)過(guò)程中各個(gè)模塊更加獨(dú)立,耦合度更小,便于不同的開發(fā)者和開發(fā)組織之間獨(dú)立的進(jìn)行開發(fā)和測(cè)試。
如何進(jìn)行動(dòng)態(tài)鏈接?
看如下代碼:
// lib.c
void func(int i) {
printf("func %d \n", i);
}
// Program.c
void func(int i);
int main() {
func(1);
return 0;
}
編譯運(yùn)行過(guò)程如下:
gcc -fPIC -shared -o lib.so lib.c
test Program.c ./lib.so gcc -o
test ./
func 1
通過(guò)-fPIC和-shared可以生成一個(gè)動(dòng)態(tài)鏈接庫(kù),再鏈接到可執(zhí)行程序就可以正常運(yùn)行。
通過(guò)readelf命令可以查看動(dòng)態(tài)鏈接庫(kù)的segment信息:
readelf -l lib.so
Elf file type is DYN (Shared object file)
Entry point 0x530
There are 7 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x00000000000006e4 0x00000000000006e4 R E 0x200000
LOAD 0x0000000000000e10 0x0000000000200e10 0x0000000000200e10
0x0000000000000218 0x0000000000000220 RW 0x200000
DYNAMIC 0x0000000000000e20 0x0000000000200e20 0x0000000000200e20
0x00000000000001c0 0x00000000000001c0 RW 0x8
NOTE 0x00000000000001c8 0x00000000000001c8 0x00000000000001c8
0x0000000000000024 0x0000000000000024 R 0x4
GNU_EH_FRAME 0x0000000000000644 0x0000000000000644 0x0000000000000644
0x0000000000000024 0x0000000000000024 R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x0000000000000e10 0x0000000000200e10 0x0000000000200e10
0x00000000000001f0 0x00000000000001f0 R 0x1
Section to Segment mapping:
Segment Sections...
00 .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame
01 .init_array .fini_array .dynamic .got .got.plt .data .bss
02 .dynamic
03 .note.gnu.build-id
04 .eh_frame_hdr
05
06 .init_array .fini_array .dynamic .got
可以看見動(dòng)態(tài)鏈接模塊的裝載地址從0開始,0是無(wú)效地址,它的裝載地址會(huì)在程序運(yùn)行時(shí)再確定,在編譯時(shí)是不確定的。
改一下程序:
// Program.c
void func(int i);
int main() {
func(1);
sleep(-1);
return 0;
}
運(yùn)行讀取maps信息:
~/test$ ./test &
[1] 126
~/test$ func 1
cat /proc/126/maps
7ff2c59f0000-7ff2c5bd7000 r-xp 00000000 00:00 516391 /lib/x86_64-linux-gnu/libc-2.27.so
7ff2c5bd7000-7ff2c5be0000 ---p 001e7000 00:00 516391 /lib/x86_64-linux-gnu/libc-2.27.so
7ff2c5be0000-7ff2c5dd7000 ---p 000001f0 00:00 516391 /lib/x86_64-linux-gnu/libc-2.27.so
7ff2c5dd7000-7ff2c5ddb000 r--p 001e7000 00:00 516391 /lib/x86_64-linux-gnu/libc-2.27.so
7ff2c5ddb000-7ff2c5ddd000 rw-p 001eb000 00:00 516391 /lib/x86_64-linux-gnu/libc-2.27.so
7ff2c5ddd000-7ff2c5de1000 rw-p 00000000 00:00 0
7ff2c5df0000-7ff2c5df1000 r-xp 00000000 00:00 189022 /mnt/d/wzq/wzq/util/test/lib.so
7ff2c5df1000-7ff2c5df2000 ---p 00001000 00:00 189022 /mnt/d/wzq/wzq/util/test/lib.so
7ff2c5df2000-7ff2c5ff0000 ---p 00000002 00:00 189022 /mnt/d/wzq/wzq/util/test/lib.so
7ff2c5ff0000-7ff2c5ff1000 r--p 00000000 00:00 189022 /mnt/d/wzq/wzq/util/test/lib.so
7ff2c5ff1000-7ff2c5ff2000 rw-p 00001000 00:00 189022 /mnt/d/wzq/wzq/util/test/lib.so
7ff2c6000000-7ff2c6026000 r-xp 00000000 00:00 516353 /lib/x86_64-linux-gnu/ld-2.27.so
7ff2c6026000-7ff2c6027000 r-xp 00026000 00:00 516353 /lib/x86_64-linux-gnu/ld-2.27.so
7ff2c6227000-7ff2c6228000 r--p 00027000 00:00 516353 /lib/x86_64-linux-gnu/ld-2.27.so
7ff2c6228000-7ff2c6229000 rw-p 00028000 00:00 516353 /lib/x86_64-linux-gnu/ld-2.27.so
7ff2c6229000-7ff2c622a000 rw-p 00000000 00:00 0
7ff2c62e0000-7ff2c62e3000 rw-p 00000000 00:00 0
7ff2c62f0000-7ff2c62f2000 rw-p 00000000 00:00 0
7ff2c6400000-7ff2c6401000 r-xp 00000000 00:00 189023 /mnt/d/wzq/wzq/util/test/test
7ff2c6600000-7ff2c6601000 r--p 00000000 00:00 189023 /mnt/d/wzq/wzq/util/test/test
7ff2c6601000-7ff2c6602000 rw-p 00001000 00:00 189023 /mnt/d/wzq/wzq/util/test/test
7fffee96f000-7fffee990000 rw-p 00000000 00:00 0 [heap]
7ffff6417000-7ffff6c17000 rw-p 00000000 00:00 0 [stack]
7ffff729d000-7ffff729e000 r-xp 00000000 00:00 0 [vdso]
可以看到,整個(gè)進(jìn)程虛擬地址空間中,多出了幾個(gè)文件的映射,lib.so和test一樣,它們都是被操作系統(tǒng)用同樣的方法映射到進(jìn)程的虛擬地址空間,只是它們占據(jù)的虛擬地址和長(zhǎng)度不同,從maps里可以看見里面還有l(wèi)ibc-2.27.so,這是C語(yǔ)言運(yùn)行庫(kù),還有一個(gè)ld-2.27.so,這是Linux下的動(dòng)態(tài)鏈接器,動(dòng)態(tài)鏈接器和普通共享對(duì)象一樣被映射到進(jìn)程的地址空間,在系統(tǒng)開始運(yùn)行test前,會(huì)先把控制權(quán)交給動(dòng)態(tài)鏈接器,動(dòng)態(tài)鏈接器完成所有的動(dòng)態(tài)鏈接工作后會(huì)把控制權(quán)交給test,然后執(zhí)行test程序。
當(dāng)鏈接器將Program.o鏈接成可執(zhí)行文件時(shí),這時(shí)候鏈接器必須確定目標(biāo)文件中所引用的func函數(shù)的性質(zhì),如果是一個(gè)定義于其它靜態(tài)目標(biāo)文件中的函數(shù),那么鏈接器將會(huì)按照靜態(tài)鏈接的規(guī)則,將Program.o的func函數(shù)地址進(jìn)行重定位,如果func是一個(gè)定義在某個(gè)動(dòng)態(tài)鏈接共享對(duì)象中的函數(shù),那么鏈接器將會(huì)將這個(gè)符號(hào)的引用標(biāo)記為一個(gè)動(dòng)態(tài)鏈接的符號(hào),不對(duì)它進(jìn)行地址重定位,將這個(gè)過(guò)程留在裝載時(shí)再進(jìn)行。
動(dòng)態(tài)鏈接的方式
動(dòng)態(tài)鏈接有兩種方式:裝載時(shí)重定位和地址無(wú)關(guān)代碼技術(shù)。
裝載時(shí)重定位:在鏈接時(shí)對(duì)所有絕對(duì)地址的引用不作重定位,而把這一步推遲到裝載時(shí)完成,也叫基址重置,每個(gè)指令和數(shù)據(jù)相當(dāng)于模塊裝載地址是固定的,系統(tǒng)會(huì)分配足夠大的空間給裝載模塊,當(dāng)裝載地址確定后,那指令和數(shù)據(jù)地址自然也就確定了。然而動(dòng)態(tài)鏈接模塊被裝載映射到虛擬空間,指令被重定位后對(duì)于每個(gè)進(jìn)程來(lái)講是不同的,沒(méi)有辦法做到同一份指令被多個(gè)進(jìn)程共享,所以指令對(duì)不同的進(jìn)程來(lái)說(shuō)有不同的副本,還是空間浪費(fèi),怎么解決這個(gè)問(wèn)題?使用fPIC方法。
地址無(wú)關(guān)代碼:指令部分無(wú)法在多個(gè)進(jìn)程之間共享,不能節(jié)省內(nèi)存,所以引入了地址無(wú)關(guān)代碼的技術(shù)。我們平時(shí)編程過(guò)程中可能都見過(guò)-fPIC的編譯選項(xiàng),這個(gè)就代表使用了地址無(wú)關(guān)代碼技術(shù)來(lái)實(shí)現(xiàn)真正的動(dòng)態(tài)鏈接?;舅枷刖褪鞘褂肎OT(全局偏移表),這是一個(gè)指向變量或函數(shù)地址的指針數(shù)組,當(dāng)指令要訪問(wèn)變量或者調(diào)用函數(shù)時(shí),會(huì)去GOT中找到相應(yīng)的地址進(jìn)行間接跳轉(zhuǎn)訪問(wèn),每個(gè)變量或函數(shù)都對(duì)應(yīng)一個(gè)地址,鏈接器在裝載模塊的時(shí)候會(huì)查找每個(gè)變量和函數(shù)的地址,然后填充GOT中的各個(gè)項(xiàng),確保每個(gè)指針指向的地址正確。GOT放在數(shù)據(jù)段,所以它可以在模塊裝載時(shí)被修改,并且每個(gè)進(jìn)程都可以有獨(dú)立的副本,相互不受影響。
tips
-fpic和-fPIC的區(qū)別:它們都是地址無(wú)關(guān)代碼技術(shù),-fpic產(chǎn)生的代碼相對(duì)較小較快,但是在某些平臺(tái)會(huì)有些限制,所以大多數(shù)情況下都是用-fPIC來(lái)產(chǎn)生地址無(wú)關(guān)代碼。
-fPIC和-fPIE的區(qū)別:一個(gè)作用于共享對(duì)象,一個(gè)作用于可執(zhí)行文件,一個(gè)以地址無(wú)關(guān)方式編譯的可執(zhí)行文件被稱作地址無(wú)關(guān)可執(zhí)行文件。
-fpie和-fPIE的區(qū)別:類似于-fpic和-fPIC的區(qū)別
延遲綁定技術(shù)
在程序剛啟動(dòng)時(shí)動(dòng)態(tài)鏈接器會(huì)尋找并裝載所需要的共享對(duì)象,然后進(jìn)行符號(hào)地址尋址重定位等工作,這些工作會(huì)減慢程序的啟動(dòng)速度,如果解決?
使用PLT延遲綁定技術(shù),這里會(huì)單獨(dú)有一個(gè)叫.PLT的段,ELF將 GOT拆分成兩個(gè)表.GOT和.GOT.PLT,其中.GOT用來(lái)保存全局變量的引用地址,.GOT.PLT用來(lái)保存外部函數(shù)的地址,每個(gè)外部函數(shù)在PLT中都有一個(gè)對(duì)應(yīng)項(xiàng),在初始化時(shí)不會(huì)綁定,而是在函數(shù)第一次被用到時(shí)才進(jìn)行綁定,將函數(shù)真實(shí)地址與對(duì)應(yīng)表項(xiàng)進(jìn)行綁定,之后就可以進(jìn)行間接跳轉(zhuǎn)。
顯式運(yùn)行時(shí)鏈接
支持動(dòng)態(tài)鏈接的系統(tǒng)往往都支持顯式運(yùn)行時(shí)鏈接,也叫運(yùn)行時(shí)加載,讓程序自己在運(yùn)行時(shí)控制加載的模塊,在需要時(shí)加載需要的模塊,在不需要時(shí)將其卸載。這種運(yùn)行時(shí)加載方式使得程序的模塊組織變得很靈活,可以用來(lái)實(shí)現(xiàn)一些諸如插件、驅(qū)動(dòng)等功能。
通過(guò)這四個(gè)API可以進(jìn)行顯式運(yùn)行時(shí)鏈接:
dlopen():打開動(dòng)態(tài)鏈接庫(kù)
dlsym():查找符號(hào)
dlerror():錯(cuò)誤處理
dlclose():關(guān)閉動(dòng)態(tài)鏈接庫(kù)
參考這段使用代碼:
int main() {
void *handle;
void (*f)(int);
char *error;
handle = dlopen("./lib.so", RTLD_NOW);
if (handle == NULL) {
printf("handle null \n");
return -1;
}
f = dlsym(handle, "func");
do {
if ((error = dlerror()) != NULL) {
printf("error\n");
break;
}
f(100);
} while (0);
dlclose(handle);
return 0;
}
編譯運(yùn)行:
test program.c -ldl gcc -o
test ./
func 100
總結(jié)
為什么要進(jìn)行動(dòng)態(tài)鏈接?為了解決靜態(tài)鏈接浪費(fèi)空間和更新困難的缺點(diǎn)。
動(dòng)態(tài)鏈接的方式?裝載時(shí)重定位和地址無(wú)關(guān)代碼技術(shù)。
地址無(wú)關(guān)代碼技術(shù)原理?通過(guò)GOT段實(shí)現(xiàn)間接跳轉(zhuǎn)。
延遲加載技術(shù)原理?對(duì)外部函數(shù)符號(hào)通過(guò)PLT段實(shí)現(xiàn)延遲綁定及間接跳轉(zhuǎn)。
如果進(jìn)行顯式運(yùn)行時(shí)鏈接?通過(guò)<dlfcn.h>頭文件中的四個(gè)函數(shù),代碼如上。
參考資料
https://www.ibm.com/developerworks/cn/linux/l-dynlink/index.html
http://chuquan.me/2018/06/03/linking-static-linking-dynamic-linking/
https://www.cnblogs.com/tracylee/archive/2012/10/15/2723816.html
《程序員的自我修養(yǎng):鏈接裝載與庫(kù)》
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀點(diǎn),不代表本平臺(tái)立場(chǎng),如有問(wèn)題,請(qǐng)聯(lián)系我們,謝謝!