1. 程式人生 > >使用反彙編理解動態庫函式呼叫方式GOT/PLT

使用反彙編理解動態庫函式呼叫方式GOT/PLT

文章轉載自:

http://blog.csdn.net/anzhsoft/article/details/18776111

 本文主要講解動態庫函式的地址是如何在執行時被定位的。首先介紹一下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. 反彙編

我們使用的程式碼是:

  1. #include <iostream>
  2. #include <stdlib.h>
  3. void fun(int a)  
  4. {  
  5.   a++;  
  6. }  
  7. int main()  
  8. {  
  9.   fun(1);  
  10.   int x = rand();  
  11.   return 0;  
  12. }  

動態庫裡面需要重定位的函式在.got.plt這個段裡面,通過readelf我們可以看到,它一共有六個地址空間,前三個我們已經解釋了。說明該程式預留了三個所需要重新定位的函式。因此用不到的函式是永遠不會被載入的。

  1. [23] .dynamic          DYNAMIC          0000000000600e10  00000e10  
  2.      00000000000001d0  0000000000000010  WA       8     0     8  
  3. [24] .got              PROGBITS         0000000000600fe0  00000fe0  
  4.      0000000000000008  0000000000000008  WA       0     0     8  
  5. [25] .got.plt          PROGBITS         0000000000600fe8  00000fe8  
  6.      0000000000000048  0000000000000008  WA       0     0     8  

反彙編main函式:
  1. (gdb) disas main  
  2. Dump of assembler code for function main:  
  3. 0x0000000000400549 <main+0>:    push   %rbp  
  4. 0x000000000040054a <main+1>:    mov    %rsp,%rbp  
  5. 0x000000000040054d <main+4>:    sub    $0x10,%rsp  
  6. 0x0000000000400551 <main+8>:    mov    $0x1,%edi  
  7. 0x0000000000400556 <main+13>:   callq  0x40053c <fun>  
  8. 0x000000000040055b <main+18>:   callq  0x400440 <[email protected]>  
  9. 0x0000000000400560 <main+23>:   mov    %eax,-0x4(%rbp)  
  10. 0x0000000000400563 <main+26>:   mov    $0x0,%eax  
  11. 0x0000000000400568 <main+31>:   leaveq  
  12. 0x0000000000400569 <main+32>:   retq  
  13. End of assembler dump.  
可以看到其實呼叫我們自定義的fun和系統庫函式rand形成的彙編差不多,沒有額外的處理。接著向下看rand:
  1. (gdb) disas 0x400440  
  2. Dump of assembler code for function [email protected]:  
  3. 0x0000000000400440 <[email protected]+0>:        jmpq   *0x200bc2(%rip)        # 0x601008 <_GLOBAL_OFFSET_TABLE_+32>  
  4. 0x0000000000400446 <[email protected]+6>:        pushq  $0x1  
  5. 0x000000000040044b <[email protected]+11>:       jmpq   0x400420  
  6. End of assembler dump.  
真正有意思的在# 0x601008 <_GLOBAL_OFFSET_TABLE_+32>。也就是[email protected]首先會跳到這裡。我們看一下這裡是什麼:
  1. (gdb) x 0x601008  
  2. 0x601008 <_GLOBAL_OFFSET_TABLE_+32>:    0x00400446  
接著看0x00400446是什麼:
  1. (gdb) x/5i 0x00400446  
  2. 0x400446 <[email protected]+6>:  pushq  $0x1  
  3. 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的實際地址:

  1. (gdb) x 0x601008  
  2. 0x601008 <_GLOBAL_OFFSET_TABLE_+32>:    0xf7ab6470  

可以看到,rand的地址已經修改為0xf7ab6470了。然後可以通過maps確認一下是否libc load在這個地址:

  1. (gdb) shell cat /proc/`pgrep a.out`/maps  
  2. 00400000-00401000 r-xp 00000000 08:02 491638                             /root/study/got/a.out  
  3. 00600000-00601000 r--p 00000000 08:02 491638                             /root/study/got/a.out  
  4. 00601000-00602000 rw-p 00001000 08:02 491638                             /root/study/got/a.out  
  5. 7ffff7a80000-7ffff7bd5000 r-xp 00000000 08:02 327685                     /lib64/libc-2.11.1.so  
  6. 7ffff7bd5000-7ffff7dd4000 ---p 00155000 08:02 327685                     /lib64/libc-2.11.1.so  
  7. 7ffff7dd4000-7ffff7dd8000 r--p 00154000 08:02 327685                     /lib64/libc-2.11.1.so  
  8. 7ffff7dd8000-7ffff7dd9000 rw-p 00158000 08:02 327685                     /lib64/libc-2.11.1.so  
  9. 7ffff7dd9000-7ffff7dde000 rw-p 00000000 00:00 0  
  10. 7ffff7dde000-7ffff7dfd000 r-xp 00000000 08:02 327698                     /lib64/ld-2.11.1.so  
  11. 7ffff7fc4000-7ffff7fc7000 rw-p 00000000 00:00 0  
  12. 7ffff7ffa000-7ffff7ffb000 rw-p 00000000 00:00 0  
  13. 7ffff7ffb000-7ffff7ffc000 r-xp 00000000 00:00 0                          [vdso]  
  14. 7ffff7ffc000-7ffff7ffd000 r--p 0001e000 08:02 327698                     /lib64/ld-2.11.1.so  
  15. 7ffff7ffd000-7ffff7ffe000 rw-p 0001f000 08:02 327698                     /lib64/ld-2.11.1.so  
  16. 7ffff7ffe000-7ffff7fff000 rw-p 00000000 00:00 0  
  17. 7ffffffea000-7ffffffff000 rw-p 00000000 00:00 0                          [stack]  
  18. ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]  

沒有問題,如我們所分析的那樣:
  1. 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/


相關推薦

使用彙編理解動態函式呼叫方式GOT/PLT

文章轉載自: http://blog.csdn.net/anzhsoft/article/details/18776111  本文主要講解動態庫函式的地址是如何在執行時被定位的。首先介紹一下PIC和Relocatable的動態庫的區別。然後講解一下GOT和PLT的理論知

C語言中呼叫靜態函式動態函式方式

C語言中呼叫動態庫函式的兩種方式 方式一.隱式呼叫 將動態庫的相關檔案拷貝到當前目錄下(lib、dll),然後新增以下程式碼,在程式中指定連線庫函式。 注意:第二個引數給出的是引入庫檔案(或稱“匯出庫檔案”),而不是dll。在程式執行過程中,lib將dll中需要用到的函式對映到對應的記憶

C++中使用_asm彙編呼叫動態函式的一點問題

    因為從事dll 編寫的相關工作。沒寫完一個dll 之後都要對函式進行測試,對每個dll都要寫一個測試demo的話就非常費勁。能不能一個公共的測試軟體來各種dll裡的函式測試呢?     嘗試開始,從外界的.h檔案中讀取函式名很簡單,但是我們不能在程式已經編譯的過程中

使用JNA呼叫c/c++的so動態函式

       最近專案收到個需求,需要呼叫c寫的函式,給的是so檔案,查閱了資料,so檔案為linux下的動態庫函式檔案,windos下為dll檔案。傳統方案用JNI方式進行連線,大致看了下JNI方式實在麻煩,崩潰中找到JNA,併成功實現了呼叫,特此記錄使用過程。 一、將s

C語言 呼叫動態函式重名問題分析

設計兩個動態庫 第一個動態庫:libHelloc: func1.h #ifndef FUNC1_H_ #define FUNC1_H_ int func1(); void func(); #endif func1.c #include "func1.h" int

Java呼叫C/C++生成的動態函式

問題背景 之前的文章中,筆者將超長整數的四則運算利用C語言實現,因個人需要在web專案中使用該功能, 此時能想到的辦法是重寫實現過程,即利用Java重寫一遍C的實現過程 不談工作量的多少,單單是這個重寫的過程就讓我望而生畏,程式設計師最頭疼的一個是bug找不到,還有一個就是

C++函式呼叫方式(_stdcall, _pascal, _cdecl...)總結

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!        

DEP引起的DLL函式呼叫失敗

1          什麼是DEP(資料執行保護) 根據微軟官方定義:資料執行保護 (DEP) 是一種有助於防止您的計算機免受病毒和其他安全威脅破壞的安全功能。有害的程式可能會通過試圖執行(也稱為“執行”)

VS2010 中編寫動態呼叫動態

https://www.cnblogs.com/zhengfa-af/p/8108187.html https://blog.csdn.net/qq_22642239/article/details/80451299 VS2010 中編寫動態庫和呼叫動態庫 百度查了一下在VS中編寫動態庫

利用遞迴函式呼叫方式,將所輸入的5個字元,以相反順序打印出來

#include<stdio.h> int main() { void dg(char a[],int x); char a[5]; gets(a); dg(a,5); printf("\n"); return 0; } void dg(char a[5],in

個人js學習例項-點選按鈕實現全選與選,及封裝函式呼叫前後

原始: <!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="wid

C++函式呼叫方式 stdcall pascal cdecl 總結

__stdcall:       _stdcall 呼叫約定相當於16位動態庫中經常使用的PASCAL呼叫約定。在32位的VC++5.0中PASCAL呼叫約定不再被支援(實際上它已被定義為__stdcall。除了__pascal外,__fo

簡單動態以及呼叫動態例子

動態庫的寫法,以前沒有寫過動態庫,憑第六感覺應該很簡單。but,我卻在網上找資料時,沒有一個例子是我想要的,簡單又能說明問題。以至於耗費了大量的時間。以下例子是用C語言編寫,編譯環境為vs2015。//-------------------------------------

matlab 高斯模糊非函式實現方式

簡單講一下原理和思路:   高斯模糊就是讓一個高斯矩陣和所要模糊的矩陣相點乘(即兩個矩陣對應位置的兩個數相乘),然後把所得矩陣的各項之和相加,即為模糊中心點的值。   所謂高斯矩陣就是由高斯函式(即

linux動態so呼叫外部so,執行時出現undefined symbol

1、首先排查,C++呼叫了c的庫?是不是需要加上extern "c",尤其是類的動態庫,需要用到工廠模式,create一個物件出來,該工廠函式需要extern "c"宣告。 extern "C" CDbBase* create(); extern "C" void dest

C++批量載入動態函式方法

1、列舉定義enum  {    // 0 - GigE DLL (implicitly called)    Func_isVersionCompliantDLL,    Func_isDriverAv

理解JavaScript的函式呼叫和this

多年以來,我看到了許多人對於JavaScript函式呼叫有很多困惑。特別是許多人會抱怨,”this”在函式呼叫中的語義是令人疑惑的。 在我看來,通過理解核心的函式呼叫的原始模型,並且去看一下在此基礎之上的其他方式的函式呼叫(對原始呼叫的思想的抽取)可以消除這些困惑。

linux環境下的c++ 動態呼叫

主要是為了平時的學習記錄,不妥的地方,煩請指點。一.下面主要是dlopen開啟動態庫.so相關的API介面函式。1. void* dlopen(const char* filename,int flag);filename 是動態庫的path路徑,flag是動態庫載入的幾種方

彙編理解堆疊及printf

#include <stdio.h> int main() {     long long a = 1, b = 2, c = 3;     printf("%d %d %d\n", a,b,c);     return 0; }//Tencent某年實習生筆試

linux系統呼叫函式呼叫的區別

Linux下對檔案操作有兩種方式:系統呼叫(system call)和庫函式呼叫(Library functions)。可以參考《Linux程式設計》(英文原版為《Beginning Linux Programming》,作者是Neil Matthew和Richard St