首頁技術(shù)文章正文

C/c++培訓(xùn)之Linux系統(tǒng)動態(tài)庫加載過程分析

更新時間:2017-07-02 來源:黑馬程序員c/c++培訓(xùn)學(xué)院 瀏覽量:

在Linux系統(tǒng)開發(fā)中,我們頻繁的使用動態(tài)庫(又稱共享庫),它相較于靜態(tài)庫而言有節(jié)省空間、便于更新等優(yōu)點(diǎn)。但同時,動態(tài)庫也有其缺點(diǎn),加載速度相較于靜態(tài)庫而言較慢。那么,為什么調(diào)用動態(tài)庫內(nèi)的函數(shù)要比調(diào)用靜態(tài)庫內(nèi)函數(shù)速度慢呢?它的加載過程具體又是怎樣的呢?我們可借助gdb調(diào)試工具和反匯編工具objdump來找尋原因。
 
首先準(zhǔn)備簡單的動態(tài)庫測試函數(shù):
  
準(zhǔn)備測試程序:
借助gcc工具生成動態(tài)庫,鏈接動態(tài)庫,編譯生成可執(zhí)行文件,并幫助動態(tài)鏈接器指定動態(tài)庫加載位置。
  1. gcc -c -fPIC add.c sub.c mul.c
  2. gcc -shared -o libmymath.so add.o sub.o mul.o
  3. gcc main.c -o app -L ./ -l mymath -I ./
  4. export LD_LIBRARY_PATH=./
 
接下來,我們來研究下,在 main.c 中調(diào)用共享庫的函數(shù) add是如何實(shí)現(xiàn)的。首先反匯編看一下動態(tài)庫libmymath.so,方便后期數(shù)據(jù)比對。(由于數(shù)據(jù)較多,這里只保留了與后期分析相關(guān)聯(lián)的部分,同時為了方便觀察地址,我們以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é)果來看add 函數(shù)并沒有直接鏈接到可執(zhí)行文件中。而且 call  8048470 <add@plt>這條指令調(diào)用的也不是 add 函數(shù)的地址。共享庫是位置無關(guān)代碼,在運(yùn)行時可以加載到任意地址,其加載地址只有在動態(tài)鏈接時才能確定,所以在 main 函數(shù)中不可能直接通過絕對地址調(diào)用add函數(shù),而是通過間接尋址來找 add 函數(shù)的。
對照上面的指令,我們使用 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這個地址里存的是什么:
 
(gdb) x 0x804a00c
0x804a00c <add@got.plt>:  0x08048476
 
對應(yīng)app反匯編結(jié)果,我們發(fā)現(xiàn)原來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)入了動態(tài)鏈接器 /lib/ld-linux.so.2 ,在其中完成動態(tài)鏈接的過程并調(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));
 
這時,再來看看0x804a00c這個地址里保存的是什么:
 
(gdb) x 0x804a00c
0x804a00c <add@got.plt>:  0xf7fd4538
(gdb) x 0xf7fd4538
0xf7fd4538 <add>: 0x8be58955
 
我們發(fā)現(xiàn)0x804a00c中不再保存其下一條指令push  $0x0的地址,而存入了一個新的地址,繼續(xù)跟蹤這個地址找到了add函數(shù)真正被加載到內(nèi)存的位置。其中的0x8be58955正對應(yīng)文檔開頭反匯編動態(tài)庫所得到的add函數(shù)前三條指令。由于我們所使用的計算機(jī)采用小端法存儲,所以低位保存在低字節(jié)上。
動態(tài)鏈接器已經(jīng)把 add 函數(shù)的地址存在這里了,所以下次再調(diào)用 add 函數(shù)就可以直接從 jmp  *0x804a00c 指令直接跳到它首條指令的地址,而不必再進(jìn)入 /lib/ld-linux.so.2 做動態(tài)鏈接了。
我們首次查看0x804a00c的時候,其內(nèi)部并沒有保存add函數(shù)實(shí)際的地址。而當(dāng)函數(shù)被調(diào)用,動態(tài)鏈接器加載完成,會將add真正加載至內(nèi)存的地址填寫到與plt對應(yīng)的got中。有一種描述這種綁定動態(tài)庫函數(shù)的方式,稱之為“延遲綁定”。正是由于首次調(diào)用的這一延遲,導(dǎo)致調(diào)用動態(tài)庫函數(shù)不像調(diào)用靜態(tài)庫函數(shù)那樣快捷。

本文版權(quán)歸黑馬程序員C/c++培訓(xùn)學(xué)院所有,歡迎轉(zhuǎn)載,轉(zhuǎn)載請注明作者出處。謝謝!
作者:黑馬程序員C/C++培訓(xùn)學(xué)院
首發(fā):http://c.itheima.com
分享到:
在線咨詢 我要報名
和我們在線交談!