本文主要講解動态庫函數的位址是如何在運作時被定位的。首先介紹一下PIC和Relocatable的動态庫的差別。然後講解一下GOT和PLT的理論知識。GOT是Global Offset Table,是儲存庫函數位址的區域。程式運作時,庫函數的位址會設定到GOT中。由于動态庫的函數是在使用時才被加載,是以剛開始GOT表是空的。位址的設定就涉及到了PLT,Procedure Linkage Table,它包含了一些代碼以調用庫函數,它可以被了解成一系列的小函數,這些小函數的數量其實就是庫函數的被使用到的函數的數量。簡單來說,PLT就是跳轉到GOT中所設定的位址而已。如果這個位址是空,那麼PLT的跳轉會巧妙的調用_dl_runtime_resolve去擷取最終位址并設定到GOT中去。由于庫函數的位址在運作時不會變,是以GOT一旦設定以後PLT就可以直接跳轉到庫函數的真實位址了。最後使用反彙編驗證和跳轉流程圖對上述結論加深了解。
1. 背景-PIC VS Relocatable
在 Linux 下制作動态連結庫,“标準” 的做法是編譯成位置無關代碼(Position Independent Code,PIC),然後連結成一個動态連結庫。那麼什麼是PIC呢?如果是非PIC的,那麼會有什麼問題?
(1) 可重定位代碼(relocatable code):Windows DLL 以及不使用 -fPIC 的 Linux so。
生成動态庫時假定它被加載在位址 0 處。加載時它會被加載到一個位址(base),這時要進行一次重定位(relocation),把代碼、資料段中所有的位址加上這個 base 的值。這樣代碼運作時就能使用正确的位址了。當要再加載時根據加載到的位置再次重定位的。(因為它裡面的代碼并不是位置無關代碼)。因為so被每個程式加載的位置都不同,顯然這些重定位後的代碼也不同,當然不能共享。如果被多個應用程式共同使用,那麼它們必須每個程式維護一份so的代碼副本了。當然,主流現代作業系統都啟用了分頁記憶體機制,這使得重定位時可以使用 COW(copy on write)來節省記憶體(32 位 Windows 就是這樣做的);然而,頁面的粒度還是比較大的(例如 IA32 上是 4KiB),至少對于代碼段來說能節省的相當有限。不能共享就失去了共享庫的好處,實際上和靜态庫的差別并不大,在運作時占用的記憶體是類似的,僅僅是二進制代碼占的硬碟空間小一些。
(2) 位置無關代碼(position independent code):使用 -fPIC 的 Linux so。
這樣的代碼本身就能被放到線性位址空間的任意位置,無需修改就能正确執行。通常的方法是擷取指令指針(如 x86 的 EIP 寄存器)的值,加上一個偏移得到全局變量/函數的位址。AMD64 下,必須使用位置無關代碼。x86下,在建立so時會有一個警告。但是這樣的so可以完全正常工作。PIC 的缺點主要就是代碼有可能長一些。例如 x86,由于不能直接使用 [EIP+constant] 這樣的尋址方式,甚至不能直接将 EIP 的值交給其他寄存器,要用到 GOT(global offset table)來定位全局變量和函數。這樣導緻代碼的效率略低。PIC 的加載速度稍快,因為不需要做重定位。多個程序引用同一個 PIC 動态庫時,可以共用記憶體。這一個庫在不同程序中的虛拟位址不同,但作業系統顯然會把它們映射到同一塊實體記憶體上。
是以,除非你的so不會被共享,否則還是加上-fPIC吧。
2. GOT和PLT
我們都知道動态庫是在運作時綁定的。那麼編譯器是如何找到動态連結庫裡面的函數的位址呢?事實上,直到我們第一次調用這個函數,我們并不知道這個函數的位址,這個功能要做延遲綁定 lazy bind。 因為程式的分支很多,并不是所有的分支都能跑到,想想我們的異常處理,異常處理分支的動态連結庫裡面的函數也許永遠跑不到,是以,啟動時解析所有出現過的動态庫裡面的函數是個浪費的辦法,降低性能并且沒有必要。
Global Offset Table(GOT)
在位置無關代碼中,一般不能包含絕對虛拟位址(如共享庫)。當在程式中引用某個共享庫中的符号時,編譯連結階段并不知道這個符号的具體位置,隻有等到動态連結器将所需要的共享庫加載時進記憶體後,也就是在運作階段,符号的位址才會最終确定。是以,需要有一個資料結構來儲存符号的絕對位址,這就是GOT表的作用,GOT表中每項儲存程式中引用其它符号的絕對位址。這樣,程式就可以通過引用GOT表來獲得某個符号的位址。
在x86結構中,GOT表的前三項保留,用于儲存特殊的資料結構位址,其它的各項儲存符号的絕對位址。對于符号的動态解析過程,我們隻需要了解的就是第二項和第三項,即GOT[1]和GOT[2]:GOT[1]儲存的是一個位址,指向已經加載的共享庫的連結清單位址;GOT[2]儲存的是一個函數的位址,定義如下:GOT[2] = &_dl_runtime_resolve,這個函數的主要作用就是找到某個符号的位址,并把它寫到與此符号相關的GOT項中,然後将控制轉移到目标函數,後面我們會詳細分析。GOT示意如下圖,GOT表slot的數量就是3 + number of functions to be loaded.
Procedure Linkage Table(PLT)
過程連結表(PLT)的作用就是将位置無關的函數調用轉移到絕對位址。在編譯連結時,連結器并不能控制執行從一個可執行檔案或者共享檔案中轉移到另一個中(如前所說,這時候函數的位址還不能确定),是以,連結器将控制轉移到PLT中的某一項。而PLT通過引用GOT表中的函數的絕對位址,來把控制轉移到實際的函數。
在實際的可執行程式或者共享目标檔案中,GOT表在名稱為.got.plt的section中,PLT表在名稱為.plt的section中。
3. 反彙編
我們使用的代碼是:
#include <iostream>
#include <stdlib.h>
void fun(int a)
{
a++;
}
int main()
{
fun(1);
int x = rand();
return 0;
}
動态庫裡面需要重定位的函數在.got.plt這個段裡面,通過readelf我們可以看到,它一共有六個位址空間,前三個我們已經解釋了。說明該程式預留了三個所需要重新定位的函數。是以用不到的函數是永遠不會被加載的。
[23] .dynamic DYNAMIC 0000000000600e10 00000e10
00000000000001d0 0000000000000010 WA 8 0 8
[24] .got PROGBITS 0000000000600fe0 00000fe0
0000000000000008 0000000000000008 WA 0 0 8
[25] .got.plt PROGBITS 0000000000600fe8 00000fe8
0000000000000048 0000000000000008 WA 0 0 8
反彙編main函數:
(gdb) disas main
Dump of assembler code for function main:
0x0000000000400549 <main+0>: push %rbp
0x000000000040054a <main+1>: mov %rsp,%rbp
0x000000000040054d <main+4>: sub $0x10,%rsp
0x0000000000400551 <main+8>: mov $0x1,%edi
0x0000000000400556 <main+13>: callq 0x40053c <fun>
0x000000000040055b <main+18>: callq 0x400440 <[email protected]>
0x0000000000400560 <main+23>: mov %eax,-0x4(%rbp)
0x0000000000400563 <main+26>: mov $0x0,%eax
0x0000000000400568 <main+31>: leaveq
0x0000000000400569 <main+32>: retq
End of assembler dump.
可以看到其實調用我們自定義的fun和系統庫函數rand形成的彙編差不多,沒有額外的處理。接着向下看rand:
(gdb) disas 0x400440
Dump of assembler code for function [email protected]:
0x0000000000400440 <[email protected]+0>: jmpq *0x200bc2(%rip) # 0x601008 <_GLOBAL_OFFSET_TABLE_+32>
0x0000000000400446 <[email protected]+6>: pushq $0x1
0x000000000040044b <[email protected]+11>: jmpq 0x400420
End of assembler dump.
真正有意思的在# 0x601008 <_GLOBAL_OFFSET_TABLE_+32>。也就是[email protected]首先會跳到這裡。我們看一下這裡是什麼:
(gdb) x 0x601008
0x601008 <_GLOBAL_OFFSET_TABLE_+32>: 0x00400446
接着看0x00400446是什麼:
(gdb) x/5i 0x00400446
0x400446 <[email protected]+6>: pushq $0x1
0x40044b <[email protected]+11>: jmpq 0x400420
可能你注意到了,這裡的處理是和剛才的[email protected]的jmpq一樣。都是将0x1入棧,然後jmpq 0x400420。是以這樣就避免了GOT表是否為是真實值的檢查:如果是空,那麼去尋址;否則直接調用。
其實接下來處理的就是調用_dl_runtime_resolve_()函數,該函數最終會尋址到rand的真正位址并且會調用_dl_fixup來将rand的實際位址填入GOT表中。
我們将整個程式執行完,然後看一下0x601008 <_GLOBAL_OFFSET_TABLE_+32>是否已經修改成rand的實際位址:
(gdb) x 0x601008
0x601008 <_GLOBAL_OFFSET_TABLE_+32>: 0xf7ab6470
可以看到,rand的位址已經修改為0xf7ab6470了。然後可以通過maps确認一下是否libc load在這個位址:
(gdb) shell cat /proc/`pgrep a.out`/maps
00400000-00401000 r-xp 00000000 08:02 491638 /root/study/got/a.out
00600000-00601000 r--p 00000000 08:02 491638 /root/study/got/a.out
00601000-00602000 rw-p 00001000 08:02 491638 /root/study/got/a.out
7ffff7a80000-7ffff7bd5000 r-xp 00000000 08:02 327685 /lib64/libc-2.11.1.so
7ffff7bd5000-7ffff7dd4000 ---p 00155000 08:02 327685 /lib64/libc-2.11.1.so
7ffff7dd4000-7ffff7dd8000 r--p 00154000 08:02 327685 /lib64/libc-2.11.1.so
7ffff7dd8000-7ffff7dd9000 rw-p 00158000 08:02 327685 /lib64/libc-2.11.1.so
7ffff7dd9000-7ffff7dde000 rw-p 00000000 00:00 0
7ffff7dde000-7ffff7dfd000 r-xp 00000000 08:02 327698 /lib64/ld-2.11.1.so
7ffff7fc4000-7ffff7fc7000 rw-p 00000000 00:00 0
7ffff7ffa000-7ffff7ffb000 rw-p 00000000 00:00 0
7ffff7ffb000-7ffff7ffc000 r-xp 00000000 00:00 0 [vdso]
7ffff7ffc000-7ffff7ffd000 r--p 0001e000 08:02 327698 /lib64/ld-2.11.1.so
7ffff7ffd000-7ffff7ffe000 rw-p 0001f000 08:02 327698 /lib64/ld-2.11.1.so
7ffff7ffe000-7ffff7fff000 rw-p 00000000 00:00 0
7ffffffea000-7ffffffff000 rw-p 00000000 00:00 0 [stack]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
沒有問題,如我們所分析的那樣:
7ffff7a80000-7ffff7bd5000 r-xp 00000000 08:02 327685 /lib64/libc-2.11.1.so
以後的調用就直接調用庫函數了:
尊重原創,轉載請注明出處 anzhsoft: http://blog.csdn.net/anzhsoft/article/details/18776111
參考資料:
1. http://www.linuxidc.com/Linux/2011-06/37268.htm
2. http://blog.chinaunix.net/uid-24774106-id-3349549.html
3. http://www.linuxidc.com/Linux/2011-06/37268.htm
4. http://eli.thegreenplace.net/2011/11/03/position-independent-code-pic-in-shared-libraries/