Linux動態串連原理

來源:互聯網
上載者:User

Linux動態串連原理

注意:

以下所用的連接器是指,ld,

而載入器是指ld-linux.so;

1,  GOT表;

GOT(Global Offset Table)表中每一項都是本運行模組要引用的一個全域變數或函數的地址。可以用GOT表來間接引用全域變數、函數,也可以把GOT表的首地址作為一個基 准,用相對於該基準的位移量來引用靜態變數、靜態函數。由於載入器不會把運行模組載入到固定地址,在不同進程的地址空間中,各運行模組的絕對位址、相對位 置都不同。這種不同反映到GOT表上,就是每個進程的每個運行模組都有獨立的GOT表,所以進程間不能共用GOT表。
在x86體繫結構 上,本運行模組的GOT表首地址始終儲存在%ebx寄存器中。編譯器在每個函數入口處都產生一小段代碼,用來初始化%ebx寄存器。這一步是必要的,否 則,如果對該函數的調用來自另一運行模組,%ebx中就是調用者模組的GOT表地址;不重新初始化%ebx就用來引用全域變數和函數,當然出錯。

這兩段話的意思是說,GOT是一個映射表,這裡的內容是此段代碼裡面引用到的外部符號的地址映射,比如你用用到了一個printf函數,在這裡就會有一項假設是1000,則就像這樣的:

.Got

符號                             地址

Printf                      1000

………

這樣的話程式在運行到printf的時候就尋找到這個地址1000從而走到其實際的代碼中的地方去。

但是這裡存在一個問題,因為printf是在共用庫裡面的,而共用庫在載入的時候是沒有固定地址的,所以你不知道它的地址是1000還是2000?怎麼辦呢?

於是引入了下面的表plt,這個表的內容是什麼呢?請看下面:

2,  PLT表;

PLT(Procedure Linkage Table)表每一項都是一小段代碼,對應於本運行模組要引用的一個全域函數。以對函數fun的調用為例,PLT中代碼片斷如下:

.PLTfun:  jmp *fun@GOT(%ebx)
pushl $offset
jmp .PLT0@PC

其中引用的GOT表項被載入器初始化為下一條指令(pushl)的地址,那麼該jmp指令相當於nop空指令。

使用者程式中對fun的直接調用經編譯串連後產生一條call [email]fun@PLT 指令,這是一條相對跳轉指令(滿足浮動代碼的要求!),跳到.PLTfun 。如果這是本運行模組中第一次調用該函數,此處的jmp等於一個空指令,繼續往下執行,接著就跳到PLT[email]0。該PLT項保留給編譯器產生的 額外代碼,會把程式流程引入到載入器中去。載入器計算fun的實際入口地址,填入fun@GOT表項。圖示如下:

user program
--------------
call fun@PLT
|
v
DLL             PLT table                loader
--------------   --------------   -----------------------
fun:           <-- jmp*fun@GOT  --> change GOT entry from
|             $loader to $fun,
v             then jump to there
GOT table
--------------
fun@GOTloader

第 一次調用以後,GOT表項已指向函數的正確入口。以後再有對該函數的調用,跳到PLT表後,不再進入載入器,直接跳進函數正確入口了。從效能上分析,只有第一次調用才要載入器作一些額外處理,這是完全可以容忍的。還可以看出,載入時不用對相對跳轉的代碼進行修補,所以整個程式碼片段都能在進程間共用。

上面的話是什麼意思呢?

拿我們上面舉的例子,printf在got表裡面對應的地址是1000,而這個1000到底以為著什麼呢?

PLTfun:  jmp *fun@GOT(%ebx)
1000: pushl $offset
jmp

你可以看到所謂1000就是它下面的這個地址,也就是說在外部函數還沒有實現串連的時候,got表裡面的內容其實是指向下一條指令的,於是開始執行了plt表裡面的內容,於是這個段裡面的內容肯定包括計算當前這個函數的實際地址的內容,於是求得實際地址添入got表,假設地址為0x800989898

於是got表裡面的內容就應該這樣的:

Printf                        0x800989898

………………..

這樣當下一次調用這個printf的時候就不需要再去plt表裡面走一遭了。
這裡需要提一下的是,尋找printf的地址實際上就是遞迴尋找當前執行的程式所依賴的庫,在她們export的符號表裡面尋找,如果找到就返回,否則,報錯,就是我們經常看到的undefined referenc to XXXXX.

3,  程式碼片段重定位前提。

程式碼片段本身是存在於唯讀地區的,所以理論上它是不可能在啟動並執行時候重新修改的,但是這就涉及一個問題,如何保證Got表的正確使用,因為每一個進程都有自己的got表,而共用庫完全同時被許多個進程使用的,於是在每個函數的入口都有這樣的語句:

call L1
L1:  popl %ebx
addl $GOT+[.-.L1], %ebx
.o:  R_386_GOTPC
.so: NULL

上述過程是編譯、串連相合作的結果。編譯器產生目標檔案時,因為此時還不存在GOT表(每個運行模組有一個GOT表,一個PLT表,由連接器產生),所以暫時不能計算GOT表與當前IP間的差值,僅在第三句處設上一個R_386_GOTPC重錨點而已。然後進行串連。連接器注意到GOTPC重定位項,於是計算GOT與此處IP的差值,作為addl指令的立即定址方式運算元。以後再也不需要重定位了。

這樣做的好處是目的是什麼呢?

就是在函數內部引用外部符號的時候能夠正確的轉到適當的地方去。

4,  變數、函數引用

當引用的是靜態變數、靜態函數或字串常量時,使用R_386_GOTOFF重定位方式。它與GOTPC重定位方式很相似,同樣首先由編譯器在目標檔案中設上重錨點,然後連接器計算GOT表與被引用元素首地址的差值,作為leal指令的變址定址方式運算元。代碼片斷如下:

leal .LC1@GOTOFF(%ebx), %eax
.o:  R_386_GOTOFF
.so: NULL

當引用的是全域變數、全域函數時,編譯器會在目標檔案中設上一個R_386_GOT32重錨點。連接器會在GOT表中保留一項,註上 R_386_GLOB_DAT重錨點,用於載入器填寫被引用元素的實際地址。連接器還要計算該保留項在GOT表中的位移,作為movl指令的變址定址 方式運算元。代碼片斷如下:

movl x@GOT(%ebx), %eax
.o:  R_386_GOT32
.so: R_386_GLOB_DAT

需要指出,引用全域函數時,由GOT表讀出不是全域函數的實際入口地址,而是該函數在PLT表中的入口.PLTfun。這樣,無論直接調用,還是先取得函數地址再間接調用,程式流程都會轉入PLT表,進而把控制權轉移給載入器。載入器就是利用這個機會進行動態串連的。

 

  注意:這裡討論的是變數函數的引用,不是函數的直接調用,而是函數,變數的地址的取得,如果是函數的話,取得的實際上是plt裡面的地址,於是最終還是沒能逃過載入器的協助。

5,  直接調用函數
如前所述,浮動代碼中的函數調用語句會編譯成相對跳轉指令。首先編譯器會在目標檔案中設上一個R_386_PLT32重錨點,然後視靜態函數、全域函數不同而串連過程也有所不同。

如果是靜態函數,調用一定來自同一運行模組,調用點相對於函數進入點的位移量在串連時就可計算出來,作為call指令的相對當前IP位移跳轉運算元,由此直接進入函數入口,不用載入器操心。相關代碼片斷如下:

call f@PLT
.o:  R_386_PLT32
.so: NULL

如果是全域函數,連接器將產生到.PLTfun的相對跳轉指令,之後就如前面所述,對全域函數的第一次調用會把程式流程轉到載入器中去,然後計算函數的入口地址,填充fun@GOT表項。這稱為R_386_JMP_SLOT重定位方式。相關代碼片斷如下:

call f@PLT
.o:  R_386_PLT32
.so: R_386_JMP_SLOT

如此一來,一個全域函數可能有多至兩個重定位項。一個是必需JMP_SLOT重定位項,載入器把它指向真正的函數入口;另一個是GLOB_DAT重定位 項,載入器把它指向PLT表中的代碼片斷。取函數地址時,取得的總是GLOB_DAT重定位項的值,也就是指向.PLTfun,而不是真正的函數入口。

進一步考慮這樣一個問題:兩個動態串連庫,取同一個全域函數的地址,兩個結果進行比較。由前面的討論可知,兩個結果都沒有指向函數的真正入口,而是分別指向兩個不同的PLT表。簡單進行比較,會得出"不相等"的結論,顯然不正確,所以要特殊處理。

注意:

一個是必需JMP_SLOT重定位項,這裡指的就是直接調用函數的情況;

另一個是GLOB_DAT重定位 項,這裡指函數地址引用的情況;

6,  資料區段的重定位

在資料區段中的重定位是指對指標類型的靜態變數、全域變數進行初始化。它與程式碼片段中的重定位比較起來至少有以下明顯不 同:一、在使用者程式獲得控制權(main函數開始執行)之前就要全部完成;二、不經過GOT表間接定址,這是因為此時%ebx中還沒有正確的GOT表首地 址;三、直接修改資料區段,而程式碼片段重定位時不能修改程式碼片段。

如果引用的是靜態變數、函數、串常量,編譯器會在目標檔案中設上 R_386_32重錨點,並計算被引用變數、函數相對於所在段首地址的位移量。連接器把它改成R_386_RELATIVE重錨點,計算它相對於動態串連庫首地址(通常為零)的位移量。載入器會把運行模組真正的首地址(不為零)與該位移量相加,結果用來初始化指標變數。代碼片斷如下:

.section .rodata
.LC0: .string "Ok\n"
.data
p:     .long .LC0
.o:  R_386_32 w/ section
.so: R_386_RELATIVE

如果引用的是全域變數、函數,編譯器同樣設上R_386_32重錨點,並且記錄引用的符號名字。連接器不必動作。最後載入器尋找被引用符號,結果用來初始化指標變數。對於全域函數,尋找的結果仍然是函數在PLT表中的代碼片斷,而不是實際入口。這與前面引用全域函數的討論相同。代碼片斷如下:

.data
p:       .long printf
.o:  R_386_32 w/ symbol
.so: R_386_32 w/ symbol

7,  總結:

下表給出了前面討論得到的全部結果:
.o                          .so
--------------------------------------------------------------------------
|裝載GOT表首地址        R_386_GOTPC     NULL
程式碼片段|-----------------------------------------------------
重定位|引用變數函數地址 靜態  R_386_GOTOFF    NULL
|                 全域  R_386_GOT32     R_386_GLOB_DAT
|-----------------------------------------------------
|直接調用函數     靜態  R_386_PLT32     NULL
|                    全域  R_386_PLT32     R_386_JMP_SLOT
------|-----------------------------------------------------
資料區段|引用變數函數地址 靜態  R_386_32 w/sec  R_386_RELATIVE
重定位|                 全域  R_386_32 w/sym  R_386_32 w/sym

 

轉自:http://hi.baidu.com/osidy/item/c3f6908f0a9dacd45f0ec17b

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.