Linux 鏈接詳解----動態鏈接庫


靜態庫的缺點:

  1. 庫函數被包含在每一個運行的進程中,會造成主存的浪費。
  2. 目標文件的size過大
  3. 每次更新一個模塊都需要重新編譯,更新困難,使用不方便。

動態庫: 是一個目標文件,包含代碼和數據,它可以在程序運行時動態的加載並鏈接。修改動態庫不需要重新編譯目標文件,只需要更新動態庫即可。動態庫還可以同時被多個進程使用。在linux下生成動態庫 gcc -c a.c  -fPIC -o a.o     gcc -shared -fPIC a.o -o a.so.     這里的PIC含義就是生成位置無關代碼,動態庫允許動態裝入修改,這就必須要保證動態庫的代碼被裝入時,可執行程序不依賴與動態庫被裝入的位置,即使動態庫的長度發生變化也不會影響調用它的程序。

動態鏈接器:

在加載可執行文件時,加載器發現在可執行文件的程序頭表中有.interp段,其中包含了動態連接器路徑ld-linux.so . 加載器加載動態鏈接器,動態鏈接器完成相應的重定位工作后,再將控制權交給可執行文件。

程序頭:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
                 0x00000000000001f8 0x00000000000001f8  R E    8
  INTERP         0x0000000000000238 0x0000000000400238 0x0000000000400238
                 0x000000000000001c 0x000000000000001c  R      1
      [正在請求程序解釋器:/lib64/ld-linux-x86-64.so.2]

位置無關代碼PIC:

其中動態庫用到的一個核心概念就是與代碼無關PIC。共享庫代碼位置可以是不確定的,即使代碼長度發生變化也不影響調用它的程序,動態鏈接器是不會把可執行文件的代碼段數據段與動態鏈接庫合並的。那么這里牽涉到模塊內與模塊間的引用和跳轉問題。

模塊內的跳轉和引用:

目標文件與靜態庫中模塊內的跳轉大致相同。如下代碼:

//b.c
static
int temp = 12344; extern int a; void add(int c) { a += c; temp += c; }

//a.c
int a = 1000;
void add(int c);
int main()
{
    int c = 123;
    add(c);
    return 0;

}

我們將b.c編譯為動態庫,這里的temp就是模塊內的引用,我們將動態庫反編譯可以看到

471 00000000000006a8 <add>:
472  6a8:   55                      push   %rbp
473  6a9:   48 89 e5                mov    %rsp,%rbp
474  6ac:   89 7d fc                mov    %edi,-0x4(%rbp)
475  6af:   48 8b 05 2a 09 20 00    mov    0x20092a(%rip),%rax        # 200fe0 <    _DYNAMIC+0x1d0>
476  6b6:   8b 10                   mov    (%rax),%edx
477  6b8:   8b 45 fc                mov    -0x4(%rbp),%eax
478  6bb:   01 c2                   add    %eax,%edx
479  6bd:   48 8b 05 1c 09 20 00    mov    0x20091c(%rip),%rax        # 200fe0 <    _DYNAMIC+0x1d0>
480  6c4:   89 10                   mov    %edx,(%rax)
481  6c6:   8b 15 5c 09 20 00       mov    0x20095c(%rip),%edx        # 201028 <    temp>
482  6cc:   8b 45 fc                mov    -0x4(%rbp),%eax
483  6cf:   01 d0                   add    %edx,%eax
484  6d1:   89 05 51 09 20 00       mov    %eax,0x200951(%rip)        # 201028 <    temp>


785 0000000000201028 <temp>:             #數據段temp
786   201028:   38 30                   cmp    %dh,(%rax)

這里的mov 0x20095c(%rip),%edx  取rip中下一條指令地址+0x20095c 的數據放到edx寄存器中,地址為0x201028 正是temp的地址。模塊內的函數跳轉與此類似。

模塊間的跳轉和引用:

首先看一下全局變量的引用,看上例中b.so的反編譯 這里引用了一個全局變量a 它的定義在另一個模塊a.o 中

 

471 00000000000006a8 <add>:
472  6a8:   55                      push   %rbp
473  6a9:   48 89 e5                mov    %rsp,%rbp
474  6ac:   89 7d fc                mov    %edi,-0x4(%rbp)
475  6af:   48 8b 05 2a 09 20 00    mov    0x20092a(%rip),%rax        # 200fe0 <    _DYNAMIC+0x1d0>
476  6b6:   8b 10                   mov    (%rax),%edx
477  6b8:   8b 45 fc                mov    -0x4(%rbp),%eax
478  6bb:   01 c2                   add    %eax,%edx
479  6bd:   48 8b 05 1c 09 20 00    mov    0x20091c(%rip),%rax        # 200fe0 <    _DYNAMIC+0x1d0>
764 Disassembly of section .got:
765 
766 0000000000200fd0 <.got>:
767     ...
768 
769 Disassembly of section .got.plt:
770 
771 0000000000201000 <_GLOBAL_OFFSET_TABLE_>:
772   201000:   10 0e                   adc    %cl,(%rsi)
773   201002:   20 00                   and    %al,(%rax)

 

這里的mov 取地址是一個got數組的某個地址。GOT 全局偏移表,在data段的開始處的一個指針數組,每一個指針可以指向一個全局變量, GOT與引用數據的指令之間的相對距離固定,編譯器為GOT每一項生成一個重定位項,加載時 動態鏈接器對GOT中的各項進行重定位,填入引用的地址。(32位 占4個字節  64位 8個字節)

 

每一個引用全局數據的目標模塊都有一張自己的GOT,那么就需要一個額外的寄存器來保持GOT表目的地址。至於模塊間的函數調用和跳轉也可以使用此模塊,但是這種情況下過程調用都要求三條額外的指令,Linux這里就使用了叫做延遲綁定的技術,將過程調用的綁定推遲到第一次調用該過程。這種技術是通過倆個數據結構之間的交互來實現,即GOT 和PLT 全局偏移表和過程鏈接表, 如果一個目標模塊調用定義在共享庫中的任何函數,那么它就有自己的GOT和PLT,GOT是data節的一部分,PLT是text節的一部分<深入理解計算機系統>

上圖為 32 位linux。 從GOT[3]開始才是函數調用地址。我們將a.c b.so編譯鏈接為可執行文件a, 反匯編a 觀察函數add的調用(無關代碼已省略)。(64位機器)

Disassembly of section .plt:

0000000000400580 <add@plt-0x10>:
  400580:    ff 35 82 0a 20 00        pushq  0x200a82(%rip)        # 601008 <_GLOBAL_OFFSET_TABLE_+0x8>
  400586:    ff 25 84 0a 20 00        jmpq   *0x200a84(%rip)        # 601010 <_GLOBAL_OFFSET_TABLE_+0x10>
  40058c:    0f 1f 40 00              nopl   0x0(%rax)

0000000000400590 <add@plt>:
  400590:    ff 25 82 0a 20 00        jmpq   *0x200a82(%rip)        # 601018 <_GLOBAL_OFFSET_TABLE_+0x18> // ////++++++++++++++++++++++
  400596:    68 00 00 00 00           pushq  $0x0
  40059b:    e9 e0 ff ff ff           jmpq   400580 <_init+0x20>

00000000004005a0 <__libc_start_main@plt>:
  4005a0:    ff 25 7a 0a 20 00        jmpq   *0x200a7a(%rip)        # 601020 <_GLOBAL_OFFSET_TABLE_+0x20>
  4005a6:    68 01 00 00 00           pushq  $0x1
  4005ab:    e9 d0 ff ff ff           jmpq   400580 <_init+0x20>


00000000004006b0 <main>:
  4006b0:    55                       push   %rbp
  4006b1:    48 89 e5                 mov    %rsp,%rbp
  4006b4:    48 83 ec 10              sub    $0x10,%rsp
  4006b8:    c7 45 fc 7b 00 00 00     movl   $0x7b,-0x4(%rbp)
  4006bf:    8b 45 fc                 mov    -0x4(%rbp),%eax                
  4006c2:    89 c7                    mov    %eax,%edi                                      
  4006c4:    e8 c7 fe ff ff           callq  400590 <add@plt>           //call add--------------------------------
  4006c9:    b8 00 00 00 00           mov    $0x0,%eax
  4006ce:    c9                       leaveq 
  4006cf:    c3                       retq   


Disassembly of section .got:

0000000000600ff8 <.got>:
    ...

Disassembly of section .got.plt:
#data段  無效反編譯代碼
0000000000601000 <_GLOBAL_OFFSET_TABLE_>:
  601000:   GOT[0]   GOT    64位下每個條目8個字節   32位是4個字節
  601008:   GOT[1] 鏈接器標示信息
  601010  GOT[2]  動態連接器入口地址
601017: 00 96 05 40 00 00 add %dl,0x4005(%rsi) 60101d: 00 00 add %al,(%rax) 60101f: 00 a6 05 40 00 00 add %ah,0x4005(%rsi) 601025: 00 00 add %al,(%rax) 601027: 00 b6 05 40 00 00 add %dh,0x4005(%rsi) 60102d: 00 00 add %al,(%rax) ...

我們可以看到main函數中跳轉call add 跳轉到了地址0x400590 處,執行 jmpq *0x200a82(%rip) 指令 跳轉到 0x601018的地址 ,還是跳到GOT數組中的add位置。這個位置其實就是 jmpq *0x200a82(%rip)的下一條指令地址400596: 68 00 00 00 00 pushq $0x0。我們可以使用gdb看一下。

 

(gdb) disassemble main
Dump of assembler code for function main:
   0x00000000004006b0 <+0>:	push   %rbp
   0x00000000004006b1 <+1>:	mov    %rsp,%rbp
   0x00000000004006b4 <+4>:	sub    $0x10,%rsp
=> 0x00000000004006b8 <+8>:	movl   $0x7b,-0x4(%rbp)
   0x00000000004006bf <+15>:	mov    -0x4(%rbp),%eax
   0x00000000004006c2 <+18>:	mov    %eax,%edi
   0x00000000004006c4 <+20>:	callq  0x400590 <add@plt>
   0x00000000004006c9 <+25>:	mov    $0x0,%eax
   0x00000000004006ce <+30>:	leaveq 
   0x00000000004006cf <+31>:	retq   
End of assembler dump.
(gdb) disassemble 0x400590
Dump of assembler code for function add@plt:
   0x0000000000400590 <+0>:	jmpq   *0x200a82(%rip)        # 0x601018 <add@got.plt> 這個地址就是GOT數組中add對應的那條
   0x0000000000400596 <+6>:	pushq  $0x0
   0x000000000040059b <+11>:	jmpq   0x400580
End of assembler dump.
(gdb) x 0x601018
0x601018 <add@got.plt>:	0x00400596

 

 pushq $0x0 這條指令將add符號的ID壓入棧,然后 jmpq  0x400580 它將會跳轉到PLT[0]接下來的指令就是:

 

0000000000400580 <add@plt-0x10>:
  400580:    ff 35 82 0a 20 00        pushq  0x200a82(%rip)        # 601008 <_GLOBAL_OFFSET_TABLE_+0x8>   鏈接器標示信息入棧  前面還有個ID
  400586:    ff 25 84 0a 20 00        jmpq   *0x200a84(%rip)        # 601010 <_GLOBAL_OFFSET_TABLE_+0x10>  跳轉到鏈接器入口地址
  40058c:    0f 1f 40 00              nopl   0x0(%rax)

 

跳轉到動態連接器之后,鏈接器根據這兩個棧中的信息(其實就是重定位描述符地址和索引值)得到 add的實際地址, 在將地址放入GOT[3]中 通過gdb詳細看一下。

(gdb) n
6        add(c);  運行  add之前
(gdb) x /32x 0x601010
0x601010:    0xf7df0210    0x00007fff    0x00400596 0x00000000  
這時GOT[3]add地址還不是add的實際地址 而是0000000000400590<add@plt>: jmp的目的地址0x00400596
0x601020 <__libc_start_main@got.plt>: 0xf7839a20 0x00007fff 0x004005b6 0x00000000 0
x601030: 0x00000000 0x000003e8 0x00000000 0x00000000 0x601040: 0x00000000 0x00000000 0x00000000 0x00000000 0x601050: 0x00000000 0x00000000 0x00000000 0x00000000 0x601060: 0x00000000 0x00000000 0x00000000 0x00000000 0x601070: 0x00000000 0x00000000 0x00000000 0x00000000 0x601080: 0x00000000 0x00000000 0x00000000 0x00000000 (gdb) print add $2 = {<text variable, no debug info>} 0x7ffff7bd96a8 <add>   add函數的地址 (gdb) n 7 return 0; (gdb) x /32x 0x601010 0x601010: 0xf7df0210 0x00007fff 0xf7bd96a8 0x00007fff  
進入add GOT[3]中地址變為了 add的實際地址 再之后的
0000000000400590 <add@plt>: jmpq *0x200a82(%rip) 就會直接跳到add的地址 開始執行指令。

0x601020 <__libc_start_main@got.plt>: 0xf7839a20 0x00007fff 0x004005b6 0x00000000 0
x601030: 0x00000000 0x00000463 0x00000000 0x00000000 0x601040: 0x00000000 0x00000000 0x00000000 0x00000000 0x601050: 0x00000000 0x00000000 0x00000000 0x00000000 0x601060: 0x00000000 0x00000000 0x00000000 0x00000000 0x601070: 0x00000000 0x00000000 0x00000000 0x00000000 0x601080: 0x00000000 0x00000000 0x00000000 0x00000000

這里可以看到被引用的函數調用之前 GOT中的地址並沒有被值為函數的實際地址。之后實際地址就被裝入,跳轉到相應地址開始執行,之后就可以直接跳轉運行了,只需要一個跳轉指令即可。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM