動態(tài)符號鏈接的細節(jié)
2008-02-26
Linux支持動態(tài)連接庫,不僅節(jié)省了磁盤、內(nèi)存空間,而且可以提高程序運行效率[1]。不過引入動態(tài)連接庫也可能會帶來很多問題,例如動態(tài)連接庫的調(diào)試 [4]、升級更新[5]和潛在的安全威脅[6][7]。這里主要討論符號的動態(tài)鏈接過程,即程序在執(zhí)行過程中,對其中包含的一些未確定地址的符號進行重定 位的過程[3][8]。
本篇主要參考資料[3]和[8],前者側(cè)重實踐,后者側(cè)重原理,把兩者結合起來就方便理解程序的動態(tài)鏈接過程了。另外,動態(tài)連接庫的創(chuàng)建、使用以及調(diào)用動態(tài)連接庫的部分參考了資料[1][2]。
下面先來看看幾個基本概念,接著就介紹動態(tài)連接庫的創(chuàng)建、隱式和顯示調(diào)用,最后介紹符號的動態(tài)鏈接細節(jié)。
1、基本概念
1.1 ELF
ELF是Linux支持的一種程序文件格式,本身包含重定位、執(zhí)行、共享(動態(tài)連接庫)三種類型。(man elf)
代碼:
Code:
[Ctrl+A Select All]
演示:
Quote: |
$ gcc -c test.c #通過-c生成可重定位文件test.o,這里不會進行鏈接 |
雖然ELF文件本身就支持三種不同的類型,不過它有一個統(tǒng)一的結構。這個結構是:
文件頭部(ELF Header)
程序頭部表(Program Header Table)
節(jié)區(qū)1(Section1)
節(jié)區(qū)2(Section2)
節(jié)區(qū)3(Section3)
...
節(jié)區(qū)頭部表(Section Header Table)
無論是文件頭部、程序頭部表、節(jié)區(qū)頭部表,還是節(jié)區(qū),它們都對應著C語言里頭的一些結構體(elf.h中定義)。文件頭部主要描述ELF文件的類型,大 小,運行平臺,以及和程序頭部表和節(jié)區(qū)頭部表相關的信息。節(jié)區(qū)頭部表則用于可重定位文件,以便描述各個節(jié)區(qū)的信息,這些信息包括節(jié)區(qū)的名字、類型、大小 等。程序頭部表則用于描述可執(zhí)行文件或者動態(tài)連接庫,以便系統(tǒng)加載和執(zhí)行它們。而節(jié)區(qū)主要存放各種特定類型的信息,比如程序的正文區(qū)(代碼)、數(shù)據(jù)區(qū)(初 始化和未初始化的數(shù)據(jù))、調(diào)試信息、以及用于動態(tài)鏈接的一些節(jié)區(qū),比如解釋器(.interp)節(jié)區(qū)將指定程序動態(tài)裝載/連接器ld-linux.so的 位置,而過程鏈接表(plt)、全局偏移表(got)、重定位表則用于輔助動態(tài)鏈接過程。
1.2 符號
對于可執(zhí)行文件除了編譯器引入的一些符號外,主要就是用戶自定義的全局變量,函數(shù)等,而對于可重定位文件僅僅包含用戶自定義的一些符號。
Quote: |
$ gcc -c test.c #生成可重定位文件test.o |
1.3 重定位:"是將符號引用與符號定義進行連接的過程"[8]
從上面的演示可以看出,重定位文件test.o中的符號地址都是沒有確定的,而經(jīng)過“靜態(tài)"鏈接(gcc默認調(diào)用ld進行鏈接)以后有兩個符號地址已經(jīng)確 定了,這樣一個確定符號地址的過程實際上就是鏈接的實質(zhì)。鏈接過后,對符號的引用變成了對地址(定義符號時確定該地址)的引用,這樣程序運行時就可通過訪 問內(nèi)存地址而訪問特定的數(shù)據(jù)。
我們也注意到符號printf在可重定位文件和可執(zhí)行文件中的地址都沒有確定,這意味著該符號是一個外部符號,可能定義在動態(tài)連接庫中,在程序運行時需要通過動態(tài)鏈接器(ld-linux.so)進行重定位,即動態(tài)鏈接。
通過這個演示可以看出printf確實在glibc中有定義。
Quote: |
$ nm /lib/libc.so.6 | grep "\ printf$" |
1.4 動態(tài)鏈接
動態(tài)鏈接就是在程序運行時對符號進行重定位,確定符號對應的內(nèi)存地址的過程。
Linux下符號的動態(tài)鏈接默認采用Lazy Mode方式[3],也就是說在程序運行過程中用到該符號時才去解析它的地址。這樣一種符號解析方式有一個好處:只解析那些用到的符號,而對那些不用的符號則永遠不用解析,從而提高程序的執(zhí)行效率。
不過這種默認是可以通過設置LD_BIND_NOW為非空來打破的(下面會通過實例來分析這個變量的作用),也就是說如果設置了這個變量,動態(tài)鏈接器將在程序加載后和符號被使用之前就對這些符號的地址進行解析。
1.5 動態(tài)連接庫
上面提到重定位的過程就是對符號引用和符號地址進行鏈接的過程,而動態(tài)鏈接過程涉及到的符號引用和符號定義分別對應可執(zhí)行文件和動態(tài)連接庫,在可執(zhí)行文件中可能引用了某些動態(tài)連接庫中定義的符號,這類符號通常是函數(shù)。
為了讓動態(tài)鏈接器能夠進行符號的重定位,必須把動態(tài)連接庫的相關信息寫入到可執(zhí)行文件當中,這些信息是什么呢?
Quote: |
$ readelf -d test | grep NEEDED |
ELF文件有一個特別的節(jié)區(qū),.dynamic,它存放了和動態(tài)鏈接相關的很多信息,例如動態(tài)鏈接器通過它找到該文件使用的動態(tài)連接庫。不過,該信息并未包含動態(tài)連接庫libc.so.6的絕對路徑,那動態(tài)鏈接器去哪里查找相應的庫呢?
通過LD_LIBRARY_PATH參數(shù),它類似shell解釋器中用于查找可執(zhí)行文件的PATH環(huán)境變量,也是通過冒號分開指定了各個存放庫函數(shù)的路 徑。該變量實際上也可以通過/etc/ld.so.conf文件來指定,一行對應一個路徑名。為了提高查找和加載動態(tài)連接庫的效率,系統(tǒng)啟動后會通過 ldconfig工具創(chuàng)建一個庫的緩存/etc/ld.so.cache。如果用戶通過/etc/ld.so.conf加入了新的庫搜索路徑或者是把新庫 加到某個原有的庫目錄下,最好是執(zhí)行一下ldconf以便刷新緩存。
需要補充的是,因為動態(tài)連接庫本身還可能引用其他的庫,那么一個可執(zhí)行文件的動態(tài)符號鏈接過程可能涉及到多個庫,通過read -d可以打印出該文件直接依賴的庫,而通過ldd命令則可以打印出所有依賴或者間接依賴的庫。
Quote: |
$ ldd test |
lib.so.6通過read -d就可以看到的,是直接依賴的庫;而linux-gate.so.1在文件系統(tǒng)中并沒有對應的庫文件,它是一個虛擬的動態(tài)連接庫,對應進程內(nèi)存映像的內(nèi) 核部分,更多細節(jié)請參考資料[11];而/lib/ld-linux.so.2正好是動態(tài)鏈接器,系統(tǒng)需要用它來進行符號重定位。那ldd是怎么知道 /lib/ld-linux.so就是該文件的動態(tài)鏈接器呢?
那是因為ELF文件通過專門的節(jié)區(qū)指定了動態(tài)鏈接器,這個節(jié)區(qū)就是.interp。
Quote: |
$ readelf -x .interp test |
可以看到這個節(jié)區(qū)剛好有字符串/lib/ld-linux.so.2,即ld-linux.so的絕對路徑。
我們發(fā)現(xiàn),與libc.so不同的是,ld-linux.so的路徑是絕對路徑,而libc.so僅僅包含了文件名。原因是:程序被執(zhí)行時,ld- linux.so將最先被裝載到內(nèi)存中,沒有其他程序知道去哪里查找ld-linux.so,所以它的路徑必須是絕對的;當ld-linux.so被裝載 以后,由它來去裝載可執(zhí)行文件和相關的共享庫,它將根據(jù)PATH變量和LD_LIBRARY_PATH變量去磁盤上查找它們,因此可執(zhí)行文件和共享庫都可 以不指定絕對路徑。
下面著重介紹動態(tài)連接器本身。
1.6 動態(tài)連接器(dynamic linker/loader)
Linux下elf文件的動態(tài)鏈接器是ld-linux.so,即/lib/ld-linux.so.2。從名字來看和靜態(tài)連接器ld(gcc默認使用的 連接器,見參考資料[10])類似。通過man ld-linux可以獲取與動態(tài)鏈接器相關的資料,包括各種相關的環(huán)境變量和文件都有詳細的說明。
對于環(huán)境變量,除了上面提到過的LD_LIBRARY_PATH和LD_BIND_NOW變量外,還有其他幾個重要參數(shù),比如LD_PRELOAD用于指 定預裝載一些庫,以便替換其他庫中的函數(shù),從而做一些安全方面的處理[6][9][12],而環(huán)境變量LD_DEBUG可以用來進行動態(tài)鏈接的相關調(diào)試。
對于文件,除了上面提到的ld.so.conf和ld.so.cache外,還有一個文件/etc/ld.so.preload用于指定需要預裝載的庫。
從上一小節(jié)中發(fā)現(xiàn)有一個專門的節(jié)區(qū).interp存放有動態(tài)鏈接器,但是這個節(jié)區(qū)為什么叫做.interp(interpeter)呢?因為當shell 解釋器或者其他父進程通過exec啟動我們的程序時,系統(tǒng)會先為ld-linux創(chuàng)建內(nèi)存映像,然后把控制權交給ld-linux,之后ld-linux 負責為可執(zhí)行程序提供運行環(huán)境,負責解釋程序的運行,因此ld-linux也叫做dynamic loader(或intepreter)(關于程序的加載過程請參考資料[13])
那么在exec()之后和程序指令運行之前的過程是怎樣的呢?ld-linux.so主要為程序本身創(chuàng)建了內(nèi)存映像(以下內(nèi)容摘自資料[8]),大體過程如下:
1) 將可執(zhí)行文件的內(nèi)存段添加到進程映像中;
2) 把共享目標內(nèi)存段添加到進程映像中;
3) 為可執(zhí)行文件和它的共享目標(動態(tài)連接庫)執(zhí)行重定位操作;
4) 關閉用來讀入可執(zhí)行文件的文件描述符,如果動態(tài)鏈接程序收到過這樣的文件描述符的話;
5) 將控制轉(zhuǎn)交給程序,使得程序好像從exec()直接得到控制
關于第1)步,在ELF文件的文件頭中就指定了該文件的入口地址,程序的代碼和數(shù)據(jù)部分會相繼map到對應的內(nèi)存中。而關于可執(zhí)行文件本身的路徑,如果指定了PATH環(huán)境變量,ld-linux會到PATH指定的相關目錄下查找。
Quote: |
$ readelf -h test | grep Entry |
對于第2)步,上一節(jié)提到的.dynamic節(jié)區(qū)指定了可執(zhí)行文件依賴的庫名,ld-linux(在這里叫做動態(tài)裝載器或程序解釋器比較合適)再從 LD_LIBRARY_PATH指定的路徑中找到相關的庫文件或者直接從/etc/ld.so.cache庫緩沖中加載相關庫到內(nèi)存中。(關于進程的內(nèi)存 映像,推薦參考資料[14])
對于第3)步,在前面已提到,如果設置了LD_BIND_NOW環(huán)境變量,這個動作就會在此時發(fā)生,否則將會采用lazy mode方式,即當某個符號被使用時才會進行符號的重定位。不過無論在什么時候發(fā)生這個動作,重定位的過程大體是一樣的(在后面將主要介紹該過程)。
對于第4)步,這個主要是釋放文件描述符。
對于第5)步,動態(tài)鏈接器把程序控制權交還給程序。
現(xiàn)在關心的主要是第3步,即如何進行符號的重定位?下面來探求這個過程。期間會逐步討論到和動態(tài)鏈接密切相關的三個數(shù)據(jù)結構,它們分別是ELF文件的過程鏈接表、全局偏移表和重定位表,這三個表都是ELF文件的節(jié)區(qū)。
1.7 過程鏈接表(plt)
從上面的演示發(fā)現(xiàn),還有一個printf符號的地址沒有確定,它應該在動態(tài)連接庫libc.so中定義,需要進行動態(tài)鏈接。這里假設采用lazy mode方式,即執(zhí)行到printf所在位置時才去解析該符號的地址。
假設當前已經(jīng)執(zhí)行到了printf所在位置,即call printf,我們通過objdump反編譯test程序的正文段看看。
Quote: |
$ objdump -d -s -j .text test | grep printf |
發(fā)現(xiàn),該地址指向了plt(即過程鏈接表)即地址80482a0處。下面查看該地址處的內(nèi)容。
Quote: |
$ objdump -D test | grep "80482a0" | grep -v call |
發(fā)現(xiàn)80482a0地址對應的是一條跳轉(zhuǎn)指令,跳轉(zhuǎn)到0x804958c地址指向的地址。到底0x804958c地址本身在什么地方呢?我們能否從.dynamic節(jié)區(qū)(該節(jié)區(qū)存放了和動態(tài)鏈接相關的數(shù)據(jù))獲取相關的信息呢?
Quote: |
$ readelf -d test |
發(fā)現(xiàn)0x8049578地址和0x804958c地址比較近,通過資料[8]查到前者正好是.got.plt(即過程鏈接表)對應的全局偏移表的入口地址。難道0x804958c正好位于.got.plt節(jié)區(qū)中?
1.8 全局偏移表(got)
現(xiàn)在進入全局偏移表看看,
Quote: |
$ readelf -x .got.plt test |
從上述結果可以看出0x804958c地址(即0x08049588+4)處存放的是a6820408,考慮到我的實驗平臺是i386,字節(jié)順序是 little-endian的,所以實際數(shù)值應該是080482a6,也就是說*(0x804958c)的值是080482a6,這個地址剛好是過程鏈接 表的最后一項call 80482a0<printf@plt>中80482a0地址往后偏移6個字節(jié),容易猜到該地址應該就是jmp指令的后一條地址。
Quote: |
$ objdump -d -d -s -j .plt test | grep "080482a0 <printf@plt>:" -A 3 |
80482a6地址恰巧是一條push指令,隨后是一條jmp指令(暫且不管push指令入棧的內(nèi)容有什么意義),執(zhí)行完push指令之后,就會跳轉(zhuǎn)到8048270地址處,下面看看8048270地址處到底有哪些指令。
Quote: |
$ objdump -d -d -s -j .plt test | grep -v "jmp 8048270 <_init+0x18>" | grep "08048270" -A 2 |
同樣是一條入棧指令跟著一條跳轉(zhuǎn)指令。不過這兩個地址0x804957c和0x8049580是連續(xù)的,而且都很熟悉,剛好都在.got.plt表里頭 (從上面我們已經(jīng)知道.got.plt的入口是0x08049578)。這樣的話,我們得確認這兩個地址到底有什么內(nèi)容。
Quote: |
$ readelf -x .got.plt test |
不過,遺憾的是通過readelf查看到的這兩個地址信息都是0,它們到底是什么呢?
現(xiàn)在只能求助參考資料[8],該資料的“3.8.5 過程鏈接表”部分在介紹過程鏈接表和全局偏移表相互合作解析符號的過程中的三步涉及到了這兩個地址和前面沒有說明的push $0x10指令。
1) 在程序第一次創(chuàng)建內(nèi)存映像時,動態(tài)鏈接器為全局偏移表的第二(0x804957c)和第三項(0x8049580)設置特殊值。
2) 原步驟5。在跳轉(zhuǎn)到08048270 <__gmon_start__@plt-0x10>,即過程鏈接表的第一項之前,有一條壓入棧指令,即push $0x10,0x10是相對于重定位表起始地址的一個偏移地址,這個偏移地址到底有什么用呢?它應該是提供給動態(tài)鏈接器的什么信息吧?后面再說明。
3) 原步驟6。跳轉(zhuǎn)到過程鏈接表的第一項之后,壓入了全局偏移表中的第二項(即0x804957c處),“為動態(tài)鏈接器提供了識別信息的機會”(具體是什么 呢?后面會簡單提到,但這個并不是很重要),然后跳轉(zhuǎn)到全局偏移表的第三項(0x8049580,這一項比較重要),把控制權交給動態(tài)連接器。
從這三步發(fā)現(xiàn)程序運行時地址0x8049580處存放的應該是動態(tài)連接器的入口地址,而重定位表0x10位置處和0x804957c處應該為動態(tài)連接器提供了解析符號需要的某些信息。
在繼續(xù)之前先總結一下過程鏈接表和全局偏移表。上面的操作過程僅僅從“局部”看過了這兩個表,但是并沒有宏觀地看里頭的內(nèi)容。下面將宏觀的分析一下, 對于過程鏈接表:
Quote: |
$ objdump -d -d -s -j .plt test |
除了該表中的第一項外,其他各項實際上是類似的。而最后一項080482a0 <printf@plt>和第一項我們都分析過,因此不難理解其他幾項的作用。過程鏈接表沒有辦法單獨工作,因為它和全局偏移表是關聯(lián)的,所 以在說明它的作用之前,先從總體上來看一下全局偏移表。
Quote: |
$ readelf -x .got.plt test |
比較全局偏移表中0x08049584處開始的數(shù)據(jù)和過程鏈接表第二項開始的連續(xù)三項中push指定所在的地址,不難發(fā)現(xiàn),它們是對應的。而 0x0804958c即push 0x10對應的地址我們剛才提到過(下一節(jié)會進一步分析),其他幾項的作用類似,都是跳回到過程鏈接表的push指令處,隨后就跳轉(zhuǎn)到過程鏈接表的第一 項,以便解析相應的符號(實際上過程鏈接表的第一個表項是進入動態(tài)鏈接器,而之前的連續(xù)兩個指令則傳送了需要解析的符號等信息)。另外 0x08049578和0x08049580處分別存放有傳遞給動態(tài)連接庫的相關信息和動態(tài)鏈接器本身的入口地址。但是還有一個地址 0x08049578,這個地址剛好是.dynamic的入口地址,該節(jié)區(qū)存放了和動態(tài)鏈接過程相關的信息,資料[8]提到這個表項實際上保留給動態(tài)鏈接 器自己使用的,以便在不依賴其他程序的情況下對自己進行初始化,所以下面將不再關注該表項。
Quote: |
$ objdump -D test | grep 080494ac |
1.9 重定位表
這里主要接著上面的push 0x10指令來分析。通過資料[8]發(fā)現(xiàn)重定位表包含如何修改其他節(jié)區(qū)的信息,以便動態(tài)鏈接器對某些節(jié)區(qū)內(nèi)的符號地址進行重定位(修改為新的地址)。那到底重定位表項提供了什么樣的信息呢?
每一個重定位項有三部分內(nèi)容,我們重點關注前兩部分。
第一部分是r_offset,這里考慮的是可執(zhí)行文件,因此根據(jù)資料發(fā)現(xiàn),它的取值是被重定位影響(可以說改變或修改)到的存儲單元的虛擬地址。
第二部分是r_info,此成員給出要進行重定位的符號表索引(重定位表項引用到的符號表),以及將實施的重定位類型(如何進行符號的重定位)。(Type)。
先來看看重定位表的具體內(nèi)容,
Quote: |
$ readelf -r test |
僅僅關注和過程鏈接表相關的.rel.plt部分,0x10剛好是1*16+0*1,即16字節(jié),作為重定位表的偏移,剛好對應該表的第三行。發(fā)現(xiàn)這個結 果中竟然包含了和printf符號相關的各種信息。不過重定位表中沒有直接指定符號printf,而是根據(jù)r_info部分從動態(tài)符號表中計算出來的,注 意觀察上述結果中的Info一列的1,2,4和下面結果的Num列的對應關系。
Quote: |
$ readelf -s test | grep ".dynsym" -A 6 |
也就是說在執(zhí)行過程鏈接表中的第一項的跳轉(zhuǎn)指令("jmp *0x8049580")調(diào)用動態(tài)鏈接器以后,動態(tài)連接器因為有了push 0x10,從而可以通過該重定位表項中的r_info找到對應符號(printf)在符號表(.dynsym)中的相關信息。
除此之外,符號表中還有Offset(r_offset)以及Type這兩個重要信息,前者表示該重定位操作后可能影響的地址0804958c,這個地址 剛好是got表項的最后一項,原來存放的是push 0x10指令的地址。這意味著,該地址處的內(nèi)容將被修改,而如何修改呢?根據(jù)Type類型R_386_JUMP_SLOT,通過資料[8]查找到該類型對 應的說明如下(原資料有誤,下面做了修改):
Quote: |
鏈接編輯器創(chuàng)建這種重定位類型主要是為了支持動態(tài)鏈接。其偏移地址成員給出過程鏈接表項的位置。動態(tài)鏈接器修改全局偏移表項的內(nèi)容,把控制傳輸給指定符號的地址。 |
這說明,動態(tài)連接器將根據(jù)該類型對全局偏移表中的最有一項,即0804958c地址處的內(nèi)容進行修改,修改為符號的實際地址,即printf函數(shù)在動態(tài)連接庫的內(nèi)存映像中的地址。
到這里,動態(tài)鏈接的宏觀過程似乎已經(jīng)了然于心,不過一些細節(jié)還是不太清楚。
下面先介紹動態(tài)連接庫的創(chuàng)建,隱式調(diào)用和顯示調(diào)用,接著進一步澄清上面還不太清楚的細節(jié),即全局偏移表中第二項到底傳遞給了動態(tài)連接器什么信息?第三項是 否就是動態(tài)連接器的地址?并討論通過設置LD_BIND_NOW而不采用默認的lazy mode進行動態(tài)鏈接和采用lazy mode動態(tài)鏈接的區(qū)別?
2、動態(tài)連接庫的創(chuàng)建和調(diào)用
在介紹動態(tài)符號鏈接的更多細節(jié)之前,先來了解一下動態(tài)連接庫的創(chuàng)建和兩種使用方法,進而引出符號解析的后臺細節(jié)。
首先來創(chuàng)建一個簡單動態(tài)連接庫。
代碼:
Code:
[Ctrl+A Select All]
Code:
[Ctrl+A Select All]
演示:
Quote: |
$ gcc -c myprintf.c |
得到三個文件libmyprintf.so,libmyprintf.so.0,libmyprintf.so.0.0,這些庫暫且存放在當前目錄下。這里有一個問題值得關注,那就是為什么要創(chuàng)建兩個符號鏈接呢?為了在不影響兼容性的前提下升級庫[5]。
現(xiàn)在寫一段代碼來使用該庫,調(diào)用其中的myprintf函數(shù),這里是隱式使用該庫:在代碼中并沒有直接使用該庫,而是通過調(diào)用myprintf隱式地使用了該庫,在編譯引用該庫的可執(zhí)行文件時需要通過-l參數(shù)指定該庫的名字。
Code:
[Ctrl+A Select All]
演示:
Quote: |
$ gcc -o test test.c -lmyprintf -L./ -I./ |
LD_LIBRARY_PATH環(huán)境變量使得庫可以放到某些指定的路徑下面,而無須在調(diào)用程序中顯式的指定該庫的絕對路徑,這樣避免了把程序限制在某些絕對路徑下,方便庫的移動。
雖然顯式調(diào)用有不便,但是能夠避免隱式調(diào)用搜索路徑的時間消耗,提高效率,除此之外,顯式調(diào)用為我們提供了一組函數(shù)調(diào)用,讓符號的重定位過程一覽無遺。
Code:
[Ctrl+A Select All]
演示:
Quote: |
$ gcc -o test1 test1.c -ldl |
這種情況下,無須包含頭文件。從這個代碼中很容易看出符號重定位的過程:
1、首先通過dlopen找到依賴庫,并加載到內(nèi)存中,再返回該庫的handle,通過dlopen我們可以指定RTLD_LAZY采用lazy mode動態(tài)鏈接模式,如果采用RTLD_NOW則和隱式調(diào)用時設置LD_BIN_NOW類似。
2、找到該庫以后就是對某個符號進行重定位,這里是確定myprintf函數(shù)的地址。
3、找到函數(shù)地址以后就可以直接調(diào)用該函數(shù)了。
關于dlopen,dlsym等后臺工作細節(jié)建議參考資料[15]。
隱式調(diào)用的動態(tài)符號鏈接過程和上面類似。下面通過一些實例來確定之前沒有明確的兩個內(nèi)容:即全局偏移表中的第二項和第三項,并進一步討論lazy mode和非lazy mode的區(qū)別。
3、動態(tài)鏈接過程
因為通過ELF文件,我們就可以確定全局偏移表的位置,因此為了確定全局偏移表位置的第三項和第四項的內(nèi)容,有兩種辦法:
1、通過gdb調(diào)試。
2、直接在函數(shù)內(nèi)部打印。
因為資料[3]詳細介紹了第一種方法,這里現(xiàn)試著通過第二種方法來確定這兩個地址的值。
Code:
[Ctrl+A Select All]
在寫好上面的代碼后就需要確定全局偏移表的地址,然后把該地址設置為代碼中的宏GOT。
Quote: |
$ make got |
把地址0x8049614替換到上述代碼中,然后重新編譯運行,查看結果。
Quote: |
$ make got |
通過兩次運行,發(fā)現(xiàn)全局偏移表中的這兩項是變化的,并且printf的地址對應的new_addr也是變化的,說明libc和ld-linux這兩個庫啟動以后對應的虛擬地址并不確定。因此,無法直接跟蹤到那個地址處的內(nèi)容,還得借助調(diào)試工具,以便確認它們。
下面重新編譯got,加上-g參數(shù)以便調(diào)試,并通過調(diào)試確認got2,got3,以及調(diào)用printf前后printf地址的重定位情況。
Quote: |
$ gcc -g -o got got.c |
通過演示發(fā)現(xiàn)一個問題(1)(2),即本來調(diào)用的是printf,為什么會進行puts的重定位呢?通過gcc -S參數(shù)編譯生成匯編代碼后發(fā)現(xiàn),gcc把printf替換成了puts,因此不難理解程序運行過程為什么對puts進行了重定位。
從演示中不難發(fā)現(xiàn),當符號被使用到時才進行重定位。因為通過調(diào)試發(fā)現(xiàn)在執(zhí)行printf之后,GOT表項的最后一項才被修改為printf(確切的說是puts)的地址。這就是所謂的lazy mode動態(tài)符號鏈接方式。
除此之外,我們?nèi)菀装l(fā)現(xiàn)GOT表第三項確實是ld-linux.so中的某個函數(shù)地址,并且發(fā)現(xiàn)在執(zhí)行printf語句之前,先進入了ld- linux.so的_dl_runtime_resolve函數(shù),而且在它返回之后,GOT表的最后一項才變?yōu)閜rintf(puts)的地址。
本來打算通過第一個斷點確認第二次調(diào)用printf時不再需要進行動態(tài)符號鏈接的,不過因為gcc把第一個替換成了puts,所以這里沒有辦法繼續(xù)調(diào)試。 如果想確認這個,你可以通過寫兩個一樣的printf語句看看。實際上第一次鏈接以后,GOT表的第三項已經(jīng)修改了,當下次再進入過程鏈接表,并執(zhí)行 “jmp *(全局偏移表中某一個地址)”指令時,*(全局偏移表中某一個地址)已經(jīng)被修改為了對應符號的實際地址,這樣jmp語句會自動跳轉(zhuǎn)到符號的地址處運行, 執(zhí)行具體的函數(shù)代碼,因此無須再進行重定位。
到現(xiàn)在GOT表中只剩下第二項還沒有被確認,通過資料[3]我們發(fā)現(xiàn),該項指向一個link_map類型的數(shù)據(jù),是一個鑒別信息,具體作用對我們來說并不是很重要,如果想了解,請參考資料[16]。
下面通過設置LD_BIND_NOW再運行一下got程序并查看結果,比較它與默認的動態(tài)鏈接方式(lazy mode)的異同。
Quote: |
$ LD_BIND_NOW=1 ./got #設置LD_BIND_NOW環(huán)境變量的運行結果 |
通過比較容易發(fā)現(xiàn),在非lazy mode(設置LD_BIND_NOW后)下,程序運行之前符號的地址就已經(jīng)被確定,即調(diào)用printf之前GOT表的最后一項已經(jīng)被確定為了 printf函數(shù)對應的地址,即0xb7e61a20,因此在程序運行之后,GOT表的第二項和第三項就保持為0,因為此時不再需要它們進行符號的重定位 了。通過這樣一個比較,就更容易理解lazy mode的特點了:在用到的時候才解析。
到這里,符號動態(tài)鏈接的細節(jié)基本上就已經(jīng)清楚了。
參考資料:
[1] LINUX系統(tǒng)中動態(tài)鏈接庫的創(chuàng)建與使用
http://www.ccw.com.cn/htm/app/linux/develop/01_8_6_2.asp
[2] LINUX動態(tài)鏈接庫高級應用
http://www.vchome.net/tech/dll/dll9.htm
[3] ELF動態(tài)解析符號過程(修訂版)
http://elfhack.whitecell.org/mydocs/ELF_symbol_resolve_process1.txt
[4] 如何在 Linux 下調(diào)試動態(tài)鏈接庫
http://unix-cd.com/unixcd12/article_5065.html
[5] Dissecting shared libraries
http://www.ibm.com/developerworks/linux/library/l-shlibs.html
[6] 關于Linux和Unix動態(tài)連接庫的安全
http://fanqiang.chinaunix.net/safe/system/2007-02-01/4870.shtml
[7] Linux系統(tǒng)下解析Elf文件DT_RPATH后門
http://article.pchome.net/content-323084.html
[8] ELF 文件格式分析
http://162.105.203.48/web/gaikuang/submission/TN05.ELF.Format.Summary.pdf
[9] C語言程序緩沖區(qū)注入分析(第二部分:緩沖區(qū)溢出和注入實例)
http://oss.lzu.edu.cn/blog/blog.php?do_showone/tid_1540.html
[10] GCC編譯的背后(第二部分:匯編和鏈接)
http://oss.lzu.edu.cn/blog/blog.php?do_showone/tid_1546.html
[11] What is Linux-gate.so.1
http://www.trilithium.com/johan/2005/08/linux-gate/
http://isomerica.net/archives/2007/05/28/what-is-linux-gateso1-and-why-is-it-missing-on-x86-64/
http://www.linux010.cn/program/Linux-gateso1-DeHanYi-pcee6103.htm
[12] Linux下緩沖區(qū)溢出攻擊的原理及對策
http://www.ibm.com/developerworks/cn/linux/l-overflow/index.html
[13] Linux命令行上程序執(zhí)行的那一剎那
http://oss.lzu.edu.cn/blog/blog.php?do_showone/tid_1543.html
[14] C語言程序緩沖區(qū)注入分析(第一部分:進程的內(nèi)存映像)
http://oss.lzu.edu.cn/blog/blog.php?do_showone/tid_1539.html
[15] Intel平臺下Linux中ELF文件動態(tài)鏈接的加載、解析及實例分析
http://www.ibm.com/developerworks/cn/linux/l-elf/part1/index.html
http://www.ibm.com/developerworks/cn/linux/l-elf/part2/index.html
[16] ELF file format and ABI
http://www.x86.org/ftp/manuals/tools/elf.pdf
http://www.muppetlabs.com/~breadbox/software/ELF.txt