Linux 為什么要動態(tài)鏈接?與靜態(tài)鏈接的區(qū)別是什么?
掃描二維碼
隨時隨地手機看文章
在前面的文章中程序喵已經(jīng)介紹過靜態(tài)鏈接的原理,這篇文章我們來解密動態(tài)鏈接。
老規(guī)矩,先拋出幾個問題:
為什么要進行動態(tài)鏈接?
如何進行動態(tài)鏈接?
什么是地址無關(guān)代碼技術(shù)?
什么是延遲綁定技術(shù)?
如何在程序運行過程中進行顯式鏈接?
為什么要進行動態(tài)鏈接?
因為靜態(tài)鏈接有缺點:
浪費內(nèi)存和磁盤空間:如下圖,
Program1和Program2分別包含Program1.o和Program2.o兩個模塊,他們都需要Lib.o模塊。靜態(tài)鏈接情況下,兩個目標文件都用到Lib.o這個模塊,所以它們同時在鏈接輸出的可執(zhí)行文件Program1和program2中有副本,同時運行時,Lib.o在磁盤和內(nèi)存中有兩份副本,當系統(tǒng)中有大量類似Lib.o的多個程序共享目標文件時,就會浪費很大空間。
靜態(tài)鏈接對程序的更新部署和發(fā)布很不友好:假如一個模塊依賴20個模塊,當20個模塊其中有一個模塊需要更新時,需要將所有的模塊都找出來重新編譯出一個可執(zhí)行程序才可以更新成功,每次更新任何一個模塊,用戶就需要重新獲得一個非常大的程序,程序如果使用靜態(tài)鏈接,那么通過網(wǎng)絡(luò)來更新程序也會非常不便,一旦程序任何位置有一個小改動,都會導(dǎo)致整個程序重新下載。
為了解決靜態(tài)鏈接的缺點,所以引入了動態(tài)鏈接,動態(tài)鏈接的內(nèi)存分布如圖,
多個程序依賴同一個共享目標文件,這個共享目標文件在磁盤和內(nèi)存中僅有一份,不會產(chǎn)生副本,簡單來講就是不像靜態(tài)鏈接一樣對那些組成程序的目標文件進行鏈接,等到程序要運行時才進行鏈接,把鏈接這個過程推遲到運行時才執(zhí)行。動態(tài)鏈接的方式使得開發(fā)過程中各個模塊更加獨立,耦合度更小,便于不同的開發(fā)者和開發(fā)組織之間獨立的進行開發(fā)和測試。
如何進行動態(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;
}
編譯運行過程如下:
gcc -fPIC -shared -o lib.so lib.c
test Program.c ./lib.so gcc -o
test ./
func 1
通過-fPIC和-shared可以生成一個動態(tài)鏈接庫,再鏈接到可執(zhí)行程序就可以正常運行。
通過readelf命令可以查看動態(tài)鏈接庫的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
可以看見動態(tài)鏈接模塊的裝載地址從0開始,0是無效地址,它的裝載地址會在程序運行時再確定,在編譯時是不確定的。
改一下程序:
// Program.c
void func(int i);
int main() {
func(1);
sleep(-1);
return 0;
}
運行讀取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]
可以看到,整個進程虛擬地址空間中,多出了幾個文件的映射,lib.so和test一樣,它們都是被操作系統(tǒng)用同樣的方法映射到進程的虛擬地址空間,只是它們占據(jù)的虛擬地址和長度不同,從maps里可以看見里面還有l(wèi)ibc-2.27.so,這是C語言運行庫,還有一個ld-2.27.so,這是Linux下的動態(tài)鏈接器,動態(tài)鏈接器和普通共享對象一樣被映射到進程的地址空間,在系統(tǒng)開始運行test前,會先把控制權(quán)交給動態(tài)鏈接器,動態(tài)鏈接器完成所有的動態(tài)鏈接工作后會把控制權(quán)交給test,然后執(zhí)行test程序。
當鏈接器將Program.o鏈接成可執(zhí)行文件時,這時候鏈接器必須確定目標文件中所引用的func函數(shù)的性質(zhì),如果是一個定義于其它靜態(tài)目標文件中的函數(shù),那么鏈接器將會按照靜態(tài)鏈接的規(guī)則,將Program.o的func函數(shù)地址進行重定位,如果func是一個定義在某個動態(tài)鏈接共享對象中的函數(shù),那么鏈接器將會將這個符號的引用標記為一個動態(tài)鏈接的符號,不對它進行地址重定位,將這個過程留在裝載時再進行。
動態(tài)鏈接的方式
動態(tài)鏈接有兩種方式:裝載時重定位和地址無關(guān)代碼技術(shù)。
裝載時重定位:在鏈接時對所有絕對地址的引用不作重定位,而把這一步推遲到裝載時完成,也叫基址重置,每個指令和數(shù)據(jù)相當于模塊裝載地址是固定的,系統(tǒng)會分配足夠大的空間給裝載模塊,當裝載地址確定后,那指令和數(shù)據(jù)地址自然也就確定了。然而動態(tài)鏈接模塊被裝載映射到虛擬空間,指令被重定位后對于每個進程來講是不同的,沒有辦法做到同一份指令被多個進程共享,所以指令對不同的進程來說有不同的副本,還是空間浪費,怎么解決這個問題?使用fPIC方法。
地址無關(guān)代碼:指令部分無法在多個進程之間共享,不能節(jié)省內(nèi)存,所以引入了地址無關(guān)代碼的技術(shù)。我們平時編程過程中可能都見過-fPIC的編譯選項,這個就代表使用了地址無關(guān)代碼技術(shù)來實現(xiàn)真正的動態(tài)鏈接?;舅枷刖褪鞘褂肎OT(全局偏移表),這是一個指向變量或函數(shù)地址的指針數(shù)組,當指令要訪問變量或者調(diào)用函數(shù)時,會去GOT中找到相應(yīng)的地址進行間接跳轉(zhuǎn)訪問,每個變量或函數(shù)都對應(yīng)一個地址,鏈接器在裝載模塊的時候會查找每個變量和函數(shù)的地址,然后填充GOT中的各個項,確保每個指針指向的地址正確。GOT放在數(shù)據(jù)段,所以它可以在模塊裝載時被修改,并且每個進程都可以有獨立的副本,相互不受影響。
tips
-fpic和-fPIC的區(qū)別:它們都是地址無關(guān)代碼技術(shù),-fpic產(chǎn)生的代碼相對較小較快,但是在某些平臺會有些限制,所以大多數(shù)情況下都是用-fPIC來產(chǎn)生地址無關(guān)代碼。
-fPIC和-fPIE的區(qū)別:一個作用于共享對象,一個作用于可執(zhí)行文件,一個以地址無關(guān)方式編譯的可執(zhí)行文件被稱作地址無關(guān)可執(zhí)行文件。
-fpie和-fPIE的區(qū)別:類似于-fpic和-fPIC的區(qū)別
延遲綁定技術(shù)
在程序剛啟動時動態(tài)鏈接器會尋找并裝載所需要的共享對象,然后進行符號地址尋址重定位等工作,這些工作會減慢程序的啟動速度,如果解決?
使用PLT延遲綁定技術(shù),這里會單獨有一個叫.PLT的段,ELF將 GOT拆分成兩個表.GOT和.GOT.PLT,其中.GOT用來保存全局變量的引用地址,.GOT.PLT用來保存外部函數(shù)的地址,每個外部函數(shù)在PLT中都有一個對應(yīng)項,在初始化時不會綁定,而是在函數(shù)第一次被用到時才進行綁定,將函數(shù)真實地址與對應(yīng)表項進行綁定,之后就可以進行間接跳轉(zhuǎn)。
顯式運行時鏈接
支持動態(tài)鏈接的系統(tǒng)往往都支持顯式運行時鏈接,也叫運行時加載,讓程序自己在運行時控制加載的模塊,在需要時加載需要的模塊,在不需要時將其卸載。這種運行時加載方式使得程序的模塊組織變得很靈活,可以用來實現(xiàn)一些諸如插件、驅(qū)動等功能。
通過這四個API可以進行顯式運行時鏈接:
dlopen():打開動態(tài)鏈接庫
dlsym():查找符號
dlerror():錯誤處理
dlclose():關(guān)閉動態(tài)鏈接庫
參考這段使用代碼:
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;
}
編譯運行:
test program.c -ldl gcc -o
test ./
func 100
總結(jié)
為什么要進行動態(tài)鏈接?為了解決靜態(tài)鏈接浪費空間和更新困難的缺點。
動態(tài)鏈接的方式?裝載時重定位和地址無關(guān)代碼技術(shù)。
地址無關(guān)代碼技術(shù)原理?通過GOT段實現(xiàn)間接跳轉(zhuǎn)。
延遲加載技術(shù)原理?對外部函數(shù)符號通過PLT段實現(xiàn)延遲綁定及間接跳轉(zhuǎn)。
如果進行顯式運行時鏈接?通過<dlfcn.h>頭文件中的四個函數(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):鏈接裝載與庫》
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲服務(wù)。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!