Linux下的動態串連庫及其實現機制

來源:互聯網
上載者:User

摘 要:本文介紹了動態串連庫的優點,詳細闡述了x86體繫結構上Linux系統的編譯器
、連接器、載入器如何使用多種重定位方式來實現該功能

關鍵詞:動態串連庫;Linux;重定位

The Implementation Mechanism of DLL under Linux

【Abstract】In this paper, we discuss the advantage of using dynamic linking
. We also demonstrate in detail how compiler, linker and loader implement th
is feature by using several kinds of relocations under nowadays Linux system
, especially on x86 architectures.

【Keywords】dynamic link library; DLL; Linux; relocation

Linux與Windows的動態串連庫概念相似,但是實現機制不同。它引入了GOT表和PLT表的
概念,綜合使用了多種重定位項,實現了"浮動代碼",達到了更好的共用效能。本文對
這些技術逐一進行了詳細討論。

本文著重討論x86體繫結構,這是因為(1)運行Linux的各種體繫結構中,以x86最為普及
;(2)該體繫結構上的Windows作業系統廣為人知,由此可以較容易的理解Linux的類似概
念;

下表列出了Windows與Linux的近義詞,文中將不加以區分:

Windows Linux
動態串連庫(DLL) Shared Object
目標檔案(.obj) 檔案名稱結尾常是 .o
可執行檔(.exe) Executable(檔案名稱無特定標誌)
連接器(link.exe) Linker Editor (ld)
載入器(exec/loader) Dynamic Linker (ld-linux.so)
段(segment) 節(section)

一些關鍵字在本文中有特定含義,需要澄清:

編譯單元:一個C語言源檔案,經過編譯後將產生一個目標檔案
運行模組:一個動態串連庫或者一個可執行檔。簡稱為模組
自動變數、函數:C語言auto關鍵字修飾的對象
靜態變數、函數:C語言static關鍵字修飾的對象
全域變數、函數:C語言extern關鍵字修飾的對象

1 動態串連庫的優點

程式編製一般需經編輯、編譯、串連、載入和運行幾個步驟。由於一些公用代碼需要反
複使用,就把它們預先編譯成目標檔案並儲存在"庫"中。當它與使用者程式的目標檔案連
接時,連接器得從庫中選取使用者程式需要的代碼,然後複製到產生的可執行檔中。這
種庫稱為靜態庫,其特點是可執行檔中包含了庫代碼的一份完整拷貝。顯然,當靜態
庫被多個程式使用時,磁碟上、記憶體中都是多份冗餘拷貝。

而使用動態串連庫就克服了這個缺陷。當它與使用者程式的目標檔案串連時,連接器只是
作上標記,說明程式需要該動態串連庫,而不真的把庫代碼複製到可執行檔中;僅當
可執行檔運行時,載入器根據這個標記,檢查該庫是否已經被其它可執行檔載入進
記憶體。如果已存在於記憶體中,不用再從磁碟上載入,只要共用記憶體中已有的代碼即可。
這樣磁碟、記憶體中始終只有一份代碼,較靜態庫為優。

2 Linux動態串連庫的重要特點:浮動代碼

在Windows中,串連產生動態串連庫時要指定一個首地址。應用程式運行時,載入器將盡
可能把動態串連庫裝入到該地址;如果地址已被佔用,該動態串連庫只能被載入到其它
地址空間內,這時就要對庫中的代碼和資料進行修補,或叫做重定位。如此一來,庫的
多個執行個體在記憶體中經過重定位後,彼此將不盡相同,自然不再能共用了。為了避免這個
缺陷,Windows內建的庫都指定了互不重疊的地址,儘管如此,其它軟體廠商的產品仍然
不可避免的使用重疊地址,由此部分喪失了使用動態串連庫的好處。

在Linux中,為了達到更好的共用效能,使用了與Windows不一樣的策略:浮動代碼(Po
sition Independent Code,簡稱PIC)。具體說,使用的轉移指令都是相對於當前程式
計數器(IP)的位移量;代碼中引用變數、函數的地址都是相對於某個基地址的位移量
。總之,從不引用一個絕對位址。這樣,動態串連庫無論被載入到什麼地址空間,不用
修補代碼就可以正常工作。既然只有一份代碼,就容易實現共用了。

值得指出,此處所指的共用,是指為了節省儲存空間,多個進程使用動態串連庫程式碼片段、
唯讀資料區段在記憶體中的唯一映像;另一種常用的共用定義,是指多個進程對同一段(可
能是動態分配的)儲存區進行讀寫,實現處理序間通訊(IPC)。後一種共用定義與本文無
可執行檔運行時,載入器根據這個標記,檢查該庫是否已經被其它可執行檔載入進
記憶體。如果已存在於記憶體中,不用再從磁碟上載入,只要共用記憶體中已有的代碼即可。
這樣磁碟、記憶體中始終只有一份代碼,較靜態庫為優。

2 Linux動態串連庫的重要特點:浮動代碼

在Windows中,串連產生動態串連庫時要指定一個首地址。應用程式運行時,載入器將盡
可能把動態串連庫裝入到該地址;如果地址已被佔用,該動態串連庫只能被載入到其它
地址空間內,這時就要對庫中的代碼和資料進行修補,或叫做重定位。如此一來,庫的
多個執行個體在記憶體中經過重定位後,彼此將不盡相同,自然不再能共用了。為了避免這個
缺陷,Windows內建的庫都指定了互不重疊的地址,儘管如此,其它軟體廠商的產品仍然
不可避免的使用重疊地址,由此部分喪失了使用動態串連庫的好處。

在Linux中,為了達到更好的共用效能,使用了與Windows不一樣的策略:浮動代碼(Po
sition Independent Code,簡稱PIC)。具體說,使用的轉移指令都是相對於當前程式
計數器(IP)的位移量;代碼中引用變數、函數的地址都是相對於某個基地址的位移量
。總之,從不引用一個絕對位址。這樣,動態串連庫無論被載入到什麼地址空間,不用
修補代碼就可以正常工作。既然只有一份代碼,就容易實現共用了。

值得指出,此處所指的共用,是指為了節省儲存空間,多個進程使用動態串連庫程式碼片段、
唯讀資料區段在記憶體中的唯一映像;另一種常用的共用定義,是指多個進程對同一段(可
能是動態分配的)儲存區進行讀寫,實現處理序間通訊(IPC)。後一種共用定義與本文無
關。

3 Linux動態串連庫的實現機制:重定位

3.1 重定位概述

浮動代碼通過重定位操作得以實現。而重定位可以按多種標準進行分類:

-- 按發生的地點,可分成對程式碼片段(.text)重定位和對資料區段(.data)重定位。

-- 按發生的時間,可分成串連時重定位和載入時重定位(載入時重定位也稱為動態重定
位)。但這兩步並不總是必不可少的。例如,要實現浮動代碼就不能對程式碼片段進行動態
重定位,這時採取的辦法是,把需要動態重定位的項搬到資料區段中去,然後在程式碼片段中
引用這些項。

-- 按重定位項引用的對象,可分成資料引用和函數引用。如果引用的是待用資料或靜態
函數,連接器會最佳化產生的程式碼,去掉動態重定位項。

-- 從字面上講, x86體繫結構上的Linux使用了多種重定位方式,名字首碼以"R_386_"
,後面分別接:32、GOT32、PLT32、COPY、GLOB_DAT、JMP_SLOT、RELATIVE、GOTOFF、
GOTPC。每種方式都有特定的含義。
以上幾種分類中最重要的是按地點分類。而下文也將以它為主線,逐一介紹各種重定位
項。首先,引入兩個關鍵概念:GOT表和PLT表。

3.2 GOT表

GOT(Global Offset Table)表中每一項都是本運行模組要引用的一個全域變數或函數
的地址。可以用GOT表來間接引用全域變數、函數,也可以把GOT表的首地址作為一個基
准,用相對於該基準的位移量來引用靜態變數、靜態函數。
由於載入器不會把運行模組載入到固定地址,在不同進程的地址空間中,各運行模組的
絕對位址、相對位置都不同。這種不同反映到GOT表上,就是每個進程的每個運行模組都
有獨立的GOT表,所以進程間不能共用GOT表。

在x86體繫結構上,本運行模組的GOT表首地址始終儲存在%ebx寄存器中。編譯器在每個
函數入口處都產生一小段代碼,用來初始化%ebx寄存器。這一步是必要的,否則,如果
對該函數的調用來自另一運行模組,%ebx中就是調用者模組的GOT表地址;不重新初始化
%ebx就用來引用全域變數和函數,當然出錯。

3.3 PLT表

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

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

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

使用者程式中對fun的直接調用經編譯串連後產生一條call fun@PLT指令,這是一條相對跳
轉指令(滿足浮動代碼的要求!),跳到.PLTfun。如果這是本運行模組中第一次調用該
函數,此處的jmp等於一個空指令,繼續往下執行,接著就跳到.PLT0。該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@GOT:$loader

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

熟悉Windows的程式員很容易注意到,GOT表、PLT表與Windows中的引入表(Import)有
類似之處。其它對應關係還有: Linux的version script與Windows的.DEF檔案;Linux
的dynamic symbols section與Windows的輸出表(Export)。不再舉更多例子了。

3.4 程式碼片段重定位

需要說明,由浮動代碼的要求,程式碼片段內不應該存在重定位項。此處只是借用了"在代碼
段中"這個短語,實際的重定位項還是位於資料區段的GOT表內。儘管如此,它與3.5節"數
據段中的重定位"的區別是很明顯的。

a) 裝載GOT表首地址

使用GOT表當然事先要知道它的首地址,然而該首地址會隨運行模組被載入的首地址不同
而不同。Linux使用了一個技巧在運行時求出正確的GOT表首地址。代碼片斷如下,緊接
其後列出的是對應的目標檔案(.o)與動態串連庫(.so)中的重定位項類型:

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

如前所述,該代碼片斷存在於每個函數的入口處。程式第一句把當前程式計數器(IP)
值推進堆棧,第二句又把它從堆棧中彈出來,結果相當於movl %eip, %ebx,只不過合法
的x86指令集中不允許%eip作為運算元而已。然後第三句把%ebx加上一個GOT表與IP值的
差,這個差值是個與動態串連庫載入首地址無關的常數,在串連時即可求出。整個過程
用類C語言描述如下:

%ebx = %eip;
%ebx += ($GOT - %eip)

至此%ebx等於GOT表首地址。

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

b) 引用變數、函數地址

當引用的是靜態變數、靜態函數或字串常量時,使用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(參見3.3節)。這樣,無論直接調用,還是先取得函數地址再間
接調用,程式流程都會轉入PLT表,進而把控制權轉移給載入器。載入器就是利用這個機
會進行動態串連的。

c) 直接調用函數

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

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

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

如果是全域函數,連接器將產生到.PLTfun的相對跳轉指令,之後就如3.3節所述,對全
局函數的第一次調用會把程式流程轉到載入器中去,然後計算函數的入口地址,填充fu
n@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表。簡單進行比較,會得出"不相等"的結論,顯然不正確,所以要特殊處理。

3.5 資料區段重定位

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

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

.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

3.6 總結

下表給出了前面討論得到的全部結果:

.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
------------------------------------------------------------

4 結束語

Windows使用PE檔案格式,Linux使用ELF檔案格式,這是兩種動態串連庫不同的根源。本
文從ELF規範出發,深入討論了Linux動態串連庫的具體實現,目的在於進一步推廣Linu
x的研究與應用。

5 附錄:Linux組譯工具文法

x86體繫結構上的Linux彙編器相容於AT&T System V/386彙編器的文法,與常見的Intel
文法頗有不同,如下表:

AT&T Intel
常數 首碼$:pushl $4 push 4
寄存器 首碼%:%ebx ebx
跳轉指令(絕對位址) 首碼*:jmp *fun
跳轉指令(相對位移) 無標記:jmp fun
目的、源運算元的順序 源在前:movl $4,%eax 目的在前:mov eax,4
運算元尺寸 尾碼b、w、l:movl 修飾符byte ptr等等
變址定址 [base+disp] disp(base)

參考文獻

[1] Executable and Linking Format Spec v1.2, TIS Committee, 1995
http://x86.ddj.com/ftp/manuals/tools/elf.pdf
[2] GNU Project (gcc, libc, binutils), Free Software Foundation, Inc., 1999
http://www.gnu.org/software/
[3] Solaris 2.5 Linker and Libraries Guide, Sun Microsystems Inc., 1999
http://docs.sun.com/
ftp://192.18.99.138/802-1955/802-1955.pdf
[4] SVR4 ABI x86 Supplement, The Santa Cruz Operation, Inc., 1999
http://www.sco.com/developer/devspecs/abx86-4.pdf
[5] ELF: From The Programmer's Perspective, H J Lu, 1995
http://metalab.unc.edu/pub/Linux/GCC/elf.ps.gz
[6] Using ld: The GNU linker, S Chamberlain, Cygnus Support, 1994

http://www.gnu.org/manual/ld-2.9.1/ps/ld.ps.gz

相關文章

聯繫我們

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