ELF(Executable and Linking Format)是一種對(duì)象文件的格式,用于定義不同類型的對(duì)象文件(Object files)中都放了什么東西、以及都以什么樣的格式去放這些東西。它自最早在 System V 系統(tǒng)上出現(xiàn)后,被 xNIX 世界所廣泛接受,作為缺省的二進(jìn)制文件格式來(lái)使用??梢哉f(shuō),ELF是構(gòu)成眾多xNIX系統(tǒng)的基礎(chǔ)之一,所以作為嵌入式Linux系統(tǒng)乃至內(nèi)核驅(qū)動(dòng)程序開(kāi)發(fā)人員,你最好熟悉并掌握它。
其實(shí),關(guān)于ELF這個(gè)主題,網(wǎng)絡(luò)上已經(jīng)有相當(dāng)多的文章存在,但是其介紹的內(nèi)容比較分散,使得初學(xué)者不太容易從中得到一個(gè)系統(tǒng)性的認(rèn)識(shí)。為了幫助大家學(xué)習(xí),我這里打算寫(xiě)一系列連貫的文章來(lái)介紹ELF以及相關(guān)的應(yīng)用。這是這個(gè)系列中的第一篇文章,主要是通過(guò)不同工具的使用來(lái)熟悉ELF文件的內(nèi)部結(jié)構(gòu)以及相關(guān)的基本概念。后面的文章,我們會(huì)介紹很多高級(jí)的概念和應(yīng)用,比方動(dòng)態(tài)鏈接和加載,動(dòng)態(tài)庫(kù)的開(kāi)發(fā),C語(yǔ)言Main函數(shù)是被誰(shuí)以及如何被調(diào)用的,ELF格式在內(nèi)核中的支持,Linux內(nèi)核中對(duì)ELF section的擴(kuò)展使用等等。
好的,開(kāi)始我們的第一篇文章。在詳細(xì)進(jìn)入正題之前,先給大家介紹一點(diǎn)ELF文件格式的參考資料。在ELF格式出來(lái)之后,TISC(Tool Interface Standard Committee)委員會(huì)定義了一套ELF標(biāo)準(zhǔn)。你可以從這里(http://refspecs.freestandards.org/elf/)找到詳細(xì)的標(biāo)準(zhǔn)文檔。TISC委員會(huì)前后出了兩個(gè)版本,v1.1和v1.2。兩個(gè)版本內(nèi)容上差不多,但就可讀性上來(lái)講,我還是推薦你讀 v1.2的。因?yàn)樵趘1.2版本中,TISC重新組織原本在v1.1版本中的內(nèi)容,將它們分成為三個(gè)部分(books):
a) Book I
介紹了通用的適用于所有32位架構(gòu)處理器的ELF相關(guān)內(nèi)容
b) Book II
介紹了處理器特定的ELF相關(guān)內(nèi)容,這里是以Intel x86 架構(gòu)處理器作為例子介紹
c) Book III
介紹了操作系統(tǒng)特定的ELF相關(guān)內(nèi)容,這里是以運(yùn)行在x86上面的 UNIX System V.4 作為例子介紹
值得一說(shuō)的是,雖然TISC是以x86為例子介紹ELF規(guī)范的,但是如果你是想知道非x86下面的ELF實(shí)現(xiàn)情況,那也可以在http://refspecs.freestandards.org/elf/中找到特定處理器相關(guān)的Supplment文檔。比方ARM相關(guān)的,或者M(jìn)IPS相關(guān)的等等。另外,相比較UNIX系統(tǒng)的另外一個(gè)分支BSD Unix,Linux系統(tǒng)更靠近 System V 系統(tǒng)。所以關(guān)于操作系統(tǒng)特定的ELF內(nèi)容,你可以直接參考v1.2標(biāo)準(zhǔn)中的內(nèi)容。
這里多說(shuō)些廢話:別忘了 Linus 在實(shí)現(xiàn)Linux的第一個(gè)版本的時(shí)候,就是看了介紹Unix內(nèi)部細(xì)節(jié)的書(shū):《The of the Unix Operating System》,得到很多啟發(fā)。這本書(shū)對(duì)應(yīng)的操作系統(tǒng)是System V 的第二個(gè)Release。這本書(shū)介紹了操作系統(tǒng)的很多設(shè)計(jì)觀念,并且行文簡(jiǎn)單易懂。所以雖然現(xiàn)在的Linux也吸取了其他很多Unix變種的設(shè)計(jì)理念,但是如果你想研究學(xué)習(xí)Linux內(nèi)核,那還是以看這本書(shū)作為開(kāi)始為好。這本書(shū)也是我在接觸Linux內(nèi)核之前所看的第一本介紹操作系統(tǒng)的書(shū),所以我極力向大家推薦。(在學(xué)校雖然學(xué)過(guò)操作系統(tǒng)原理,但學(xué)的也是很糟糕最后導(dǎo)致期末考試才四十來(lái)分,記憶仿佛還在昨天:))
好了,還是回來(lái)開(kāi)始我們第一篇ELF主題相關(guān)的文章吧。這篇文章主要是通過(guò)使用不同的工具來(lái)分析對(duì)象文件,來(lái)使你掌握ELF文件的基本格式,以及了解相關(guān)的基本概念。你在讀這篇文章的時(shí)候,希望你在電腦上已經(jīng)打開(kāi)了那個(gè) v1.2 版本的ELF規(guī)范,并對(duì)照著文章內(nèi)容看規(guī)范里的文字。
首先,你需要知道的是所謂對(duì)象文件(Object files)有三個(gè)種類:
1) 可重定位的對(duì)象文件(Relocatable file)
這是由匯編器匯編生成的 .o 文件。后面的鏈接器(link editor)拿一個(gè)或一些 Relocatable object files 作為輸入,經(jīng)鏈接處理后,生成一個(gè)可執(zhí)行的對(duì)象文件 (Executable file) 或者一個(gè)可被共享的對(duì)象文件(Shared object file)。我們可以使用 ar 工具將眾多的 .o Relocatable object files 歸檔(archive)成 .a 靜態(tài)庫(kù)文件。如何產(chǎn)生 Relocatable file,你應(yīng)該很熟悉了,請(qǐng)參見(jiàn)我們相關(guān)的基本概念文章和JulWiki。另外,可以預(yù)先告訴大家的是我們的內(nèi)核可加載模塊 .ko 文件也是 Relocatable object file。
2) 可執(zhí)行的對(duì)象文件(Executable file)
這我們見(jiàn)的多了。文本編輯器vi、調(diào)式用的工具gdb、播放mp3歌曲的軟件mplayer等等都是Executable object file。你應(yīng)該已經(jīng)知道,在我們的 Linux 系統(tǒng)里面,存在兩種可執(zhí)行的東西。除了這里說(shuō)的 Executable object file,另外一種就是可執(zhí)行的腳本(如shell腳本)。注意這些腳本不是 Executable object file,它們只是文本文件,但是執(zhí)行這些腳本所用的解釋器就是 Executable object file,比如 bash shell 程序。
3) 可被共享的對(duì)象文件(Shared object file)
這些就是所謂的動(dòng)態(tài)庫(kù)文件,也即 .so 文件。如果拿前面的靜態(tài)庫(kù)來(lái)生成可執(zhí)行程序,那每個(gè)生成的可執(zhí)行程序中都會(huì)有一份庫(kù)代碼的拷貝。如果在磁盤(pán)中存儲(chǔ)這些可執(zhí)行程序,那就會(huì)占用額外的磁盤(pán)空間;另外如果拿它們放到Linux系統(tǒng)上一起運(yùn)行,也會(huì)浪費(fèi)掉寶貴的物理內(nèi)存。如果將靜態(tài)庫(kù)換成動(dòng)態(tài)庫(kù),那么這些問(wèn)題都不會(huì)出現(xiàn)。動(dòng)態(tài)庫(kù)在發(fā)揮作用的過(guò)程中,必須經(jīng)過(guò)兩個(gè)步驟:
a) 鏈接編輯器(link editor)拿它和其他Relocatable object file以及其他shared object file作為輸入,經(jīng)鏈接處理后,生存另外的 shared object file 或者 executable file。
b) 在運(yùn)行時(shí),動(dòng)態(tài)鏈接器(dynamic linker)拿它和一個(gè)Executable file以及另外一些 Shared object file 來(lái)一起處理,在Linux系統(tǒng)里面創(chuàng)建一個(gè)進(jìn)程映像。
以上所提到的 link editor 以及 dynamic linker 是什么東西,你可以參考我們基本概念中的相關(guān)文章。對(duì)于什么是編譯器,匯編器等你應(yīng)該也已經(jīng)知道,在這里只是使用他們而不再對(duì)他們進(jìn)行詳細(xì)介紹。為了下面的敘述方便,你可以下載test.tar.gz包,解壓縮后使用"make"進(jìn)行編譯。編譯完成后,會(huì)在目錄中生成一系列的ELF對(duì)象文件,更多描述見(jiàn)里面的 README 文件。我們下面的論述都基于這些產(chǎn)生的對(duì)象文件。
make所產(chǎn)生的文件,包括 sub.o/sum.o/test.o/libsub.so/test 等等都是ELF對(duì)象文件。至于要知道它們都屬于上面三類中的哪一種,我們可以使用 file 命令來(lái)查看:
[yihect@juliantec test]$ file sum.o sub.o test.o libsub.so test
sum.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped
sub.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped
test.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped
libsub.so: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), not stripped
test: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), for GNU/Linux 2.2.5, dynamically linked (uses shared libs), not stripped
結(jié)果很清楚的告訴我們他們都屬于哪一個(gè)類別。比方 sum.o 是應(yīng)用在x86架構(gòu)上的可重定位文件。這個(gè)結(jié)果也間接的告訴我們,x86是小端模式(LSB)的32位結(jié)構(gòu)。那對(duì)于 file 命令來(lái)說(shuō),它又能如何知道這些信息?答案是在ELF對(duì)象文件的最前面有一個(gè)ELF文件頭,里面記載了所適用的處理器、對(duì)象文件類型等各種信息。在TISCv1.2的規(guī)范中,用下面的圖描述了ELF對(duì)象文件的基本組成,其中ELF文件頭赫然在目。

等等,為什么會(huì)有左右兩個(gè)很類似的圖來(lái)說(shuō)明ELF的組成格式?這是因?yàn)镋LF格式需要使用在兩種場(chǎng)合:
a) 組成不同的可重定位文件,以參與可執(zhí)行文件或者可被共享的對(duì)象文件的鏈接構(gòu)建;
b) 組成可執(zhí)行文件或者可被共享的對(duì)象文件,以在運(yùn)行時(shí)內(nèi)存中進(jìn)程映像的構(gòu)建。
所以,基本上,圖中左邊的部分表示的是可重定位對(duì)象文件的格式;而右邊部分表示的則是可執(zhí)行文件以及可被共享的對(duì)象文件的格式。正如TISCv1.2規(guī)范中所闡述的那樣,ELF文件頭被固定地放在不同類對(duì)象文件的最前面。至于它里面的內(nèi)容,除了file命令所顯示出來(lái)的那些之外,更重要的是包含另外一些數(shù)據(jù),用于描述ELF文件中ELF文件頭之外的內(nèi)容。如果你的系統(tǒng)中安裝有 GNU binutils 包,那我們可以使用其中的 readelf 工具來(lái)讀出整個(gè)ELF文件頭的內(nèi)容,比如:
[yihect@juliantec test]$ readelf -h ./sum.o ELF Header: Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Class: ELF32 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: REL (Relocatable file) Machine: Intel 80386 Version: 0x1 Entry point address: 0x0 Start of program headers: 0 (bytes into file) Start of section headers: 184 (bytes into file) Flags: 0x0 Size of this header: 52 (bytes) Size of program headers: 0 (bytes) Number of program headers: 0 Size of section headers: 40 (bytes) Number of section headers: 9 Section header string table index: 6
這個(gè)輸出結(jié)果能反映出很多東西。那如何來(lái)看這個(gè)結(jié)果中的內(nèi)容,我們還是就著TISCv1.2規(guī)范來(lái)。在實(shí)際寫(xiě)代碼支持ELF格式對(duì)象文件格式的時(shí)候,我們都會(huì)定義許多C語(yǔ)言的結(jié)構(gòu)來(lái)表示ELF格式的各個(gè)相關(guān)內(nèi)容,比方這里的ELF文件頭,你就可以在TISCv1.2規(guī)范中找到這樣的結(jié)構(gòu)定義(注意我們研究的是針對(duì)x86架構(gòu)的ELF,所以我們只考慮32位版本,而不考慮其他如64位之類的):

這個(gè)結(jié)構(gòu)里面出現(xiàn)了多種數(shù)據(jù)類型,同樣可以在規(guī)范中找到相關(guān)說(shuō)明:

在我們以后一系列文章中,我們會(huì)著重拿實(shí)際的程序代碼來(lái)分析,介時(shí)你會(huì)在頭文件中找到同樣的定義。但是這里,我們只討論規(guī)范中的定義,暫不考慮任何程序代碼。在ELF頭中,字段e_machine和e_type指明了這是針對(duì)x86架構(gòu)的可重定位文件,最前面有個(gè)長(zhǎng)度為16字節(jié)的字段中有一個(gè)字節(jié)表示了它適用于32bits機(jī)器,而不是64位的。除了這些之外,另外ELF頭還告訴了我們其他一些特別重要的信息,分別是:
a) 這個(gè)sum.o的進(jìn)入點(diǎn)是0x0(e_entry),這表面Relocatable objects不會(huì)有程序進(jìn)入點(diǎn)。所謂程序進(jìn)入點(diǎn)是指當(dāng)程序真正執(zhí)行起來(lái)的時(shí)候,其第一條要運(yùn)行的指令的運(yùn)行時(shí)地址。因?yàn)镽elocatable objects file只是供再鏈接而已,所以它不存在進(jìn)入點(diǎn)。而可執(zhí)行文件test和動(dòng)態(tài)庫(kù).so都存在所謂的進(jìn)入點(diǎn),你可以用 readelf -h 看看。后面我們的文章中會(huì)介紹可執(zhí)行文件的e_entry指向C庫(kù)中的_start,而動(dòng)態(tài)庫(kù).so中的進(jìn)入點(diǎn)指向 call_gmon_start。這些后面再說(shuō),這里先不深入討論。
b) 這個(gè)sum.o文件包含有9個(gè)sections,但卻沒(méi)有segments(Number of program headers為0)。
那什么是所謂 sections 呢?可以說(shuō),sections 是在ELF文件里頭,用以裝載內(nèi)容數(shù)據(jù)的最小容器。在ELF文件里面,每一個(gè) sections 內(nèi)都裝載了性質(zhì)屬性都一樣的內(nèi)容,比方:
1) .text section 里裝載了可執(zhí)行代碼;
2) .data section 里面裝載了被初始化的數(shù)據(jù);
3) .bss section 里面裝載了未被初始化的數(shù)據(jù);
4) 以 .rec 打頭的 sections 里面裝載了重定位條目;
5) .symtab 或者 .dynsym section 里面裝載了符號(hào)信息;
6) .strtab 或者 .dynstr section 里面裝載了字符串信息;
7) 其他還有為滿足不同目的所設(shè)置的section,比方滿足調(diào)試的目的、滿足動(dòng)態(tài)鏈接與加載的目的等等。
一個(gè)ELF文件中到底有哪些具體的 sections,由包含在這個(gè)ELF文件中的 section head table(SHT)決定。在SHT中,針對(duì)每一個(gè)section,都設(shè)置有一個(gè)條目,用來(lái)描述對(duì)應(yīng)的這個(gè)section,其內(nèi)容主要包括該 section 的名稱、類型、大小以及在整個(gè)ELF文件中的字節(jié)偏移位置等等。我們也可以在TISCv1.2規(guī)范中找到SHT表中條目的C結(jié)構(gòu)定義:

我們可以像下面那樣來(lái)使用 readelf 工具來(lái)查看可重定位對(duì)象文件 sum.o 的SHT表內(nèi)容:[yihect@juliantec test]$ readelf -S ./sum.o
There are 9 section headers, starting at offset 0xb8:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 00000000 000034 00000b 00 AX 0 0 4
[ 2] .data PROGBITS 00000000 000040 000004 00 WA 0 0 4
[ 3] .bss NOBITS 00000000 000044 000000 00 WA 0 0 4
[ 4] .note.GNU-stack PROGBITS 00000000 000044 000000 00 0 0 1
[ 5] .comment PROGBITS 00000000 000044 00002d 00 0 0 1
[ 6] .shstrtab STRTAB 00000000 000071 000045 00 0 0 1
[ 7] .symtab SYMTAB 00000000 000220 0000a0 10 8 7 4
[ 8] .strtab STRTAB 00000000 0002c0 00001d 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings)
I (info), L (link order), G (group), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)
這個(gè)結(jié)果顯示了 sum.o 中包含的所有9個(gè)sections。因?yàn)閟um.o僅僅是參與link editor鏈接的可重定位文件,而不參與最后進(jìn)程映像的構(gòu)建,所以Addr(sh_addr)為0。后面你會(huì)看到可執(zhí)行文件以及動(dòng)態(tài)庫(kù)文件中大部分sections的這一字段都是有某些取值的。Off(sh_offset)表示了該section離開(kāi)文件頭部位置的距離。Size(sh_size)表示section的字節(jié)大小。ES(sh_entsize)只對(duì)某些形式的sections 有意義。比方符號(hào)表 .symtab section,其內(nèi)部包含了一個(gè)表格,表格的每一個(gè)條目都是特定長(zhǎng)度的,那這里的這個(gè)字段就表示條目的長(zhǎng)度10。Al(sh_addralign)是地址對(duì)齊要求。另外剩下的兩列Lk和Inf,對(duì)應(yīng)著條目結(jié)構(gòu)中的字段sh_link和字段sh_info。它們中記錄的是section head table 中的條目索引,這就意味著,從這兩個(gè)字段出發(fā),可以找到對(duì)應(yīng)的另外兩個(gè) section,其具體的含義解釋依據(jù)不同種類的 section 而不同,后面會(huì)介紹。
注意上面結(jié)果中的 Flg ,表示的是對(duì)應(yīng)section的相關(guān)標(biāo)志。比方.text section 里面存儲(chǔ)的是代碼,所以就是只讀的(X);.data和.bss里面存放的都是可寫(xiě)的(W)數(shù)據(jù)(非在堆棧中定義的數(shù)據(jù)),只不過(guò)前者存的是初始化過(guò)的數(shù)據(jù),比方程序中定義的賦過(guò)初值的全局變量等;而后者里面存儲(chǔ)的是未經(jīng)過(guò)初始化的數(shù)據(jù)。因?yàn)槲唇?jīng)過(guò)初始化就意味著不確定這些數(shù)據(jù)剛開(kāi)始的時(shí)候會(huì)有些什么樣的值,所以針對(duì)對(duì)象文件來(lái)說(shuō),它就沒(méi)必要為了存儲(chǔ)這些數(shù)據(jù)而在文件內(nèi)多留出一塊空間,因此.bss section的大小總是為0。后面會(huì)看到,當(dāng)可執(zhí)行程序被執(zhí)行的時(shí)候,動(dòng)態(tài)連接器會(huì)在內(nèi)存中開(kāi)辟一定大小的空間來(lái)存放這些未初始化的數(shù)據(jù),里面的內(nèi)存單元都被初始化成0。可執(zhí)行程序文件中雖然沒(méi)有長(zhǎng)度非0的 .bss section,但卻記錄有在程序運(yùn)行時(shí),需要開(kāi)辟多大的空間來(lái)容納這些未初始化的數(shù)據(jù)。
另外一個(gè)標(biāo)志A說(shuō)明對(duì)應(yīng)的 section 是Allocable的。所謂 Allocable 的section,是指在運(yùn)行時(shí),進(jìn)程(process)需要使用它們,所以它們被加載器加載到內(nèi)存中去。
而與此相反,存在一些non-Allocable 的sections,它們只是被鏈接器、調(diào)試器或者其他類似工具所使用的,而并非參與進(jìn)程的運(yùn)行中去的那些 section。比方后面要介紹的字符串表section .strtab,符號(hào)表 .symtab section等等。當(dāng)運(yùn)行最后的可執(zhí)行程序時(shí),加載器會(huì)加載那些 Allocable 的部分,而 non-Allocable 的部分則會(huì)被繼續(xù)留在可執(zhí)行文件內(nèi)。所以,實(shí)際上,這些 non-Allocable 的section 都可以被我們用 stip 工具從最后的可執(zhí)行文件中刪除掉,刪除掉這些sections的可執(zhí)行文件照樣能夠運(yùn)行,只不過(guò)你沒(méi)辦法來(lái)進(jìn)行調(diào)試之類的事情罷了。
我們?nèi)匀豢梢允褂?readelf -x SecNum 來(lái)傾印出不同 section 中的內(nèi)容。但是,無(wú)奈其輸出結(jié)果都是機(jī)器碼,對(duì)我們?nèi)藖?lái)說(shuō)不具備可讀性。所以我們換用 binutils 包中的另外一個(gè)工具 objdump 來(lái)看看這些 sections 中到底具有哪些內(nèi)容,先來(lái)看看 .text section 的:[yihect@juliantec test]$ objdump -d -j .text ./sum.o
./sum.o: file format elf32-i386
Disassembly of section .text:
00000000 :
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 8b 45 0c mov 0xc(%ebp),%eax
6: 03 45 08 add 0x8(%ebp),%eax
9: c9 leave
a: c3 ret
objdump 的選項(xiàng) -d 表示要對(duì)由 -j 選擇項(xiàng)指定的 section 內(nèi)容進(jìn)行反匯編,也就是由機(jī)器碼出發(fā),推導(dǎo)出相應(yīng)的匯編指令。上面結(jié)果顯示在 sum.o 對(duì)象文件的 .text 中只是包含了函數(shù) sum_func 的定義。用同樣的方法,我們來(lái)看看 sum.o 中 .data section 有什么內(nèi)容:[yihect@juliantec test]$ objdump -d -j .data ./sum.o
./sum.o: file format elf32-i386
Disassembly of section .data:
00000000 :
0: 17 00 00 00 ....
這個(gè)結(jié)果顯示在 sum.o 的 .data section 中定義了一個(gè)四字節(jié)的變量 gv_inited,其值被初始化成 0x00000017,也就是十進(jìn)制值 23。別忘了,x86架構(gòu)是使用小端模式的。
我們接下來(lái)來(lái)看看字符串表section .strtab。你可以選擇使用 readelf -x :
[yihect@juliantec test]$ readelf -x 8 ./sum.o
Hex dump of section '.strtab':
0x00000000 64657469 6e695f76 6700632e 6d757300 .sum.c.gv_inited
0x00000010 00 68630063 6e75665f 6d757300 .sum_func.ch.
上面命令中的 8 是 .strtab section 在SHT表格中的索引值,從上面所查看的SHT內(nèi)容中可以找到。盡管這個(gè)命令的輸出結(jié)果不是那么具有可讀性,但我們還是得來(lái)說(shuō)一說(shuō)如何看這個(gè)結(jié)果,因?yàn)楹罄m(xù)文章中將會(huì)使用大量的這種命令。上面結(jié)果中的十六進(jìn)制數(shù)據(jù)部分從右到左看是地址遞增的方向,而字符內(nèi)容部分從左到右看是地址遞增的方向。所以,在 .strtab section 中,按照地址遞增的方向來(lái)看,各字節(jié)的內(nèi)容依次是 0x00、0x73、0x75、0x6d、0x2e ....,也就是字符 、's'、'u'、'm'、'.' ... 等。如果還是看不太明白,你可以使用 hexdump 直接dumping出 .strtab section 開(kāi)頭(其偏移在文件內(nèi)0x2c0字節(jié)處)的 32 字節(jié)數(shù)據(jù):
[yihect@juliantec test]$ hexdump -s 0x2c0 -n 32 -c ./sum.o
00002c0 s u m . c g v _ i n i t e d
00002d0 s u m _ f u n c c h
00002dd
.strtab section 中存儲(chǔ)著的都是以字符
為分割符的字符串,這些字符串所表示的內(nèi)容,通常是程序中定義的函數(shù)名稱、所定義過(guò)的變量名稱等等。。。當(dāng)對(duì)象文件中其他地方需要和一個(gè)這樣的字符串相關(guān)聯(lián)的時(shí)候,往往會(huì)在對(duì)應(yīng)的地方存儲(chǔ) .strtab section 中的索引值。比方下面將要介紹的符號(hào)表 .symtab section 中,有一個(gè)條目是用來(lái)描述符號(hào) gv_inited 的,那么在該條目中就會(huì)有一個(gè)字段(st_name)記錄著字符串 gv_inited 在 .strtab section 中的索引 7 。 .shstrtab 也是字符串表,只不過(guò)其中存儲(chǔ)的是 section 的名字,而非所函數(shù)或者變量的名稱。
字符串表在真正鏈接和生成進(jìn)程映像過(guò)程中是不需要使用的,但是其對(duì)我們調(diào)試程序來(lái)說(shuō)就特別有幫助,因?yàn)槲覀內(nèi)丝雌饋?lái)最舒服的還是自然形式的字符串,而非像天書(shū)一樣的數(shù)字符號(hào)。前面使用objdump來(lái)反匯編 .text section 的時(shí)候,之所以能看到定義了函數(shù) sum_func ,那也是因?yàn)榇嬖谶@個(gè)字符串表的原因。當(dāng)然起關(guān)鍵作用的,還是符號(hào)表 .symtab section 在其中作為中介,下面我們就來(lái)看看符號(hào)表。
雖然我們同樣可以使用 readelf -x 來(lái)查看符號(hào)表(.symtab)section的內(nèi)容,但是其結(jié)果可讀性太差,我們換用 readelf -s 或者 objdump -t 來(lái)查看(前者輸出結(jié)果更容易看懂):
[yihect@juliantec test]$ readelf -s ./sum.o
Symbol table '.symtab' contains 10 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FILE LOCAL DEFAULT ABS sum.c
2: 00000000 0 SECTION LOCAL DEFAULT 1
3: 00000000 0 SECTION LOCAL DEFAULT 2
4: 00000000 0 SECTION LOCAL DEFAULT 3
5: 00000000 0 SECTION LOCAL DEFAULT 4
6: 00000000 0 SECTION LOCAL DEFAULT 5
7: 00000000 4 OBJECT GLOBAL DEFAULT 2 gv_inited
8: 00000000 11 FUNC GLOBAL DEFAULT 1 sum_func
9: 00000001 1 OBJECT GLOBAL DEFAULT COM ch
在符號(hào)表內(nèi)針對(duì)每一個(gè)符號(hào),都會(huì)相應(yīng)的設(shè)置一個(gè)條目。在繼續(xù)介紹上面的結(jié)果之前,我們還是從規(guī)范中找出符號(hào)表內(nèi)條目的C結(jié)構(gòu)定義:

上面結(jié)果中 Type 列顯示出符號(hào)的種類。Bind 列定義了符號(hào)的綁定類型。種類和綁定類型合并在一起,由結(jié)構(gòu)中 st_info 字段來(lái)定義。在ELF格式中,符號(hào)類型總共可以有這么幾種:

類型 STT_OBJECT 表示和該符號(hào)對(duì)應(yīng)的是一個(gè)數(shù)據(jù)對(duì)象,比方程序中定義過(guò)的變量、數(shù)組等,比方上面的 gv_inited 和 ch;類型 STT_FUNC 表示該符號(hào)對(duì)應(yīng)的是函數(shù),比方上面的 sum_func函數(shù)。類型 STT_SECTION 表示該符號(hào)和一個(gè) section 相關(guān),這種符號(hào)用于重定位。關(guān)于重定位,我們下文會(huì)介紹。
符號(hào)的綁定類型表示了這個(gè)符號(hào)的可見(jiàn)性,是僅本對(duì)象文件可見(jiàn)呢,還是全局可見(jiàn)。它的取值主要有三種:STB_LOCA、STB_GLOBAL和STB_WEAK,具體的內(nèi)容還請(qǐng)參見(jiàn)規(guī)范。關(guān)于符號(hào),最重要的就是符號(hào)的值(st_value)了。依據(jù)對(duì)象文件的不同類型,符號(hào)的值所表示的含義也略有差異:
a) 在可重定位文件中,如果該符號(hào)對(duì)應(yīng)的section index(上面的Ndx)為SHN_COMMON,那么符號(hào)的值表示的是該數(shù)據(jù)的對(duì)齊要求,比方上面的變量 ch 。
b) 在可重定位文件中,除去上面那條a中定義的符號(hào),對(duì)于其他的符號(hào)來(lái)說(shuō),其值表示的是對(duì)應(yīng) section 內(nèi)的偏移值。比方 gv_inited 變量定義在 .data section 的最前面,所以其值為0。
c) 在可執(zhí)行文件或者動(dòng)態(tài)庫(kù)中,符號(hào)的值表示的是運(yùn)行時(shí)的內(nèi)存地址。
好,咱們?cè)賮?lái)介紹重定位。在所產(chǎn)生的對(duì)象文件 test.o 中有對(duì)函數(shù) sum_func 的引用,這對(duì)我們的x386結(jié)構(gòu)來(lái)說(shuō),其實(shí)就是一條call指令。既然 sum_func 是定義在 sum.o 中的,那對(duì) test.o 來(lái)說(shuō),它就是一個(gè)外部引用。所以,匯編器在產(chǎn)生 test.o 的時(shí)候,它會(huì)產(chǎn)生一個(gè)重定位條目。重定位條目中會(huì)包含以下幾類東西:
1) 它會(huì)包含一個(gè)符號(hào)表中一個(gè)條目的索引,因?yàn)檫@樣我們才知道它具體是哪個(gè)符號(hào)需要被重定位的;
2) 它會(huì)包含一個(gè) .text section 中的地址單元的偏移值。原本這個(gè)偏移值處的地址單元里面應(yīng)該存放著 call 指令的操作數(shù)。對(duì)上面來(lái)說(shuō),也就是函數(shù) sum_func 的地址,但是目前這個(gè)地址匯編器還不知道。
3) 它還會(huì)包含一個(gè)tag,以指明該重定位屬于何種類型。
當(dāng)我們用鏈接器去鏈接這個(gè)對(duì)象文件的時(shí)候,鏈接器會(huì)遍歷所有的重定位條目,碰到像 sum_func 這樣的外部引用,它會(huì)找到 sum_func 的確切地址,并且把它寫(xiě)回到上面 call 指令操作數(shù)所占用的那個(gè)地址單元。像這樣的操作,稱之為重定位操作。link editor 和 dynamic linker 都要完成一些重定位操作,只不過(guò)后者的動(dòng)作更加復(fù)雜,因?yàn)樗窃谶\(yùn)行時(shí)動(dòng)態(tài)完成的,我們以后的文章會(huì)介紹相關(guān)的內(nèi)容。概括一下,所謂重定位操作就是:“匯編的時(shí)候產(chǎn)生一個(gè)空坐位,上面用紅紙寫(xiě)著要坐在這個(gè)座位上的人的名字,然后連接器在開(kāi)會(huì)前安排那個(gè)人坐上去”。
如前面我們說(shuō)過(guò)的,對(duì)象文件中的重定位條目,會(huì)構(gòu)成一個(gè)個(gè)單獨(dú)的 section。這些 section 的名字,常會(huì)是這樣的形式:".rel.XXX"。其中XXX表示的是這些重定位條目所作用到的section,如 .text section。重定位條目所構(gòu)成的section需要和另外兩個(gè)section產(chǎn)生關(guān)聯(lián):符號(hào)表section(表示要重定位的是哪一個(gè)符號(hào))以及受影響地址單元所在的section。在使用工具來(lái)查看重定位section之前,我們先從規(guī)范中找出來(lái)表示重定位條目的結(jié)構(gòu)定義(有兩種,依處理器架構(gòu)來(lái)定):

結(jié)構(gòu)中 r_offset 對(duì)于可重定位文件.o來(lái)說(shuō),就是地址單元的偏移值(前面的b條);另外對(duì)可執(zhí)行文件或者動(dòng)態(tài)庫(kù)來(lái)說(shuō),就是該地址單元的運(yùn)行時(shí)地址。上面 a條中的符號(hào)表內(nèi)索引和c條中的類型,一起構(gòu)成了結(jié)構(gòu)中的字段 r_info。
重定位過(guò)程在計(jì)算最終要放到受影響地址單元中的時(shí)候,需要加上一個(gè)附加的數(shù) addend。當(dāng)某一種處理器選用 Elf32_Rela 結(jié)構(gòu)的時(shí)候,該 addend 就是結(jié)構(gòu)中的 r_addend 字段;否則該 addend 就是原本存儲(chǔ)在受影響地址單元中的原有值。x86架構(gòu)選用 Elf32_Rel 結(jié)構(gòu)來(lái)表示重定位條目。ARM架構(gòu)也是用這個(gè)。
重定位類型意味著如何去修改受影響的地址單元,也就是按照何種方式去計(jì)算需要最后放在受影響單元里面的值。具體的重定位類型有哪些,取決與特定的處理器架構(gòu),你可以參考相關(guān)規(guī)范。這種計(jì)算方式可以非常的簡(jiǎn)單,比如在x386上的 R_386_32 類型,它規(guī)定只是將附加數(shù)加上符號(hào)的值作為所需要的值;該計(jì)算方式也可以是非常的復(fù)雜,比如老版本ARM平臺(tái)上的 R_ARM_PC26。在這篇文章的末尾,我會(huì)詳細(xì)介紹一種重定位類型:R_386_PC32。至于另外一些重要的重定位類型,如R_386_GOTPC,R_386_PLT32,R_386_GOT32,R_386_GLOB_DAT 以及 R_386_JUMP_SLOT 等。讀者可以先自己研究,也許我們會(huì)在后面后面的文章中討論到相關(guān)主題時(shí)再行介紹。
我們可以使用命令 readelf -r 來(lái)查看重定位信息:
[yihect@juliantec test_2]$ readelf -r test.o
Relocation section '.rel.text' at offset 0x464 contains 8 entries:
Offset Info Type Sym.Value Sym. Name
00000042 00000902 R_386_PC32 00000000 sub_func
00000054 00000a02 R_386_PC32 00000000 sum_func
0000005d 00000a02 R_386_PC32 00000000 sum_func
0000007a 00000501 R_386_32 00000000 .rodata
0000007f 00000b02 R_386_PC32 00000000 printf
0000008d 00000c02 R_386_PC32 00000000 double_gv_inited
00000096 00000501 R_386_32 00000000 .rodata
0000009b 00000b02 R_386_PC32 00000000 printf
至此,ELF對(duì)象文件格式中的 linking view ,也就是上面組成圖的左邊部分,我們已經(jīng)介紹完畢。在這里最重要的概念是 section。在可重定位文件里面,section承載了大多數(shù)被包含的東西,代碼、數(shù)據(jù)、符號(hào)信息、重定位信息等等??芍囟ㄎ粚?duì)象文件里面的這些sections是作為輸入,給鏈接器那去做鏈接用的,所以這些 sections 也經(jīng)常被稱做輸入 section。
鏈接器在鏈接可執(zhí)行文件或動(dòng)態(tài)庫(kù)的過(guò)程中,它會(huì)把來(lái)自不同可重定位對(duì)象文件中的相同名稱的 section 合并起來(lái)構(gòu)成同名的 section。接著,它又會(huì)把帶有相同屬性(比方都是只讀并可加載的)的 section 都合并成所謂 segments(段)。segments 作為鏈接器的輸出,常被稱為輸出section。我們開(kāi)發(fā)者可以控制哪些不同.o文件的sections來(lái)最后合并構(gòu)成不同名稱的 segments。如何控制呢,就是通過(guò) linker script 來(lái)指定。關(guān)于鏈接器腳本,我們這里不予討論。
一個(gè)單獨(dú)的 segment 通常會(huì)包含幾個(gè)不同的 sections,比方一個(gè)可被加載的、只讀的segment 通常就會(huì)包括可執(zhí)行代碼section .text、只讀的數(shù)據(jù)section .rodata以及給動(dòng)態(tài)鏈接器使用的符號(hào)section .dymsym等等。section 是被鏈接器使用的,但是 segments 是被加載器所使用的。加載器會(huì)將所需要的 segment 加載到內(nèi)存空間中運(yùn)行。和用 sections header table 來(lái)指定一個(gè)可重定位文件中到底有哪些 sections 一樣。在一個(gè)可執(zhí)行文件或者動(dòng)態(tài)庫(kù)中,也需要有一種信息結(jié)構(gòu)來(lái)指出包含有哪些 segments。這種信息結(jié)構(gòu)就是 program header table,如ELF對(duì)象文件格式中右邊的 execute view 所示的那樣。
我們可以用 readelf -l 來(lái)查看可執(zhí)行文件的程序頭表,如下所示:
[yihect@juliantec test_2]$ readelf -l ./test
Elf file type is EXEC (Executable file)
Entry point 0x8048464
There are 7 program headers, starting at offset 52
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000034 0x08048034 0x08048034 0x000e0 0x000e0 R E 0x4
INTERP 0x000114 0x08048114 0x08048114 0x00013 0x00013 R 0x1
[Requesting program interpreter: /lib/ld-linux.so.2]
LOAD 0x000000 0x08048000 0x08048000 0x0073c 0x0073c R E 0x1000
LOAD 0x00073c 0x0804973c 0x0804973c 0x00110 0x00118 RW 0x1000
DYNAMIC 0x000750 0x08049750 0x08049750 0x000d0 0x000d0 RW 0x4
NOTE 0x000128 0x08048128 0x08048128 0x00020 0x00020 R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.ABI-tag .hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .eh_frame
03 .ctors .dtors .jcr .dynamic .got .got.plt .data .bss
04 .dynamic
05 .note.ABI-tag
06
結(jié)果顯示,在可執(zhí)行文件 ./test 中,總共有7個(gè) segments。同時(shí),該結(jié)果也很明白顯示出了哪些 section 映射到哪一個(gè) segment 當(dāng)中去。比方在索引為2的那個(gè)segment 中,總共有15個(gè) sections 映射進(jìn)來(lái),其中包括我們前面提到過(guò)的 .text section。注意這個(gè)segment 有兩個(gè)標(biāo)志: R 和 E。這個(gè)表示該segment是可讀的,也可執(zhí)行的。如果你看到標(biāo)志中有W,那表示該segment是可寫(xiě)的。
我們還是來(lái)解釋一下上面的結(jié)果,希望你能對(duì)照著TISCv1.2規(guī)范里面的文本來(lái)看,我這里也列出程序頭表?xiàng)l目的C結(jié)構(gòu):

上面類型為PHDR的segment,用來(lái)包含程序頭表本身。類型為INTERP的segment只包含一個(gè) section,那就是 .interp。在這個(gè)section中,包含了動(dòng)態(tài)鏈接過(guò)程中所使用的解釋器路徑和名稱。在Linux里面,這個(gè)解釋器實(shí)際上就是 /lib/ ,這可以通過(guò)下面的 hexdump 看出來(lái):[yihect@juliantec test_2]$ hexdump -s 0x114 -n 32 -C ./test
00000114 2f 6c 69 62 2f 6c 64 2d 6c 69 6e 75 78 2e 73 6f |/lib/ld-linux.so|
00000124 2e 32 00 00 04 00 00 00 10 00 00 00 01 00 00 00 |.2..............|
00000134
為什么會(huì)有這樣的一個(gè) segment?這是因?yàn)槲覀儗?xiě)的應(yīng)用程序通常都需要使用動(dòng)態(tài)鏈接庫(kù).so,就像 test 程序中所使用的 libsub.so 一樣。我們還是先大致說(shuō)說(shuō)程序在linux里面是怎么樣運(yùn)行起來(lái)的吧。當(dāng)你在 shell 中敲入一個(gè)命令要執(zhí)行時(shí),內(nèi)核會(huì)幫我們創(chuàng)建一個(gè)新的進(jìn)程,它在往這個(gè)新進(jìn)程的進(jìn)程空間里面加載進(jìn)可執(zhí)行程序的代碼段和數(shù)據(jù)段后,也會(huì)加載進(jìn)動(dòng)態(tài)連接器(在Linux里面通常就是 /lib/ld-linux.so 符號(hào)鏈接所指向的那個(gè)程序,它本省就是一個(gè)動(dòng)態(tài)庫(kù))的代碼段和數(shù)據(jù)。在這之后,內(nèi)核將控制傳遞給動(dòng)態(tài)鏈接庫(kù)里面的代碼。動(dòng)態(tài)連接器接下來(lái)負(fù)責(zé)加載該命令應(yīng)用程序所需要使用的各種動(dòng)態(tài)庫(kù)。加載完畢,動(dòng)態(tài)連接器才將控制傳遞給應(yīng)用程序的main函數(shù)。如此,你的應(yīng)用程序才得以運(yùn)行。
這里說(shuō)的只是大致的應(yīng)用程序啟動(dòng)運(yùn)行過(guò)程,更詳細(xì)的,我們會(huì)在后續(xù)的文章中繼續(xù)討論。我們說(shuō)link editor鏈接的應(yīng)用程序只是部分鏈接過(guò)的應(yīng)用程序。經(jīng)常的,在應(yīng)用程序中,會(huì)使用很多定義在動(dòng)態(tài)庫(kù)中的函數(shù)。最最基礎(chǔ)的比方C函數(shù)庫(kù)(其本身就是一個(gè)動(dòng)態(tài)庫(kù))中定義的函數(shù),每個(gè)應(yīng)用程序總要使用到,就像我們test程序中使用到的 printf 函數(shù)。為了使得應(yīng)用程序能夠正確使用動(dòng)態(tài)庫(kù),動(dòng)態(tài)連接器在加載動(dòng)態(tài)庫(kù)后,它還會(huì)做更進(jìn)一步的鏈接,這就是所謂的動(dòng)態(tài)鏈接。為了讓動(dòng)態(tài)連接器能成功的完成動(dòng)態(tài)鏈接過(guò)程,在前面運(yùn)行的link editor需要在應(yīng)用程序可執(zhí)行文件中生成數(shù)個(gè)特殊的 sections,比方 .dynamic、.dynsym、.got和.plt等等。這些內(nèi)容我們會(huì)在后面的文章中進(jìn)行討論。
我們先回到上面所輸出的文件頭表中。在接下來(lái)的數(shù)個(gè) segments 中,最重要的是三個(gè) segment:代碼段,數(shù)據(jù)段和堆棧段。代碼段和堆棧段的 VirtAddr 列的值分別為 0x08048000 和 0x0804973c。這是什么意思呢?這是說(shuō)對(duì)應(yīng)的段要加載在進(jìn)程虛擬地址空間中的起始地址。雖然在可執(zhí)行文件中規(guī)定了 text segment和 data segment 的起始地址,但是最終,在內(nèi)存中的這些段的真正起始地址,卻可能不是這樣的,因?yàn)樵趧?dòng)態(tài)鏈接器加載這些段的時(shí)候,需要考慮到頁(yè)面對(duì)齊的因素。為什么?因?yàn)橄駒86這樣的架構(gòu),它給內(nèi)存單元分配讀寫(xiě)權(quán)限的最小單位是頁(yè)(page)而不是字節(jié)。也就是說(shuō),它能規(guī)定從某個(gè)頁(yè)開(kāi)始、連續(xù)多少頁(yè)是只讀的。卻不能規(guī)定從某個(gè)頁(yè)內(nèi)的哪一個(gè)字節(jié)開(kāi)始,連續(xù)多少個(gè)字節(jié)是只讀的。因?yàn)閤86架構(gòu)中,一個(gè)page大小是4k,所以,動(dòng)態(tài)鏈接器在加載 segment 到虛擬內(nèi)存中的時(shí)候,其真實(shí)的起始地址的低12位都是零,也即以 0x1000 對(duì)齊。
我們先來(lái)看看一個(gè)真實(shí)的進(jìn)程中的內(nèi)存空間信息,拿我們的 test 程序作為例子。在 Linux 系統(tǒng)中,有一個(gè)特殊的由內(nèi)核實(shí)現(xiàn)的虛擬文件系統(tǒng) /proc。內(nèi)核實(shí)現(xiàn)這個(gè)文件系統(tǒng),并將它作為整個(gè)Linux系統(tǒng)面向外部世界的一個(gè)接口。我們可以通過(guò) /proc 觀察到一個(gè)正在運(yùn)行著的Linux系統(tǒng)的內(nèi)核數(shù)據(jù)信息以及各進(jìn)程相關(guān)的信息。所以我們?nèi)绻榭茨骋粋€(gè)進(jìn)程的內(nèi)存空間情況,也可以通過(guò)它來(lái)進(jìn)行。使用/proc唯一需要注意的是,由于我們的 test 程序很小,所以當(dāng)我們運(yùn)行起來(lái)之后,它很快就會(huì)結(jié)束掉,使得我們沒(méi)有時(shí)間去查看test的進(jìn)程信息。我們需要想辦法讓它繼續(xù)運(yùn)行,或者最起碼運(yùn)行直到讓我們能從 /proc 中獲取得到想要的信息后再結(jié)束。
我們有多種選擇。最簡(jiǎn)單的是,在 test main 程序中插入一個(gè)循環(huán),然后在循環(huán)中放入 sleep() 的調(diào)用,這樣當(dāng)程序運(yùn)行到這個(gè)循環(huán)的時(shí)候,就會(huì)進(jìn)入“運(yùn)行-睡眠-運(yùn)行-睡眠”循環(huán)中。這樣我們就有機(jī)會(huì)去看它的虛擬內(nèi)存空間信息。另外一個(gè)方法,是使用調(diào)試器,如GDB。我們?cè)O(shè)置一個(gè)斷點(diǎn),然后在調(diào)試過(guò)程中讓test進(jìn)程在這個(gè)斷點(diǎn)處暫停,這樣我們也有機(jī)會(huì)獲得地址空間的信息。我們這里就使用這種方法。當(dāng)然,為了能讓GDB調(diào)試我們的 test,我們得在編譯的時(shí)候加上"-g"選項(xiàng)。最后我們用下面的命令得到 test 程序?qū)?yīng)進(jìn)程的地址空間信息。
[yihect@juliantec ~]$ cat /proc/`pgrep test`/maps
00103000-00118000 r-xp 00000000 08:02 544337 /lib/ld-2.3.4.so
00118000-00119000 r--p 00015000 08:02 544337 /lib/ld-2.3.4.so
00119000-0011a000 rw-p 00016000 08:02 544337 /lib/ld-2.3.4.so
0011c000-00240000 r-xp 00000000 08:02 544338 /lib/tls/libc-2.3.4.so
00240000-00241000 r--p 00124000 08:02 544338 /lib/tls/libc-2.3.4.so
00241000-00244000 rw-p 00125000 08:02 544338 /lib/tls/libc-2.3.4.so
00244000-00246000 rw-p 00244000 00:00 0
00b50000-00b51000 r-xp 00000000 08:02 341824 /usr/lib/libsub.so
00b51000-00b52000 rw-p 00000000 08:02 341824 /usr/lib/libsub.so
08048000-08049000 r-xp 00000000 08:05 225162 /home/yihect/test_2/test
08049000-0804a000 rw-p 00000000 08:05 225162 /home/yihect/test_2/test
b7feb000-b7fed000 rw-p b7feb000 00:00 0
b7fff000-b8000000 rw-p b7fff000 00:00 0
bff4c000-c0000000 rw-p bff4c000 00:00 0
ffffe000-fffff000 ---p 00000000 00:00 0
注意,上面命令中的pgre test 是用`括起來(lái)的,它不是單引號(hào),而是鍵盤(pán)上 Esc 字符下面的那個(gè)字符。從這個(gè)結(jié)果上可以看出,所有的段,其起始地址和結(jié)束地址(前面兩列)都是0x1000對(duì)齊的。結(jié)果中也列出了對(duì)應(yīng)的段是從哪里引過(guò)來(lái)的,比方動(dòng)態(tài)鏈接器/lib/ld-2.3.4.so、C函數(shù)庫(kù)和test程序本身。注意看test程序引入的代碼段起始地址是 0x08048000,這和我們 ELF 文件中指定的相同,但是結(jié)束地址卻是0x08049000,和文件中指定的不一致(0x08048000+0x0073c=0x0804873c)。這里,其實(shí)加載器也把數(shù)據(jù)segment中開(kāi)頭一部分也映射進(jìn)了 text segment 中去;同樣的,進(jìn)程虛擬內(nèi)存空間中的 data segment 從 08049000 開(kāi)始,而可執(zhí)行文件中指定的是從 0x0804973c 開(kāi)始。所以加載器也把代碼segment中末尾一部分也映射進(jìn)了 data segment 中去了。
從程序頭表中我們可以看到一個(gè)類型為 GNU_STACK 的segment,這是 stack segment。程序頭表中的這一項(xiàng),除了 Flg/Align 兩列不為空外, 其他列都為0。這是因?yàn)槎褩6卧谔摂M內(nèi)存空間中,從哪里開(kāi)始、占多少字節(jié)是由內(nèi)核說(shuō)了算的,而不決定于可執(zhí)行程序。實(shí)際上,內(nèi)核決定把堆棧段放在整個(gè)進(jìn)程地址空間的用戶空間的最上面,所以堆棧段的末尾地址就是 0xc0000000。別忘記在 x86 中,堆棧是從高向低生長(zhǎng)的。
好,為了方便你對(duì)后續(xù)文章的理解,我們?cè)谶@里討論一種比較簡(jiǎn)單的重定位類型 R_386_PC32。前面我們說(shuō)過(guò)重定義的含義,也即在連接階段,根據(jù)某種計(jì)算方式計(jì)算出一個(gè)新的值(通常是地址),然后將這個(gè)值重新改寫(xiě)到對(duì)象文件或者內(nèi)存映像中某個(gè)section中的某個(gè)地址單元中去的這樣一個(gè)過(guò)程。那所謂重定位類型,就規(guī)定了使用何種方式,去計(jì)算這個(gè)值。既然是計(jì)算,那就肯定需要涉及到所要納入計(jì)算的變量。實(shí)際上,具體有哪些變量參與計(jì)算如同如何進(jìn)行計(jì)算一樣也是不固定的,各種重定位類型有自己的規(guī)定。
根據(jù)規(guī)范里面的規(guī)定,重定位類型 R_386_PC32 的計(jì)算需要有三個(gè)變量參與:S,A和P。其計(jì)算方式是 S+A-P。根據(jù)規(guī)范,當(dāng)R_386_PC32類型的重定位發(fā)生在 link editor 鏈接若干個(gè) .o 對(duì)象文件從而形成可執(zhí)行文件的過(guò)程中的時(shí)候,變量S指代的是被重定位的符號(hào)的實(shí)際運(yùn)行時(shí)地址,而變量P是重定位所影響到的地址單元的實(shí)際運(yùn)行時(shí)地址。在運(yùn)行于x86架構(gòu)上的Linux系統(tǒng)中,這兩個(gè)地址都是虛擬地址。變量A最簡(jiǎn)單,就是重定位所需要的附加數(shù),它是一個(gè)常數(shù)。別忘了x86架構(gòu)所使用的重定位條目結(jié)構(gòu)體類型是 Elf32_Rela,所以附加數(shù)就存在于受重定位影響的地址單元中。重定位最后將計(jì)算得到的值patch到這個(gè)地址單元中。
或許,咱們舉一個(gè)實(shí)際例子來(lái)闡述可能對(duì)你更有用。在我們的 test 程序中,test.c 的 main 函數(shù)中需要調(diào)用定義在 sum.o 中的 sum_func 函數(shù),所以link editor 在將 test.o/sum.o 聯(lián)結(jié)成可執(zhí)行文件 test 的時(shí)候,必須處理一個(gè)重定位,這個(gè)重定位就是 R_386_PC32 類型的。我們先用 objdump 來(lái)查看 test.o 中的 .text section 內(nèi)容(我只選取了前面一部分):[yihect@juliantec test_2]$ objdump -d -j .text ./test.o
./test.o: file format elf32-i386
Disassembly of section .text:
00000000 <main />:
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 83 ec 18 sub $0x18,%esp
6: 83 e4 f0 and $0xfffffff0,%esp
9: b8 00 00 00 00 mov $0x0,%eax
e: 83 c0 0f add $0xf,%eax
11: 83 c0 0f add $0xf,%eax
14: c1 e8 04 shr $0x4,%eax
17: c1 e0 04 shl $0x4,%eax
1a: 29 c4 sub %eax,%esp
1c: c7 45 fc 0a 00 00 00 movl $0xa,0xfffffffc(%ebp)
23: c7 45 f8 2d 00 00 00 movl $0x2d,0xfffffff8(%ebp)
2a: c7 45 f4 03 00 00 00 movl $0x3,0xfffffff4(%ebp)
31: c7 45 f0 48 00 00 00 movl $0x48,0xfffffff0(%ebp)
38: 83 ec 08 sub $0x8,%esp
3b: ff 75 f0 pushl 0xfffffff0(%ebp)
3e: ff 75 f4 pushl 0xfffffff4(%ebp)
41: e8 fc ff ff ff call 42
46: 83 c4 08 add $0x8,%esp
49: 50 push %eax
4a: 83 ec 0c sub $0xc,%esp
4d: ff 75 f8 pushl 0xfffffff8(%ebp)
50: ff 75 fc pushl 0xfffffffc(%ebp)
53: e8 fc ff ff ff call 54
58: 83 c4 14 add $0x14,%esp
......
如結(jié)果所示,在離開(kāi) .text section 開(kāi)始 0x53 字節(jié)的地方,有一條call指令。這條指令是對(duì) sum_func 函數(shù)的調(diào)用,objdump 將其反匯編成 call 54,這是因?yàn)槠?0x54 字節(jié)的地方原本應(yīng)該放著 sum_func 函數(shù)的地址,但現(xiàn)在因?yàn)?sum_func 定義在 sum.o 中,所以這個(gè)地方就是重定位需要做 patch 的地址單元所在處。我們注意到,這個(gè)地址單元的值為 0xfffffffc,也就是十進(jìn)制的 -4(計(jì)算機(jī)中數(shù)是用補(bǔ)碼表示的)。所以,參與重定位運(yùn)算的變量A就確定了,即是 -4。
我們?cè)?test.o 中找出影響該地址單元的重定位記錄如下:
[yihect@juliantec test_2]$ readelf -r ./test.o | grep 54
00000054 00000a02 R_386_PC32 00000000 sum_func
果然,如你所見(jiàn),該條重定位記錄是 R_386_PC32 類型的。前面變量A確定了,那么另外兩個(gè)變量S和變量P呢?從正向去計(jì)算這兩個(gè)變量的值比較麻煩。盡管我們知道,在Linux里面,鏈接可執(zhí)行程序時(shí)所使用的默認(rèn)的鏈接器腳本將最后可執(zhí)行程序的 .text segment 起始地址設(shè)置在 0x08048000的位置。但是,從這個(gè)地址出發(fā),去尋找符號(hào)(函數(shù))sub_func 和 上面受重定位影響的地址單元的運(yùn)行時(shí)地址的話,需要經(jīng)過(guò)很多人工計(jì)算,所以比較麻煩。
相反的,我們使用objdump工具像下面這樣分析最終鏈接生成的可執(zhí)行程序 ./test 的 .text segment 段,看看函數(shù) sum_func 和 那個(gè)受影響單元的運(yùn)行時(shí)地址到底是多少,這是反向的查看鏈接器的鏈接結(jié)果。鏈接器在鏈接的過(guò)程中是正向的將正確的地址分配給它們的。
[yihect@juliantec test_2]$ objdump -d -j .text ./test
./test: file format elf32-i386
Disassembly of section .text:
08048498 :
8048498: 31 ed xor %ebp,%ebp
......
08048540 <main />:
......
804858a: 83 ec 0c sub $0xc,%esp
804858d: ff 75 f8 pushl 0xfffffff8(%ebp)
8048590: ff 75 fc pushl 0xfffffffc(%ebp)
8048593: e8 74 00 00 00 call 804860c
8048598: 83 c4 14 add $0x14,%esp
804859b: 50 push %eax
......
0804860c :
804860c: 55 push %ebp
804860d: 89 e5 mov %esp,%ebp
804860f: 8b 45 0c mov 0xc(%ebp),%eax
8048612: 03 45 08 add 0x8(%ebp),%
8048615: c9 leave
8048616: c3 ret
8048617: 90 nop
......
從中很容易的就可以看出,鏈接器給函數(shù) sum_func 分配的運(yùn)行時(shí)地址是 0x0804860c,所以變量S的值就是 0x0804860c。那么變量P呢?它表示的是重定位所影響地址單元的運(yùn)行地址。如果要計(jì)算這個(gè)地址,我們可以先看看 main 函數(shù)的運(yùn)行時(shí)地址,再加上0x54字節(jié)的偏移來(lái)得到。從上面看出 main 函數(shù)的運(yùn)行時(shí)地址為 0x08048540,所以重定位所影響地址單元的運(yùn)行時(shí)地址為 0x08048540+0x54 = 0x08048594。所以重定位計(jì)算的最終結(jié)果為:
S+A-P = 0x0804860c+(-4)-0x08048594 = 0x00000074
從上面可以看出,鏈接器在鏈接過(guò)程中,確實(shí)也把這個(gè)計(jì)算得到的結(jié)果存儲(chǔ)到了上面 call 指令操作數(shù)所在的地址單元中去了。那么,程序在運(yùn)行時(shí),是如何憑借這樣一條帶有如此操作數(shù)的 call 指令來(lái)調(diào)用到(或者跳轉(zhuǎn)到)函數(shù) sum_func 中去的呢?
你看,調(diào)用者 main 和被調(diào)用者 sum_func 處在同一個(gè)text segment中。根據(jù)x86架構(gòu)或者IBM兼容機(jī)的匯編習(xí)慣,段內(nèi)轉(zhuǎn)移或者段內(nèi)跳轉(zhuǎn)時(shí)使用的尋址方式是PC相對(duì)尋址。也就是若要讓程序從一個(gè)段內(nèi)的A處,跳轉(zhuǎn)到同一段內(nèi)的B處,那么PC相對(duì)尋址會(huì)取程序在A處執(zhí)行時(shí)的PC值,再加上某一個(gè)偏移值(offset),得到要跳轉(zhuǎn)的目標(biāo)地址(B處地址)。那么,對(duì)于x86架構(gòu)來(lái)說(shuō),由于有規(guī)定,PC總是指向下一條要執(zhí)行的指令,那么當(dāng)程序執(zhí)行在call指令的時(shí)候,PC指向的是下一條add指令,其值也就是 0x8048598。最后,尋址的時(shí)候再加上call指令的操作數(shù)0x74作為偏移,計(jì)算最終的 sum_func 函數(shù)目標(biāo)地址為 0x8048598+0x74 = 0x804860c。
有點(diǎn)意思吧:),如果能繞出來(lái),那說(shuō)明我們是真的明白了,其實(shí),繞的過(guò)程本身就充滿著趣味性,就看你自己的心態(tài)了。說(shuō)到這里,本文行將結(jié)束。本文所介紹的很多內(nèi)容,可能在某些同學(xué)眼中會(huì)過(guò)于簡(jiǎn)單,但是為了體現(xiàn)知識(shí)的完整性、同時(shí)也為了讓大家先有個(gè)基礎(chǔ)以便更容易的看后續(xù)的文章,我們還是在這里介紹一下ELF格式的基礎(chǔ)知識(shí)。下面一篇關(guān)于ELF主題的文章,將詳細(xì)介紹動(dòng)態(tài)連接的內(nèi)在實(shí)現(xiàn)。屆時(shí),你將看到大量的實(shí)際代碼挖掘。