Linux動態連接原理

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章