天天看點

elf動态連結執行個體

小知識:

readelf -s 既能顯示靜态符号.symtab, 也能顯示動态符号.dynsym

動态符号.dynsym 中包含導出函數表,導入函數表.

strip 隻會删除靜态符号.symtab 及對應的.strtab, 并不會删除動态符号.dynsym及對應的.dynstr

注意啊,so或exe檔案可以strip, 但連結式檔案不能strip, 因為它們沒有的靜态符号就成廢的檔案了,将不能參與靜态連接配接了.

問題: 當運作檔案調用動态連接配接庫的函數時, 它是怎樣找到函數的位址的呢? 下面就來研究一下:

1.建立測試檔案

建立檔案common.c

int val = 1; 
int func(void) 
{ 
    return (val+10); 
} 
           

建立檔案test.c

extern int val; 
extern int func(void); 

int main() 
{ 
    val = 10; 
    func(); 
    return 0; 
} 
           

2.編譯

Makefile 如下:

all:  test
test: test.o common.so
    gcc -g -o $@ test.o ./common.so

common.so: common.c
    gcc -shared -fPIC -o $@ $<

%.o:%.c
    gcc -g -c -o $@ $<

clean:
    rm *.o common.so test
           

3. test 可執行檔案分析

反彙編
           

可執行程式如何通路動态連接配接庫中的變量和函數的呢?

我們看它的反彙編代碼!

objdump -S test > test.S 
           
cd <main>:
extern int val; 
extern int func(void); 

int main() 
{ 
  cd:                         push   %rbp
  ce:     e5                mov    %rsp,%rbp
  val = ; 
  d1:   c7   09   0a    movl   $0xa,(%rip)        # 601040 <__TMC_END__>
  d8:      
  func(); 
  db:   e8 f fe ff ff          callq  d <func@plt>
  return ; 
  e:   b8              mov    $0x,%eax
} 
  e5:   d                      pop    %rbp
  e6:   c3                      retq   
  e7:    0f f        nopw   (%rax,%rax,)
  ee:     
           

val 位址是601040

func 位址是4005d0

查test各段位址, 601040落入bss節, 看來動态連結庫的全局變量會在可執行檔案的.bssx節留有副本.

4005d0 屬于.plt節, .plt節已經被反彙編成代碼,容易檢視

Disassembly of section .plt:

a <__libc_start_main@plt->:
  a:   ff   0a         pushq  (%rip)        # 601008 <_GLOBAL_OFFSET_TABLE_+0x8>
  a6:   ff   0a         jmpq   *0x200a64(%rip)        # 601010 <_GLOBAL_OFFSET_TABLE_+0x10>
  ac:   0f f               nopl   (%rax)

b <__libc_start_main@plt>:
  b:   ff   0a         jmpq   *0x200a62(%rip)        # 601018 <_GLOBAL_OFFSET_TABLE_+0x18>
  b6:                 pushq  $0x
  bb:   e9 e ff ff ff          jmpq   a <_init+>

c <__gmon_start_@plt>:
  c:   ff  a 0a         jmpq   *0x200a5a(%rip)        # 601020 <_GLOBAL_OFFSET_TABLE_+0x20>
  c6:                 pushq  $0x1
  cb:   e9 d ff ff ff          jmpq   a <_init+>

d <func@plt>:
  d:   ff   0a         jmpq   *0x200a52(%rip)        # 601028 <_GLOBAL_OFFSET_TABLE_+0x28>
  d6:                 pushq  $0x2
  db:   e9 c ff ff ff          jmpq   a <_init+>
           

第一條指令,向601028所存儲的位址跳躍.

601028屬于.got.plt節,

$ readelf -x .got.plt test

Hex dump of section '.got.plt':
       ..`.............
     b6054000  ..........@.....
   c6054000  d6054000  ..@.......@.....
           

601028存儲資料為4005d6, 周轉半天原來執行的還是[email protected] 的下一條指令.

繼續向下,把2 push到堆棧,跳轉到4005a0.

4005a0 是plt的第一個入口.

它向堆棧中推入601008,跳轉到601010存儲的位址

這兩個資料都在.got.plt 中, 目前顯示全為0, 這兩個位址顯然應該由動态連接配接程式

要填上正确的位址才能執行.

現在再看看.got.plt, 每8byte為一個entry, 4,5,6 3個entry 分别指向.plt 的2,3,4入口

經查資料知,第一個入口600e18是.dynamic 位址, 第2個入口是module id, 第3個入口是

_dl_runtime_resolve()函數的入口,這兩個現在全是0, 動态加載器會初始化它們.

調用動态庫函數為什麼這麼複雜呢? 我忽然領悟原來這就是傳說中的”延時bind”.

調用_dl_runtime_resolve()被bind 之後, 就會修改.got.plt的對應入口項, 下一次調用就

直接拿到了調用函數位址了, 而不是.plt的第一個入口進行位址解析.

4. 調試跟蹤記憶體的變化

用gdb 或 ida 可以調試跟蹤,下面就以gdb為例子吧. gdb 很強大!

gdb test

b main

r

程式斷下來了, 我們關心什麼呢?

1. val 變量的位址.

(gdb) p &val

$3 = (int *) 0x601040

  1. func 位址,

    p func, gdb 一直列印0x7fff f7bd86a5,是以這個位址是被gdb掩蓋住了.

    當執行到func()時, 你可以用si 指令單步跟入,就可以繼續檢視所關心的位址了!

  2. .got.plt 内容是如何變化的.

執行func()之前, 可見module_id, resolv()都已填好.

[email protected] 也已填好位址, 因為__libc_start_main()函數已經被調用過了.

gmon_start@plt 沒有填好

[email protected] 沒有填好

(gdb) x/x 
:         
:         
:         


(gdb) x/x 
 <[email protected]>:          
           

執行func()之後

(gdb) x/x 
 <[email protected]>:          
           

從readelf -h test 中看程式入口點是0x4005e0, 從反彙編中看這是.text開始位置, _start符号位址,

可見程式從start 開始執行而不是main符号, _start 會調用 _libc函數[email protected], 而後者是庫函數,

它會調用到main

好了,上面講清楚了.got.plt 和 .plt, 就知道了動态binding, 延遲加載的含義和為什麼動态調用是這樣的.

5.下面來點理論總結

  1. 如果elf檔案需要動态加載, 那麼elf檔案中要指明動态加載器, 這由.interp 節指明
    $ readelf -p .interp test
    
    String dump of section '.interp':
      [     ]  /lib64/ld-linux-x86-so
               
  2. 動态加載器首先要完成自舉,就是自己加載自己,這個不是我們關心的.
  3. 然後加載器加載我們需要的共享對象.

    可執行檔案的.dynamic段中的DT_NEEDED入口下,記錄了該可執行檔案所依賴的共享對象。

    $readelf -d test |grep NEEDED

    0x0000000000000001 (NEEDED) Shared library: [./common.so]

    0x0000000000000001 (NEEDED) Shared library: [libc.so.6]

    動态連結器将所有依賴的共享對象裝載到記憶體,并将符号表合并到全局符号表中,

    是以當所有的共享對象都被裝載進來的時候,全局符号表中包含了程序中所有動态連結所需要的符号。

  4. 各對象重定位和初始化

    連結器已經擁有了程序的全局符号表, 然後對可執行檔案和共享對象的 .got/.got.plt等的每個需要重定位的位置進行修正。

    重定位完成之後,如果共享對象有.init段,那麼動态連結器會執行.init段中的代碼,用以實作共享對象特有的初始化過程。

    當初始化都完成之後,連結器将程序的控制權轉交給程式入口并開始執行。

    程式的入口是_start, 它調用libc的__libc_start_main, 後者調用可執行檔案從main

6.總結2

  1. 符号的本質是什麼?

    符号的本質是符号化數值.

    編譯程式在編譯階段, 對于某些資料或函數的引用還不能确定其位址,隻能留給下一步去完成,

    這時候就需要引入符号, 符号是有名稱的,此時符号的位址全部給0, 待符号值确定之後,再修改為正确的位址值.

  2. 重定位的本質是什麼?

    重定位的本質,就是要修改引用,就是要修改代碼段和資料段的内容,當然,它的修改是有依據的,這就是重定位表.

    修改意味着,代碼和資料是不能共享的,因為共享的條件是:

    一塊區域, 其他的程式都可以引用它,而不能修改它.否則就會影響别的程式對它的使用.

可執行檔案對資料和函數的引用可以通過.rel.text和.rel.data表,修正.text, .data中的調用部分

但是,共享庫中不能包含.rela.text, .rela.data, 因為它要共享,區塊不能被修改,

那如果它調用外部函數和外部資料,又是如何做到的呢?

3.共享庫對全局資料的通路.

不管是内部資料還是外部資料,隻要是全局的,都按外部資料處理.

在共享對象中引入一個段叫.got, 全局偏移表. 它儲存的是每一個符号的位址,由于這個符号位址目前還不知道,是以先全部填充為0, 具體的符号位址由加載器來填充. 這個是不是可以叫做動态重定位?!

這樣,共享庫對資料的通路就可以固定了下來. 它是通過先取到位址,再取到資料兩個步驟來通路資料的.

加載器要根據動态重定位資料節.rela.dyn 來修改為符号位址

如果動态加載器遇到兩個同名的全局符号怎麼辦?

如果遇到同名符号,一種簡單的辦法是忽略後解析到的同名符号,這種現象叫做全局符号介入。

這意味着,後加載的子產品定義的變量可能會被前面加載的子產品的變量所覆寫.

4.共享庫對全局函數的通路.

和資料一樣,共享對象不能使用相對位址通路全局函數,也是要分兩步,先拿到全局函數位址,再調用函數.

這個表叫.got.plt 表, 用來儲存全局函數的位址.

共享庫還采用了一種延遲綁定技術,引入了.plt 節,

.plt 節中儲存有簡短的跳轉到函數位址的代碼,其中的函數位址,就是.got.plt表的内容, 但.got.plt表的位址加載器并沒有解析, 而是保留着它的初始值. 這樣執行.plt 中的代碼将會調用的.plt後部分,解析這個函數位址的代碼,并把結果傳回到.got.plt表項中, 相當于重定位了函數位址. 以後再調用就不會解析函數位址了,而是直接調用函數.這種在運作時再确定函數位址的方法叫延遲綁定.

好處是節省了初始化時對導入函數的binding時間

對函數的調用,也有全局符号介入問題,即後出現的同名函數會被忽略,不能進入函數位址表中.