i386 Linux下Elf動態連結分析

來源:互聯網
上載者:User

分析Palm的so檔案用到了裡面的__i686.get_pc_thunk.bx,所以轉載一篇來自http://www.cs.virginia.edu/~wh5a/blog/i386%20Linux%E4%B8%8B%20ELF%20%E5%8A%A8%E6%80%81%E9%93%BE%E6%8E%A5%E5%88%86%E6%9E%90%20%EF%BC%88%E4%B8%80%EF%BC%89.html的文章。

 

Ian Lance Taylor正在寫連載文章介紹linkers。
一直想瞭解dynamic linking的過程,於是正好就學習了一下。
program loader、program linker和dynamic linker的具體工作過程暫不在討論範圍。
首先,隨便寫個小程式然後用objdump看一下:

$ objdump -d main.o
00000000 :
0: 8d 4c 24 04 lea 0x4(%esp),%ecx
4: 83 e4 f0 and $0xfffffff0,%esp
7: ff 71 fc pushl 0xfffffffc(%ecx)
a: 55 push %ebp
b: 89 e5 mov %esp,%ebp
d: 51 push %ecx
e: 83 ec 14 sub $0x14,%esp
11: e8 fc ff ff ff call 12
16: 89 04 24 mov %eax,(%esp)
19: e8 fc ff ff ff call 1a
1e: e8 fc ff ff ff call 1f
23: 89 44 24 04 mov %eax,0x4(%esp)
27: c7 04 24 00 00 00 00 movl $0x0,(%esp)
2e: e8 fc ff ff ff call 2f
33: 83 c4 14 add $0x14,%esp
36: 59 pop %ecx
37: 5d pop %ebp
38: 8d 61 fc lea 0xfffffffc(%ecx),%esp
3b: c3 ret

我們看到main call了一些函數,但地址都是12,1a這樣的數字。這些數字表示的是本.text section中的offset,需要被linker patch。這些relocations資訊可以用readelf -r查看:

$ readelf -r main.o
Relocation section '.rel.text' at offset 0x388 contains 5 entries:
Offset Info Type Sym.Value Sym. Name
00000012 00000902 R_386_PC32 00000000 foo
0000001a 00000a02 R_386_PC32 00000000 printf
0000001f 00000b02 R_386_PC32 00000000 bar
0000002a 00000501 R_386_32 00000000 .rodata
0000002f 00000a02 R_386_PC32 00000000 printf

這些函數地址在link時會被patch成真正的地址(靜態聯入),或者在plt中的地址(動態聯入)。我們readelf -r a.out還能看到列印出如下資訊,這些資訊我們在後面還會看到有所呼應:

$ readelf -r a.out
Relocation section '.rel.dyn' at offset 0x34c contains 1 entries:
Offset Info Type Sym.Value Sym. Name
08049744 00000206 R_386_GLOB_DAT 00000000 __gmon_start__
Relocation section '.rel.plt' at offset 0x354 contains 5 entries:
Offset Info Type Sym.Value Sym. Name
08049754 00000107 R_386_JUMP_SLOT 00000000 bar
08049758 00000207 R_386_JUMP_SLOT 00000000 __gmon_start__
0804975c 00000407 R_386_JUMP_SLOT 00000000 __libc_start_main
08049760 00000507 R_386_JUMP_SLOT 00000000 foo
08049764 00000607 R_386_JUMP_SLOT 00000000 printf

以動態串連方式產生的可執行檔會在.interp這個section中寫入dynamic loader的路徑。例如:

wh5a@power3 /tmp/dyn $ readelf -S a.out|grep interp
[ 1] .interp PROGBITS 08048134 000134 000013 00 A 0 0 1
wh5a@power3 /tmp/dyn $ gdb -q a.out
(gdb) x/s 0x8048134
0x8048134: "/lib/ld-linux.so.2"

這樣的話在程式被載入時,dynamic loader將會被調用以便裝入程式依賴的動態庫。
下面來看foo函數是如何被調用的:
0x080484e5 : call 0x80483d4
我們前面提到從main中call的函數地址都需要被patch,這個過程被static linker做了第一步,使得call指令指向plt中。第二步的patch預設時將會在動態時按需進行,也就是lazy symbol binding。這一行為可以通過環境變數LD_BIND_NOW=1進行修改,這樣在debugging時會有些協助。plt的基地址可以使用readelf -S看出來,got也是很重要的資訊,在此一併列出:

[11] .plt PROGBITS 08048394 000394 000060 04 AX 0 0 4
[21] .got PROGBITS 08049744 000744 000004 04 WA 0 0 4
[22] .got.plt PROGBITS 08049748 000748 000020 04 WA 0 0 4

plt每個entry是16 bytes,got每個entry是4 bytes。
PLT0的資訊比較特殊(這裡把具體的地址列出以便理解):

push GOT[1] ; 0x804974c
jmp GOT[2] ; *0x8049750
0x00000000 ; padding

這之後的plt表項對應於每個動態函數。它們的順序與readelf -r列出的順序相同。我們知道foo是第4個函數,也就是PLT[4],通過計算地址知道是0x80483d4,確實就是main函數call它的地址。
其中內容為:

PLT4:
080483d4 :
80483d4: ff 25 60 97 04 08 jmp *0x8049760 ; GOT[6]
80483da: 68 18 00 00 00 push $0x18 ; foo's relocation offset
80483df: e9 b0 ff ff ff jmp 8048394 ; PLT0

PLT entry的第一條指令跳轉到GOT中,也就是說GOT起到了又一層indirection的作用。GOT的每個表項被初始化為指向到PLT entry的第二條指令。
這裡PLT4對應於GOT6(還記得readelf -r顯示的資訊嗎?找到0x8049760了嗎?)是因為GOT[0..2]都有特殊的作用(GOT[0]似乎指向.dynamic section,存放的是給dynamic loader有用的一些資訊 [5])。那麼這個GOT的基址又是怎麼來的呢?它對應於.got.plt這個section。.got section存放的應該是global variables,還有待繼續研究。
初始狀態下,foo尚未resolve,所以GOT尚未被dynamic linker patch。這樣一來,GOT6使得0x80483da這條指令被執行。這條指令的作用是將foo所對應的offset壓棧。接下來跳到PLT0繼續執行。
PLT0首先將GOT1中的內容(指向一個link_map結構)壓棧,然後跳到GOT2繼續執行。GOT[2]指向_dl_runtime_resolve函數。這個函數是由dynamic linker提供的,通過查看proc檔案系統的maps檔案也可以看出GOT[2]確實指向的是/lib/ld-2.6.so的地址空間。這個函數是很簡單的一段彙編,用來建立必要的堆棧環境以便讓_dl_fixup(源碼在glibc/elf/dl-runtime.c)來完成真正的工作。前面我們壓入了兩個參數,一個是GOT[1]的內容,也就是一個link_map的地址,另一個是待解決symbol的offset,在這裡是0x18。這之後的詳細工作過程參見源碼及[2]。這個函數最終將會patch GOT[6],使得下次PLT4(也就是foo)再次被調用時可以直接取到foo的真正地址。接下來,_dl_fixup返回foo的真正地址給_dl_runtime_resolve,它將傳回值放到棧頂,xchg %eax,(%esp),然後直接一個ret就跳到了foo了。
最後再總結一下.got.plt的作用:

.got.plt (0x8049748)
0x0804966c GOT[0], .dynamic
0x00ba3650 GOT[1], the link map
0x00b9a2b0 GOT[2], always jump here to resolve symbols. /lib/ld-linux.so.2 is loaded here.
...
0x080483da GOT[6], not resolved yet, so points right back to the instruction after the jmp

References:
[1] Linkers part 4
[2] ELF動態解析符號過程(修訂版)
[3] How to hijack the Global Offset Table with pointers for root shells
[4] The ELF Object File Format: Introduction
[5] The ELF Object File Format by Dissection
[6] Before main() 分析

前面分析的是從應用程式調用動態庫的情況。動態庫本身是怎麼完成符號解析的呢?
根據Ian Lance Taylor的說法,最好是以PIC(-fpic)的方式編譯shared lib,不這樣也可以,但會增加dynamic linker做重定位的負擔。以PIC方式編譯的lib可以大量減少必要的relocation info,但調用non-static functions和訪問global/static variables的時候都需要通過plt/got間接進行 (All problems in computer science can be solved by another level of indirection.)
如果libfoo.so是以PIC模式編譯的,並調用了一個外部函數bar,則bar會出現在.rel.plt section中(readelf -r);而如果不是以PIC模式編譯,則bar將出現在.rel.dyn section中。如果是後者的話,dynamic linker會在load libfoo.so的時候利用該section提供的位移量資訊,直接將其中引用bar的地方patch上,這樣一來也就意味著指令本身被修改了(dynamic linker之後是不是應該重新將指令改為唯讀?),因此也就喪失了可被多個進程共用的特性。而如果是PIC模式,則會有一次間接的過程,我們現在分析的就是這一過程。與函數調用不同,我們發現全域變數不論是以哪種方式編譯,它們的重定位資訊都被置於.rel.dyn中,我想這是因為資料的訪問不像控制轉移一樣可以藉助幾層跳轉來完成,因此也無法進行lazy binding而必須在load時做完。
PIC模式與非PIC模式最大的不同就是前者不直接patch指令,而是patch GOT。所有的指令都訪問GOT從而來達到position independence,與前文中動態解析庫函數的idea非常類似。可是既然是地址無關的,怎麼知道GOT的位置呢?關鍵在於每一個shared lib都帶有自己的GOT,而且整個lib是作為一個整體被load到記憶體,因此GOT的基址與每條指令的相對位移總是確定的。這樣一來,一條指令在訪問GOT的時候,只要算出自己當前的IP地址,再加上這個被靜態確定下來的位移量,就可以定位到自己要訪問的symbol的GOT entry了。
計算當前IP地址的函數一般是__i686.get_pc_thunk.bx,它會附帶在每個PIC module中,因此它與調用它的函數的相對位移也是固定下來的。它非常簡單:

mov (%esp),%ebx
ret

這樣就把它的返回地址,也就是caller function的IP地址給放到了ebx中。有時還能見到__i686.get_pc_thunk.cx,會寫入ecx,這是因為ebx是callee saved reg,而ecx是caller saved,因此如果一個函數要調用別的函數則最好使用ebx,否則最好使用其它寄存器。接下來的指令(0x00113458)使用自己的IP加上一個固定的位移便得到了本lib的GOT地址。這一值通常會一直緩衝在寄存器(ebx)中。

(gdb) disassemble
Dump of assembler code for function foo:
0x0011344c : push %ebp
0x0011344d : mov %esp,%ebp
0x0011344f : push %ebx
0x00113450 : sub $0x4,%esp
0x00113453 : call 0x113447 <__i686.get_pc_thunk.bx>
0x00113458 : add $0x1180,%ebx
0x0011345e : mov 0xfffffff0(%ebx),%eax ; a negative number because we are accessing .got from .got.plt
0x00113464 : movb $0x32,(%eax)
0x00113467 : call 0x113320
0x0011346c : lea 0xffffef08(%ebx),%eax
...

0x00113458這條指令的$0x1180是如何得來的呢?
我們運行readelf -r foo.o得到:

Relocation section '.rel.text' at offset 0x484 contains 7 entries:
Offset Info Type Sym.Value Sym. Name
00000008 00000b02 R_386_PC32 00000000 __i686.get_pc_thunk.bx
0000000e 00000c0a R_386_GOTPC 00000000 _GLOBAL_OFFSET_TABLE_
...

說明linker應該在.text section的0xe位移出patch上_GLOBAL_OFFSET_TABLE_的真真實位址。而0xe對應的恰恰就是foo.o的.text section的那條add指令的運算元:

$ objdump -d foo.o
Disassembly of section .text:
00000000 :
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 53 push %ebx
4: 83 ec 04 sub $0x4,%esp
7: e8 fc ff ff ff call 8 ; 0x8: __i686.get_pc_thunk.bx
c: 81 c3 02 00 00 00 add $0x2,%ebx ; 0xe: _GLOBAL_OFFSET_TABLE_
...

我們再看一下_GLOBAL_OFFSET_TABLE_這個symbol的值在libfoo.so中是多少

$ nm libfoo.so |grep _GLOBAL_OFFSET_TABLE_
000015d8 a _GLOBAL_OFFSET_TABLE_
$ readelf -S libfoo.so|grep 15d8
[20] .got.plt PROGBITS 000015d8 0005d8 00001c 04 WA 0 0 4

正是.got.plt的地址!因此可以看出來,所有訪問got的指令都留了個空,告訴linker在決定了got的地址時(其實也就是位移量而非絕對位址),把got相對於該指令的位移量填進來。
注意計算了半天得到got的地址只是為了能訪問全域變數,因為需要絕對位址(0x0011345e)。而調用plt中的函數只需要一個相對位址就夠了,因此不需要通過ebx來間接訪問(0x00113467)。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.