在Linux系統(tǒng)開(kāi)發(fā)中,我們頻繁的使用動(dòng)態(tài)庫(kù)(又稱共享庫(kù)),它相較于靜態(tài)庫(kù)而言有節(jié)省空間、便于更新等優(yōu)點(diǎn)。但同時(shí),動(dòng)態(tài)庫(kù)也有其缺點(diǎn),加載速度相較于靜態(tài)庫(kù)而言較慢。那么,為什么調(diào)用動(dòng)態(tài)庫(kù)內(nèi)的函數(shù)要比調(diào)用靜態(tài)庫(kù)內(nèi)函數(shù)速度慢呢?它的加載過(guò)程具體又是怎樣的呢?我們可借助gdb調(diào)試工具和反匯編工具objdump來(lái)找尋原因。
首先準(zhǔn)備簡(jiǎn)單的動(dòng)態(tài)庫(kù)測(cè)試函數(shù):
準(zhǔn)備測(cè)試程序:
借助gcc工具生成動(dòng)態(tài)庫(kù),鏈接動(dòng)態(tài)庫(kù),編譯生成可執(zhí)行文件,并幫助動(dòng)態(tài)鏈接器指定動(dòng)態(tài)庫(kù)加載位置。
-
gcc -c -fPIC add.c sub.c mul.c
-
gcc -shared -o libmymath.so add.o sub.o mul.o
-
gcc main.c -o app -L ./ -l mymath -I ./
-
export LD_LIBRARY_PATH=./
接下來(lái),我們來(lái)研究下,在 main.c 中調(diào)用共享庫(kù)的函數(shù) add是如何實(shí)現(xiàn)的。首先反匯編看一下動(dòng)態(tài)庫(kù)libmymath.so,方便后期數(shù)據(jù)比對(duì)。(由于數(shù)據(jù)較多,這里只保留了與后期分析相關(guān)聯(lián)的部分,同時(shí)為了方便觀察地址,我們以32位系統(tǒng)為例。)
$ objdump libmymath.so -dS
...
00000538 <add>:
538: 55 push %ebp
539: 89 e5 mov %esp,%ebp
53b: 8b 45 0c mov 0xc(%ebp),%eax
53e: 8b 55 08 mov 0x8(%ebp),%edx
541: 01 d0 add %edx,%eax
543: 5d pop %ebp
544: c3 ret
Disassembly of section .fini:
...
然后我們反匯編一下可執(zhí)行文件app的指令:
$ objdump -dS app
...
Disassembly of section .plt:
08048460 <add@plt-0x10>:
8048460: ff 35 04 a0 04 08 pushl 0x804a004
8048466: ff 25 08 a0 04 08 jmp *0x804a008
804846c: 00 00 add %al,(%eax)
...
08048470 <add@plt>:
8048470: ff 25 0c a0 04 08 jmp *0x804a00c
8048476: 68 00 00 00 00 push $0x0
804847b: e9 e0 ff ff ff jmp 8048460 <_init+0x2c>
...
080485cd <main>:
int main(void)
{
80485cd: 55 push %ebp
80485ce: 89 e5 mov %esp,%ebp
80485d0: 83 e4 f0 and $0xfffffff0,%esp
80485d3: 83 ec 20 sub $0x20,%esp
int a = 5;
80485d6: c7 44 24 18 05 00 00 movl $0x5,0x18(%esp)
80485dd: 00
int b = 9;
80485de: c7 44 24 1c 09 00 00 movl $0x9,0x1c(%esp)
80485e5: 00
printf("%d + %d = %d\n", a, b, add(a, b));
80485e6: 8b 44 24 1c mov 0x1c(%esp),%eax
80485ea: 89 44 24 04 mov %eax,0x4(%esp)
80485ee: 8b 44 24 18 mov 0x18(%esp),%eax
80485f2: 89 04 24 mov %eax,(%esp)
80485f5: e8 76 fe ff ff call 8048470 <add@plt>
...
從上述反匯編結(jié)果來(lái)看add 函數(shù)并沒(méi)有直接鏈接到可執(zhí)行文件中。而且 call 8048470 <add@plt>這條指令調(diào)用的也不是 add 函數(shù)的地址。共享庫(kù)是位置無(wú)關(guān)代碼,在運(yùn)行時(shí)可以加載到任意地址,其加載地址只有在動(dòng)態(tài)鏈接時(shí)才能確定,所以在 main 函數(shù)中不可能直接通過(guò)絕對(duì)地址調(diào)用add函數(shù),而是通過(guò)間接尋址來(lái)找 add 函數(shù)的。
對(duì)照上面的指令,我們使用 gdb 跟蹤一下:
$ gdb app
...
(gdb) start
Temporary breakpoint 1 at 0x80485d6: file main.c, line 6.
Starting program: /home/itcast/lib/app
Temporary breakpoint 1, main () at main.c:6
6 int a = 5;
(gdb) si
7 int b = 9;
(gdb) si
9 printf("%d + %d = %d\n", a, b, add(a, b));
(gdb) si
0x080485ea 9 printf("%d + %d = %d\n", a, b, add(a, b));
(gdb) si
0x080485ee 9 printf("%d + %d = %d\n", a, b, add(a, b));
(gdb) si
0x080485f2 9 printf("%d + %d = %d\n", a, b, add(a, b));
(gdb) si
0x080485f5 9 printf("%d + %d = %d\n", a, b, add(a, b));
(gdb) si
0x08048470 in add@plt ()
跳轉(zhuǎn)到 .plt 段中,現(xiàn)在將要執(zhí)行一條 jmp *0x804a00c指令,我們看看0x804a00c這個(gè)地址里存的是什么:
(gdb) x 0x804a00c
0x804a00c <add@got.plt>: 0x08048476
對(duì)應(yīng)app反匯編結(jié)果,我們發(fā)現(xiàn)原來(lái)0x08048476就是其下一條指令push $0x0的地址。好,繼續(xù)跟蹤下去:
(gdb) si
0x08048470 in add@plt ()
(gdb) si
0x08048476 in add@plt ()
(gdb) si
0x0804847b in add@plt ()
(gdb) si
0x08048460 in ?? ()
(gdb) si
0x08048466 in ?? ()
(gdb) si
0xf7ff04f0 in ?? () from /lib/ld-linux.so.2
最終進(jìn)入了動(dòng)態(tài)鏈接器 /lib/ld-linux.so.2 ,在其中完成動(dòng)態(tài)鏈接的過(guò)程并調(diào)用 add 函數(shù),我們不深入這些細(xì)節(jié)了,直接用 finish 命令返回到 main 函數(shù):
(gdb) si
0xf7ff04f2 in ?? () from /lib/ld-linux.so.2
(gdb) finish
Run till exit from #0 0xf7ff04f2 in ?? () from /lib/ld-linux.so.2
0x080485fa in main () at main.c:9
9 printf("%d + %d = %d\n", a, b, add(a, b));
這時(shí),再來(lái)看看0x804a00c這個(gè)地址里保存的是什么:
(gdb) x 0x804a00c
0x804a00c <add@got.plt>: 0xf7fd4538
(gdb) x 0xf7fd4538
0xf7fd4538 <add>: 0x8be58955
我們發(fā)現(xiàn)0x804a00c中不再保存其下一條指令push $0x0的地址,而存入了一個(gè)新的地址,繼續(xù)跟蹤這個(gè)地址找到了add函數(shù)真正被加載到內(nèi)存的位置。其中的0x8be58955正對(duì)應(yīng)文檔開(kāi)頭反匯編動(dòng)態(tài)庫(kù)所得到的add函數(shù)前三條指令。由于我們所使用的計(jì)算機(jī)采用小端法存儲(chǔ),所以低位保存在低字節(jié)上。
動(dòng)態(tài)鏈接器已經(jīng)把 add 函數(shù)的地址存在這里了,所以下次再調(diào)用 add 函數(shù)就可以直接從 jmp *0x804a00c 指令直接跳到它首條指令的地址,而不必再進(jìn)入 /lib/ld-linux.so.2 做動(dòng)態(tài)鏈接了。
我們首次查看0x804a00c的時(shí)候,其內(nèi)部并沒(méi)有保存add函數(shù)實(shí)際的地址。而當(dāng)函數(shù)被調(diào)用,動(dòng)態(tài)鏈接器加載完成,會(huì)將add真正加載至內(nèi)存的地址填寫到與plt對(duì)應(yīng)的got中。有一種描述這種綁定動(dòng)態(tài)庫(kù)函數(shù)的方式,稱之為“延遲綁定”。正是由于首次調(diào)用的這一延遲,導(dǎo)致調(diào)用動(dòng)態(tài)庫(kù)函數(shù)不像調(diào)用靜態(tài)庫(kù)函數(shù)那樣快捷。
本文版權(quán)歸黑馬程序員C/c++培訓(xùn)學(xué)院所有,歡迎轉(zhuǎn)載,轉(zhuǎn)載請(qǐng)注明作者出處。謝謝!作者:黑馬程序員C/C++培訓(xùn)學(xué)院首發(fā):http://c.itheima.com