轉(zhuǎn)自:https://www.cnblogs.com/xmphoenix/archive/2011/10/23/2221879.html
ELF(Executable and Linking Format)是一種對象文件的格式,用于定義不同類型的對象文件(Object files)中都放了什么東西杂抽、以及都以什么樣的格式去放這些東西檀何。它自最早在 System V 系統(tǒng)上出現(xiàn)后刀疙,被 xNIX 世界所廣泛接受规个,作為缺省的二進(jìn)制文件格式來使用钻趋≡迕可以說铐尚,ELF是構(gòu)成眾多xNIX系統(tǒng)的基礎(chǔ)之一,所以作為嵌入式Linux系統(tǒng)乃至內(nèi)核驅(qū)動程序開發(fā)人員或衡,你最好熟悉并掌握它焦影。
其實,關(guān)于ELF這個主題封断,網(wǎng)絡(luò)上已經(jīng)有相當(dāng)多的文章存在斯辰,但是其介紹的內(nèi)容比較分散,使得初學(xué)者不太容易從中得到一個系統(tǒng)性的認(rèn)識坡疼。為了幫助大家學(xué)習(xí)椒涯,我這里打算寫一系列連貫的文章來介紹ELF以及相關(guān)的應(yīng)用。這是這個系列中的第一篇文章,主要是通過不同工具的使用來熟悉ELF文件的內(nèi)部結(jié)構(gòu)以及相關(guān)的基本概念废岂。后面的文章祖搓,我們會介紹很多高級的概念和應(yīng)用,比方動態(tài)鏈接和加載湖苞,動態(tài)庫的開發(fā)拯欧,C語言Main函數(shù)是被誰以及如何被調(diào)用的,ELF格式在內(nèi)核中的支持财骨,Linux內(nèi)核中對ELF section的擴(kuò)展使用等等镐作。
好的,開始我們的第一篇文章隆箩。在詳細(xì)進(jìn)入正題之前该贾,先給大家介紹一點(diǎn)ELF文件格式的參考資料。在ELF格式出來之后捌臊,TISC(Tool Interface Standard Committee)委員會定義了一套ELF標(biāo)準(zhǔn)杨蛋。你可以從這里(http://refspecs.freestandards.org/elf/)找到詳細(xì)的標(biāo)準(zhǔn)文檔。TISC委員會前后出了兩個版本理澎,v1.1和v1.2逞力。兩個版本內(nèi)容上差不多,但就可讀性上來講糠爬,我還是推薦你讀 v1.2的寇荧。因為在v1.2版本中,TISC重新組織原本在v1.1版本中的內(nèi)容执隧,將它們分成為三個部分(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 作為例子介紹
值得一說的是镀琉,雖然TISC是以x86為例子介紹ELF規(guī)范的峦嗤,但是如果你是想知道非x86下面的ELF實現(xiàn)情況,那也可以在http://refspecs.freestandards.org/elf/中找到特定處理器相關(guān)的Supplment文檔滚粟。比方ARM相關(guān)的寻仗,或者M(jìn)IPS相關(guān)的等等刃泌。另外凡壤,相比較UNIX系統(tǒng)的另外一個分支BSD Unix,Linux系統(tǒng)更靠近 System V 系統(tǒng)耙替。所以關(guān)于操作系統(tǒng)特定的ELF內(nèi)容亚侠,你可以直接參考v1.2標(biāo)準(zhǔn)中的內(nèi)容。
這里多說些廢話:別忘了 Linus 在實現(xiàn)Linux的第一個版本的時候俗扇,就是看了介紹Unix內(nèi)部細(xì)節(jié)的書:《The of the Unix Operating System》硝烂,得到很多啟發(fā)。這本書對應(yīng)的操作系統(tǒng)是System V 的第二個Release铜幽。這本書介紹了操作系統(tǒng)的很多設(shè)計觀念滞谢,并且行文簡單易懂串稀。所以雖然現(xiàn)在的Linux也吸取了其他很多Unix變種的設(shè)計理念,但是如果你想研究學(xué)習(xí)Linux內(nèi)核狮杨,那還是以看這本書作為開始為好母截。這本書也是我在接觸Linux內(nèi)核之前所看的第一本介紹操作系統(tǒng)的書,所以我極力向大家推薦橄教。(在學(xué)校雖然學(xué)過操作系統(tǒng)原理清寇,但學(xué)的也是很糟糕最后導(dǎo)致期末考試才四十來分,記憶仿佛還在昨天:))
好了护蝶,還是回來開始我們第一篇ELF主題相關(guān)的文章吧华烟。這篇文章主要是通過使用不同的工具來分析對象文件,來使你掌握ELF文件的基本格式持灰,以及了解相關(guān)的基本概念盔夜。你在讀這篇文章的時候,希望你在電腦上已經(jīng)打開了那個 v1.2 版本的ELF規(guī)范搅方,并對照著文章內(nèi)容看規(guī)范里的文字比吭。
首先,你需要知道的是所謂對象文件(Object files)有三個種類:
- 可重定位的對象文件(Relocatable file)
這是由匯編器匯編生成的 .o 文件姨涡。后面的鏈接器(link editor)拿一個或一些 Relocatable object files 作為輸入衩藤,經(jīng)鏈接處理后,生成一個可執(zhí)行的對象文件 (Executable file) 或者一個可被共享的對象文件(Shared object file)涛漂。我們可以使用 ar 工具將眾多的 .o Relocatable object files 歸檔(archive)成 .a 靜態(tài)庫文件赏表。如何產(chǎn)生 Relocatable file,你應(yīng)該很熟悉了匈仗,請參見我們相關(guān)的基本概念文章和JulWiki瓢剿。另外,可以預(yù)先告訴大家的是我們的內(nèi)核可加載模塊 .ko 文件也是 Relocatable object file悠轩。
- 可執(zhí)行的對象文件(Executable file)
這我們見的多了间狂。文本編輯器vi、調(diào)式用的工具gdb火架、播放mp3歌曲的軟件mplayer等等都是Executable object file鉴象。你應(yīng)該已經(jīng)知道,在我們的 Linux 系統(tǒng)里面何鸡,存在兩種可執(zhí)行的東西纺弊。除了這里說的 Executable object file,另外一種就是可執(zhí)行的腳本(如shell腳本)骡男。注意這些腳本不是 Executable object file淆游,它們只是文本文件,但是執(zhí)行這些腳本所用的解釋器就是 Executable object file,比如 bash shell 程序犹菱。
- 可被共享的對象文件(Shared object file)
這些就是所謂的動態(tài)庫文件拾稳,也即 .so 文件。如果拿前面的靜態(tài)庫來生成可執(zhí)行程序腊脱,那每個生成的可執(zhí)行程序中都會有一份庫代碼的拷貝熊赖。如果在磁盤中存儲這些可執(zhí)行程序,那就會占用額外的磁盤空間虑椎;另外如果拿它們放到Linux系統(tǒng)上一起運(yùn)行震鹉,也會浪費(fèi)掉寶貴的物理內(nèi)存。如果將靜態(tài)庫換成動態(tài)庫捆姜,那么這些問題都不會出現(xiàn)传趾。動態(tài)庫在發(fā)揮作用的過程中,必須經(jīng)過兩個步驟:
a) 鏈接編輯器(link editor)拿它和其他Relocatable object file以及其他shared object file作為輸入泥技,經(jīng)鏈接處理后浆兰,生存另外的 shared object file 或者 executable file。
b) 在運(yùn)行時珊豹,動態(tài)鏈接器(dynamic linker)拿它和一個Executable file以及另外一些 Shared object file 來一起處理簸呈,在Linux系統(tǒng)里面創(chuàng)建一個進(jìn)程映像。
以上所提到的 link editor 以及 dynamic linker 是什么東西店茶,你可以參考我們基本概念中的相關(guān)文章蜕便。對于什么是編譯器,匯編器等你應(yīng)該也已經(jīng)知道贩幻,在這里只是使用他們而不再對他們進(jìn)行詳細(xì)介紹轿腺。為了下面的敘述方便,你可以下載test.tar.gz包丛楚,解壓縮后使用"make"進(jìn)行編譯族壳。編譯完成后,會在目錄中生成一系列的ELF對象文件趣些,更多描述見里面的 README 文件仿荆。我們下面的論述都基于這些產(chǎn)生的對象文件。
make所產(chǎn)生的文件坏平,包括 sub.o/sum.o/test.o/libsub.so/test 等等都是ELF對象文件拢操。至于要知道它們都屬于上面三類中的哪一種,我們可以使用 file 命令來查看:
[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é)果很清楚的告訴我們他們都屬于哪一個類別功茴。比方 sum.o 是應(yīng)用在x86架構(gòu)上的可重定位文件庐冯。這個結(jié)果也間接的告訴我們孽亲,x86是小端模式(LSB)的32位結(jié)構(gòu)坎穿。那對于 file 命令來說,它又能如何知道這些信息?答案是在ELF對象文件的最前面有一個ELF文件頭玲昧,里面記載了所適用的處理器栖茉、對象文件類型等各種信息。在TISCv1.2的規(guī)范中孵延,用下面的圖描述了ELF對象文件的基本組成吕漂,其中ELF文件頭赫然在目。
[圖片上傳失敗...(image-f47526-1587041043467)]
等等尘应,為什么會有左右兩個很類似的圖來說明ELF的組成格式惶凝?這是因為ELF格式需要使用在兩種場合:
a) 組成不同的可重定位文件,以參與可執(zhí)行文件或者可被共享的對象文件的鏈接構(gòu)建犬钢;
b) 組成可執(zhí)行文件或者可被共享的對象文件苍鲜,以在運(yùn)行時內(nèi)存中進(jìn)程映像的構(gòu)建。
所以玷犹,基本上混滔,圖中左邊的部分表示的是可重定位對象文件的格式;而右邊部分表示的則是可執(zhí)行文件以及可被共享的對象文件的格式歹颓。正如TISCv1.2規(guī)范中所闡述的那樣坯屿,ELF文件頭被固定地放在不同類對象文件的最前面。至于它里面的內(nèi)容巍扛,除了file命令所顯示出來的那些之外领跛,更重要的是包含另外一些數(shù)據(jù),用于描述ELF文件中ELF文件頭之外的內(nèi)容撤奸。如果你的系統(tǒng)中安裝有 GNU binutils 包隔节,那我們可以使用其中的 readelf 工具來讀出整個ELF文件頭的內(nèi)容,比如:
<pre style="margin-top: 0px; margin-bottom: 0px; white-space: pre-wrap; overflow-wrap: break-word; color: rgb(0, 0, 0); font-size: 13px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">[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
</pre>
這個輸出結(jié)果能反映出很多東西寂呛。那如何來看這個結(jié)果中的內(nèi)容怎诫,我們還是就著TISCv1.2規(guī)范來。在實際寫代碼支持ELF格式對象文件格式的時候贷痪,我們都會定義許多C語言的結(jié)構(gòu)來表示ELF格式的各個相關(guān)內(nèi)容幻妓,比方這里的ELF文件頭,你就可以在TISCv1.2規(guī)范中找到這樣的結(jié)構(gòu)定義(注意我們研究的是針對x86架構(gòu)的ELF劫拢,所以我們只考慮32位版本肉津,而不考慮其他如64位之類的):
[圖片上傳失敗...(image-3b536-1587041043464)]
這個結(jié)構(gòu)里面出現(xiàn)了多種數(shù)據(jù)類型,同樣可以在規(guī)范中找到相關(guān)說明:
[圖片上傳失敗...(image-996900-1587041043464)]
在我們以后一系列文章中舱沧,我們會著重拿實際的程序代碼來分析妹沙,介時你會在頭文件中找到同樣的定義。但是這里熟吏,我們只討論規(guī)范中的定義距糖,暫不考慮任何程序代碼玄窝。在ELF頭中,字段e_machine和e_type指明了這是針對x86架構(gòu)的可重定位文件悍引,最前面有個長度為16字節(jié)的字段中有一個字節(jié)表示了它適用于32bits機(jī)器恩脂,而不是64位的。除了這些之外趣斤,另外ELF頭還告訴了我們其他一些特別重要的信息俩块,分別是:
a) 這個sum.o的進(jìn)入點(diǎn)是0x0(e_entry),這表面Relocatable objects不會有程序進(jìn)入點(diǎn)浓领。所謂程序進(jìn)入點(diǎn)是指當(dāng)程序真正執(zhí)行起來的時候玉凯,其第一條要運(yùn)行的指令的運(yùn)行時地址。因為Relocatable objects file只是供再鏈接而已联贩,所以它不存在進(jìn)入點(diǎn)壮啊。而可執(zhí)行文件test和動態(tài)庫.so都存在所謂的進(jìn)入點(diǎn),你可以用 readelf -h 看看撑蒜。后面我們的文章中會介紹可執(zhí)行文件的e_entry指向C庫中的_start歹啼,而動態(tài)庫.so中的進(jìn)入點(diǎn)指向 call_gmon_start。這些后面再說座菠,這里先不深入討論狸眼。
b) 這個sum.o文件包含有9個sections,但卻沒有segments(Number of program headers為0)浴滴。
那什么是所謂 sections 呢拓萌?可以說,sections 是在ELF文件里頭升略,用以裝載內(nèi)容數(shù)據(jù)的最小容器微王。在ELF文件里面,每一個 sections 內(nèi)都裝載了性質(zhì)屬性都一樣的內(nèi)容品嚣,比方:
.text section 里裝載了可執(zhí)行代碼炕倘;
.data section 里面裝載了被初始化的數(shù)據(jù);
.bss section 里面裝載了未被初始化的數(shù)據(jù)翰撑;
以 .rec 打頭的 sections 里面裝載了重定位條目罩旋;
.symtab 或者 .dynsym section 里面裝載了符號信息;
.strtab 或者 .dynstr section 里面裝載了字符串信息眶诈;
其他還有為滿足不同目的所設(shè)置的section涨醋,比方滿足調(diào)試的目的、滿足動態(tài)鏈接與加載的目的等等逝撬。
一個ELF文件中到底有哪些具體的 sections浴骂,由包含在這個ELF文件中的 section head table(SHT)決定。在SHT中宪潮,針對每一個section溯警,都設(shè)置有一個條目趣苏,用來描述對應(yīng)的這個section,其內(nèi)容主要包括該 section 的名稱愧膀、類型、大小以及在整個ELF文件中的字節(jié)偏移位置等等谣光。我們也可以在TISCv1.2規(guī)范中找到SHT表中條目的C結(jié)構(gòu)定義:
[圖片上傳失敗...(image-8c64c4-1587041043464)]
我們可以像下面那樣來使用 readelf 工具來查看可重定位對象文件 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)
這個結(jié)果顯示了 sum.o 中包含的所有9個sections檩淋。因為sum.o僅僅是參與link editor鏈接的可重定位文件,而不參與最后進(jìn)程映像的構(gòu)建萄金,所以Addr(sh_addr)為0蟀悦。后面你會看到可執(zhí)行文件以及動態(tài)庫文件中大部分sections的這一字段都是有某些取值的。Off(sh_offset)表示了該section離開文件頭部位置的距離氧敢。Size(sh_size)表示section的字節(jié)大小日戈。ES(sh_entsize)只對某些形式的sections 有意義。比方符號表 .symtab section孙乖,其內(nèi)部包含了一個表格浙炼,表格的每一個條目都是特定長度的,那這里的這個字段就表示條目的長度10唯袄。Al(sh_addralign)是地址對齊要求弯屈。另外剩下的兩列Lk和Inf,對應(yīng)著條目結(jié)構(gòu)中的字段sh_link和字段sh_info恋拷。它們中記錄的是section head table 中的條目索引资厉,這就意味著,從這兩個字段出發(fā)蔬顾,可以找到對應(yīng)的另外兩個 section宴偿,其具體的含義解釋依據(jù)不同種類的 section 而不同,后面會介紹诀豁。
注意上面結(jié)果中的 Flg 窄刘,表示的是對應(yīng)section的相關(guān)標(biāo)志。比方.text section 里面存儲的是代碼舷胜,所以就是只讀的(X)都哭;.data和.bss里面存放的都是可寫的(W)數(shù)據(jù)(非在堆棧中定義的數(shù)據(jù)),只不過前者存的是初始化過的數(shù)據(jù)逞带,比方程序中定義的賦過初值的全局變量等欺矫;而后者里面存儲的是未經(jīng)過初始化的數(shù)據(jù)。因為未經(jīng)過初始化就意味著不確定這些數(shù)據(jù)剛開始的時候會有些什么樣的值展氓,所以針對對象文件來說穆趴,它就沒必要為了存儲這些數(shù)據(jù)而在文件內(nèi)多留出一塊空間,因此.bss section的大小總是為0遇汞。后面會看到未妹,當(dāng)可執(zhí)行程序被執(zhí)行的時候簿废,動態(tài)連接器會在內(nèi)存中開辟一定大小的空間來存放這些未初始化的數(shù)據(jù),里面的內(nèi)存單元都被初始化成0络它∽迕剩可執(zhí)行程序文件中雖然沒有長度非0的 .bss section,但卻記錄有在程序運(yùn)行時化戳,需要開辟多大的空間來容納這些未初始化的數(shù)據(jù)单料。
另外一個標(biāo)志A說明對應(yīng)的 section 是Allocable的。所謂 Allocable 的section点楼,是指在運(yùn)行時扫尖,進(jìn)程(process)需要使用它們,所以它們被加載器加載到內(nèi)存中去掠廓。
而與此相反换怖,存在一些non-Allocable 的sections,它們只是被鏈接器蟀瞧、調(diào)試器或者其他類似工具所使用的沉颂,而并非參與進(jìn)程的運(yùn)行中去的那些 section。比方后面要介紹的字符串表section .strtab悦污,符號表 .symtab section等等兆览。當(dāng)運(yùn)行最后的可執(zhí)行程序時,加載器會加載那些 Allocable 的部分塞关,而 non-Allocable 的部分則會被繼續(xù)留在可執(zhí)行文件內(nèi)抬探。所以,實際上帆赢,這些 non-Allocable 的section 都可以被我們用 stip 工具從最后的可執(zhí)行文件中刪除掉小压,刪除掉這些sections的可執(zhí)行文件照樣能夠運(yùn)行,只不過你沒辦法來進(jìn)行調(diào)試之類的事情罷了椰于。
我們?nèi)匀豢梢允褂?readelf -x SecNum 來傾印出不同 section 中的內(nèi)容怠益。但是,無奈其輸出結(jié)果都是機(jī)器碼瘾婿,對我們?nèi)藖碚f不具備可讀性蜻牢。所以我們換用 binutils 包中的另外一個工具 objdump 來看看這些 sections 中到底具有哪些內(nè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 的選項 -d 表示要對由 -j 選擇項指定的 section 內(nèi)容進(jìn)行反匯編偏陪,也就是由機(jī)器碼出發(fā),推導(dǎo)出相應(yīng)的匯編指令笛谦。上面結(jié)果顯示在 sum.o 對象文件的 .text 中只是包含了函數(shù) sum_func 的定義。用同樣的方法恳邀,我們來看看 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 ....
這個結(jié)果顯示在 sum.o 的 .data section 中定義了一個四字節(jié)的變量 gv_inited,其值被初始化成 0x00000017刷钢,也就是十進(jìn)制值 23乳附。別忘了,x86架構(gòu)是使用小端模式的许溅。
我們接下來來看看字符串表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)容中可以找到贤重。盡管這個命令的輸出結(jié)果不是那么具有可讀性,但我們還是得來說一說如何看這個結(jié)果清焕,因為后續(xù)文章中將會使用大量的這種命令并蝗。上面結(jié)果中的十六進(jìn)制數(shù)據(jù)部分從右到左看是地址遞增的方向,而字符內(nèi)容部分從左到右看是地址遞增的方向秸妥。所以滚停,在 .strtab section 中,按照地址遞增的方向來看粥惧,各字節(jié)的內(nèi)容依次是 0x00键畴、0x73、0x75突雪、0x6d起惕、0x2e ....,也就是字符 咏删、's'惹想、'u'、'm'督函、'.' ... 等嘀粱。如果還是看不太明白,你可以使用 hexdump 直接dumping出 .strtab section 開頭(其偏移在文件內(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 中存儲著的都是以字符
為分割符的字符串锋叨,這些字符串所表示的內(nèi)容,通常是程序中定義的函數(shù)名稱豌鸡、所定義過的變量名稱等等炉奴。。砸逊。當(dāng)對象文件中其他地方需要和一個這樣的字符串相關(guān)聯(lián)的時候师逸,往往會在對應(yīng)的地方存儲 .strtab section 中的索引值。比方下面將要介紹的符號表 .symtab section 中员辩,有一個條目是用來描述符號 gv_inited 的奠滑,那么在該條目中就會有一個字段(st_name)記錄著字符串 gv_inited 在 .strtab section 中的索引 7 。 .shstrtab 也是字符串表弃甥,只不過其中存儲的是 section 的名字,而非所函數(shù)或者變量的名稱瓶珊。
字符串表在真正鏈接和生成進(jìn)程映像過程中是不需要使用的,但是其對我們調(diào)試程序來說就特別有幫助唱较,因為我們?nèi)丝雌饋碜钍娣倪€是自然形式的字符串胸遇,而非像天書一樣的數(shù)字符號。前面使用objdump來反匯編 .text section 的時候概疆,之所以能看到定義了函數(shù) sum_func 凯旭,那也是因為存在這個字符串表的原因咐蚯。當(dāng)然起關(guān)鍵作用的矫膨,還是符號表 .symtab section 在其中作為中介,下面我們就來看看符號表馁痴。
雖然我們同樣可以使用 readelf -x 來查看符號表(.symtab)section的內(nèi)容,但是其結(jié)果可讀性太差小渊,我們換用 readelf -s 或者 objdump -t 來查看(前者輸出結(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
在符號表內(nèi)針對每一個符號,都會相應(yīng)的設(shè)置一個條目呐萨。在繼續(xù)介紹上面的結(jié)果之前凹髓,我們還是從規(guī)范中找出符號表內(nèi)條目的C結(jié)構(gòu)定義:
[圖片上傳失敗...(image-24235b-1587041043464)]
上面結(jié)果中 Type 列顯示出符號的種類。Bind 列定義了符號的綁定類型赌躺。種類和綁定類型合并在一起,由結(jié)構(gòu)中 st_info 字段來定義缅叠。在ELF格式中,符號類型總共可以有這么幾種:
[圖片上傳失敗...(image-94a63a-1587041043464)]
類型 STT_OBJECT 表示和該符號對應(yīng)的是一個數(shù)據(jù)對象,比方程序中定義過的變量庶骄、數(shù)組等,比方上面的 gv_inited 和 ch羔飞;類型 STT_FUNC 表示該符號對應(yīng)的是函數(shù)喇聊,比方上面的 sum_func函數(shù)朋贬。類型 STT_SECTION 表示該符號和一個 section 相關(guān)摆屯,這種符號用于重定位。關(guān)于重定位廷没,我們下文會介紹。
符號的綁定類型表示了這個符號的可見性狭归,是僅本對象文件可見呢,還是全局可見潭流。它的取值主要有三種:STB_LOCA嗓奢、STB_GLOBAL和STB_WEAK,具體的內(nèi)容還請參見規(guī)范。關(guān)于符號,最重要的就是符號的值(st_value)了震嫉。依據(jù)對象文件的不同類型森瘪,符號的值所表示的含義也略有差異:
a) 在可重定位文件中,如果該符號對應(yīng)的section index(上面的Ndx)為SHN_COMMON票堵,那么符號的值表示的是該數(shù)據(jù)的對齊要求扼睬,比方上面的變量 ch 。
b) 在可重定位文件中悴势,除去上面那條a中定義的符號窗宇,對于其他的符號來說蝇完,其值表示的是對應(yīng) section 內(nèi)的偏移值。比方 gv_inited 變量定義在 .data section 的最前面扇雕,所以其值為0币砂。
c) 在可執(zhí)行文件或者動態(tài)庫中,符號的值表示的是運(yùn)行時的內(nèi)存地址。
好叛薯,咱們再來介紹重定位阿宅。在所產(chǎn)生的對象文件 test.o 中有對函數(shù) sum_func 的引用,這對我們的x386結(jié)構(gòu)來說回论,其實就是一條call指令谱净。既然 sum_func 是定義在 sum.o 中的,那對 test.o 來說月培,它就是一個外部引用猬错。所以著恩,匯編器在產(chǎn)生 test.o 的時候贞盯,它會產(chǎn)生一個重定位條目遭居。重定位條目中會包含以下幾類東西:
它會包含一個符號表中一個條目的索引照捡,因為這樣我們才知道它具體是哪個符號需要被重定位的嚼贡;
它會包含一個 .text section 中的地址單元的偏移值霹俺。原本這個偏移值處的地址單元里面應(yīng)該存放著 call 指令的操作數(shù)柔吼。對上面來說,也就是函數(shù) sum_func 的地址丙唧,但是目前這個地址匯編器還不知道愈魏。
它還會包含一個tag,以指明該重定位屬于何種類型想际。
當(dāng)我們用鏈接器去鏈接這個對象文件的時候培漏,鏈接器會遍歷所有的重定位條目,碰到像 sum_func 這樣的外部引用沼琉,它會找到 sum_func 的確切地址北苟,并且把它寫回到上面 call 指令操作數(shù)所占用的那個地址單元。像這樣的操作打瘪,稱之為重定位操作友鼻。link editor 和 dynamic linker 都要完成一些重定位操作,只不過后者的動作更加復(fù)雜闺骚,因為它是在運(yùn)行時動態(tài)完成的彩扔,我們以后的文章會介紹相關(guān)的內(nèi)容。概括一下僻爽,所謂重定位操作就是:“匯編的時候產(chǎn)生一個空坐位虫碉,上面用紅紙寫著要坐在這個座位上的人的名字,然后連接器在開會前安排那個人坐上去”胸梆。
如前面我們說過的敦捧,對象文件中的重定位條目须板,會構(gòu)成一個個單獨(dú)的 section。這些 section 的名字兢卵,常會是這樣的形式:".rel.XXX"习瑰。其中XXX表示的是這些重定位條目所作用到的section,如 .text section秽荤。重定位條目所構(gòu)成的section需要和另外兩個section產(chǎn)生關(guān)聯(lián):符號表section(表示要重定位的是哪一個符號)以及受影響地址單元所在的section甜奄。在使用工具來查看重定位section之前,我們先從規(guī)范中找出來表示重定位條目的結(jié)構(gòu)定義(有兩種窃款,依處理器架構(gòu)來定):
[圖片上傳失敗...(image-550a43-1587041043464)]
結(jié)構(gòu)中 r_offset 對于可重定位文件.o來說课兄,就是地址單元的偏移值(前面的b條);另外對可執(zhí)行文件或者動態(tài)庫來說晨继,就是該地址單元的運(yùn)行時地址烟阐。上面 a條中的符號表內(nèi)索引和c條中的類型,一起構(gòu)成了結(jié)構(gòu)中的字段 r_info踱稍。
重定位過程在計算最終要放到受影響地址單元中的時候曲饱,需要加上一個附加的數(shù) addend。當(dāng)某一種處理器選用 Elf32_Rela 結(jié)構(gòu)的時候珠月,該 addend 就是結(jié)構(gòu)中的 r_addend 字段扩淀;否則該 addend 就是原本存儲在受影響地址單元中的原有值。x86架構(gòu)選用 Elf32_Rel 結(jié)構(gòu)來表示重定位條目啤挎。ARM架構(gòu)也是用這個驻谆。
重定位類型意味著如何去修改受影響的地址單元,也就是按照何種方式去計算需要最后放在受影響單元里面的值庆聘。具體的重定位類型有哪些胜臊,取決與特定的處理器架構(gòu),你可以參考相關(guān)規(guī)范伙判。這種計算方式可以非常的簡單象对,比如在x386上的 R_386_32 類型,它規(guī)定只是將附加數(shù)加上符號的值作為所需要的值宴抚;該計算方式也可以是非常的復(fù)雜勒魔,比如老版本ARM平臺上的 R_ARM_PC26。在這篇文章的末尾菇曲,我會詳細(xì)介紹一種重定位類型:R_386_PC32冠绢。至于另外一些重要的重定位類型,如R_386_GOTPC常潮,R_386_PLT32弟胀,R_386_GOT32,R_386_GLOB_DAT 以及 R_386_JUMP_SLOT 等。讀者可以先自己研究孵户,也許我們會在后面后面的文章中討論到相關(guān)主題時再行介紹萧朝。
我們可以使用命令 readelf -r 來查看重定位信息:
[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對象文件格式中的 linking view 延届,也就是上面組成圖的左邊部分剪勿,我們已經(jīng)介紹完畢贸诚。在這里最重要的概念是 section方庭。在可重定位文件里面,section承載了大多數(shù)被包含的東西酱固,代碼械念、數(shù)據(jù)、符號信息运悲、重定位信息等等龄减。可重定位對象文件里面的這些sections是作為輸入班眯,給鏈接器那去做鏈接用的希停,所以這些 sections 也經(jīng)常被稱做輸入 section。
鏈接器在鏈接可執(zhí)行文件或動態(tài)庫的過程中署隘,它會把來自不同可重定位對象文件中的相同名稱的 section 合并起來構(gòu)成同名的 section宠能。接著,它又會把帶有相同屬性(比方都是只讀并可加載的)的 section 都合并成所謂 segments(段)磁餐。segments 作為鏈接器的輸出违崇,常被稱為輸出section。我們開發(fā)者可以控制哪些不同.o文件的sections來最后合并構(gòu)成不同名稱的 segments诊霹。如何控制呢羞延,就是通過 linker script 來指定。關(guān)于鏈接器腳本脾还,我們這里不予討論伴箩。
一個單獨(dú)的 segment 通常會包含幾個不同的 sections,比方一個可被加載的鄙漏、只讀的segment 通常就會包括可執(zhí)行代碼section .text嗤谚、只讀的數(shù)據(jù)section .rodata以及給動態(tài)鏈接器使用的符號section .dymsym等等。section 是被鏈接器使用的泥张,但是 segments 是被加載器所使用的呵恢。加載器會將所需要的 segment 加載到內(nèi)存空間中運(yùn)行。和用 sections header table 來指定一個可重定位文件中到底有哪些 sections 一樣媚创。在一個可執(zhí)行文件或者動態(tài)庫中渗钉,也需要有一種信息結(jié)構(gòu)來指出包含有哪些 segments。這種信息結(jié)構(gòu)就是 program header table,如ELF對象文件格式中右邊的 execute view 所示的那樣鳄橘。
我們可以用 readelf -l 來查看可執(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個 segments术徊。同時,該結(jié)果也很明白顯示出了哪些 section 映射到哪一個 segment 當(dāng)中去鲸湃。比方在索引為2的那個segment 中赠涮,總共有15個 sections 映射進(jìn)來,其中包括我們前面提到過的 .text section暗挑。注意這個segment 有兩個標(biāo)志: R 和 E笋除。這個表示該segment是可讀的,也可執(zhí)行的炸裆。如果你看到標(biāo)志中有W垃它,那表示該segment是可寫的。
我們還是來解釋一下上面的結(jié)果烹看,希望你能對照著TISCv1.2規(guī)范里面的文本來看国拇,我這里也列出程序頭表條目的C結(jié)構(gòu):
[圖片上傳失敗...(image-fc5407-1587041043464)]
上面類型為PHDR的segment,用來包含程序頭表本身惯殊。類型為INTERP的segment只包含一個 section酱吝,那就是 .interp。在這個section中靠胜,包含了動態(tài)鏈接過程中所使用的解釋器路徑和名稱掉瞳。在Linux里面,這個解釋器實際上就是 /lib/ 浪漠,這可以通過下面的 hexdump 看出來:[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
為什么會有這樣的一個 segment陕习?這是因為我們寫的應(yīng)用程序通常都需要使用動態(tài)鏈接庫.so,就像 test 程序中所使用的 libsub.so 一樣址愿。我們還是先大致說說程序在linux里面是怎么樣運(yùn)行起來的吧该镣。當(dāng)你在 shell 中敲入一個命令要執(zhí)行時,內(nèi)核會幫我們創(chuàng)建一個新的進(jìn)程响谓,它在往這個新進(jìn)程的進(jìn)程空間里面加載進(jìn)可執(zhí)行程序的代碼段和數(shù)據(jù)段后损合,也會加載進(jìn)動態(tài)連接器(在Linux里面通常就是 /lib/ld-linux.so 符號鏈接所指向的那個程序,它本省就是一個動態(tài)庫)的代碼段和數(shù)據(jù)娘纷。在這之后嫁审,內(nèi)核將控制傳遞給動態(tài)鏈接庫里面的代碼。動態(tài)連接器接下來負(fù)責(zé)加載該命令應(yīng)用程序所需要使用的各種動態(tài)庫赖晶。加載完畢律适,動態(tài)連接器才將控制傳遞給應(yīng)用程序的main函數(shù)辐烂。如此,你的應(yīng)用程序才得以運(yùn)行捂贿。
這里說的只是大致的應(yīng)用程序啟動運(yùn)行過程纠修,更詳細(xì)的,我們會在后續(xù)的文章中繼續(xù)討論厂僧。我們說link editor鏈接的應(yīng)用程序只是部分鏈接過的應(yīng)用程序扣草。經(jīng)常的,在應(yīng)用程序中颜屠,會使用很多定義在動態(tài)庫中的函數(shù)辰妙。最最基礎(chǔ)的比方C函數(shù)庫(其本身就是一個動態(tài)庫)中定義的函數(shù),每個應(yīng)用程序總要使用到汽纤,就像我們test程序中使用到的 printf 函數(shù)上岗。為了使得應(yīng)用程序能夠正確使用動態(tài)庫,動態(tài)連接器在加載動態(tài)庫后蕴坪,它還會做更進(jìn)一步的鏈接,這就是所謂的動態(tài)鏈接敬锐。為了讓動態(tài)連接器能成功的完成動態(tài)鏈接過程背传,在前面運(yùn)行的link editor需要在應(yīng)用程序可執(zhí)行文件中生成數(shù)個特殊的 sections,比方 .dynamic台夺、.dynsym径玖、.got和.plt等等。這些內(nèi)容我們會在后面的文章中進(jìn)行討論颤介。
我們先回到上面所輸出的文件頭表中梳星。在接下來的數(shù)個 segments 中,最重要的是三個 segment:代碼段滚朵,數(shù)據(jù)段和堆棧段冤灾。代碼段和堆棧段的 VirtAddr 列的值分別為 0x08048000 和 0x0804973c。這是什么意思呢辕近?這是說對應(yīng)的段要加載在進(jìn)程虛擬地址空間中的起始地址韵吨。雖然在可執(zhí)行文件中規(guī)定了 text segment和 data segment 的起始地址,但是最終移宅,在內(nèi)存中的這些段的真正起始地址归粉,卻可能不是這樣的,因為在動態(tài)鏈接器加載這些段的時候漏峰,需要考慮到頁面對齊的因素糠悼。為什么?因為像x86這樣的架構(gòu)浅乔,它給內(nèi)存單元分配讀寫權(quán)限的最小單位是頁(page)而不是字節(jié)倔喂。也就是說,它能規(guī)定從某個頁開始、連續(xù)多少頁是只讀的滴劲。卻不能規(guī)定從某個頁內(nèi)的哪一個字節(jié)開始攻晒,連續(xù)多少個字節(jié)是只讀的。因為x86架構(gòu)中班挖,一個page大小是4k鲁捏,所以,動態(tài)鏈接器在加載 segment 到虛擬內(nèi)存中的時候萧芙,其真實的起始地址的低12位都是零给梅,也即以 0x1000 對齊。
我們先來看看一個真實的進(jìn)程中的內(nèi)存空間信息双揪,拿我們的 test 程序作為例子动羽。在 Linux 系統(tǒng)中,有一個特殊的由內(nèi)核實現(xiàn)的虛擬文件系統(tǒng) /proc渔期。內(nèi)核實現(xiàn)這個文件系統(tǒng)运吓,并將它作為整個Linux系統(tǒng)面向外部世界的一個接口。我們可以通過 /proc 觀察到一個正在運(yùn)行著的Linux系統(tǒng)的內(nèi)核數(shù)據(jù)信息以及各進(jìn)程相關(guān)的信息疯趟。所以我們?nèi)绻榭茨骋粋€進(jìn)程的內(nèi)存空間情況拘哨,也可以通過它來進(jìn)行。使用/proc唯一需要注意的是信峻,由于我們的 test 程序很小倦青,所以當(dāng)我們運(yùn)行起來之后,它很快就會結(jié)束掉盹舞,使得我們沒有時間去查看test的進(jìn)程信息产镐。我們需要想辦法讓它繼續(xù)運(yùn)行,或者最起碼運(yùn)行直到讓我們能從 /proc 中獲取得到想要的信息后再結(jié)束踢步。
我們有多種選擇癣亚。最簡單的是,在 test main 程序中插入一個循環(huán)贾虽,然后在循環(huán)中放入 sleep() 的調(diào)用逃糟,這樣當(dāng)程序運(yùn)行到這個循環(huán)的時候,就會進(jìn)入“運(yùn)行-睡眠-運(yùn)行-睡眠”循環(huán)中蓬豁。這樣我們就有機(jī)會去看它的虛擬內(nèi)存空間信息绰咽。另外一個方法,是使用調(diào)試器地粪,如GDB取募。我們設(shè)置一個斷點(diǎn),然后在調(diào)試過程中讓test進(jìn)程在這個斷點(diǎn)處暫停蟆技,這樣我們也有機(jī)會獲得地址空間的信息玩敏。我們這里就使用這種方法斗忌。當(dāng)然,為了能讓GDB調(diào)試我們的 test旺聚,我們得在編譯的時候加上"-g"選項织阳。最后我們用下面的命令得到 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 是用`括起來的唧躲,它不是單引號,而是鍵盤上 Esc 字符下面的那個字符碱璃。從這個結(jié)果上可以看出弄痹,所有的段,其起始地址和結(jié)束地址(前面兩列)都是0x1000對齊的嵌器。結(jié)果中也列出了對應(yīng)的段是從哪里引過來的肛真,比方動態(tài)鏈接器/lib/ld-2.3.4.so、C函數(shù)庫和test程序本身爽航。注意看test程序引入的代碼段起始地址是 0x08048000蚓让,這和我們 ELF 文件中指定的相同,但是結(jié)束地址卻是0x08049000岳掐,和文件中指定的不一致(0x08048000+0x0073c=0x0804873c)凭疮。這里,其實加載器也把數(shù)據(jù)segment中開頭一部分也映射進(jìn)了 text segment 中去串述;同樣的,進(jìn)程虛擬內(nèi)存空間中的 data segment 從 08049000 開始寞肖,而可執(zhí)行文件中指定的是從 0x0804973c 開始纲酗。所以加載器也把代碼segment中末尾一部分也映射進(jìn)了 data segment 中去了。
從程序頭表中我們可以看到一個類型為 GNU_STACK 的segment新蟆,這是 stack segment觅赊。程序頭表中的這一項,除了 Flg/Align 兩列不為空外琼稻, 其他列都為0吮螺。這是因為堆棧段在虛擬內(nèi)存空間中,從哪里開始帕翻、占多少字節(jié)是由內(nèi)核說了算的鸠补,而不決定于可執(zhí)行程序。實際上嘀掸,內(nèi)核決定把堆棧段放在整個進(jìn)程地址空間的用戶空間的最上面紫岩,所以堆棧段的末尾地址就是 0xc0000000。別忘記在 x86 中睬塌,堆棧是從高向低生長的泉蝌。
好歇万,為了方便你對后續(xù)文章的理解,我們在這里討論一種比較簡單的重定位類型 R_386_PC32勋陪。前面我們說過重定義的含義贪磺,也即在連接階段,根據(jù)某種計算方式計算出一個新的值(通常是地址)诅愚,然后將這個值重新改寫到對象文件或者內(nèi)存映像中某個section中的某個地址單元中去的這樣一個過程寒锚。那所謂重定位類型,就規(guī)定了使用何種方式呻粹,去計算這個值壕曼。既然是計算,那就肯定需要涉及到所要納入計算的變量等浊。實際上腮郊,具體有哪些變量參與計算如同如何進(jìn)行計算一樣也是不固定的,各種重定位類型有自己的規(guī)定筹燕。
根據(jù)規(guī)范里面的規(guī)定轧飞,重定位類型 R_386_PC32 的計算需要有三個變量參與:S,A和P撒踪。其計算方式是 S+A-P过咬。根據(jù)規(guī)范,當(dāng)R_386_PC32類型的重定位發(fā)生在 link editor 鏈接若干個 .o 對象文件從而形成可執(zhí)行文件的過程中的時候制妄,變量S指代的是被重定位的符號的實際運(yùn)行時地址掸绞,而變量P是重定位所影響到的地址單元的實際運(yùn)行時地址。在運(yùn)行于x86架構(gòu)上的Linux系統(tǒng)中耕捞,這兩個地址都是虛擬地址衔掸。變量A最簡單,就是重定位所需要的附加數(shù)俺抽,它是一個常數(shù)敞映。別忘了x86架構(gòu)所使用的重定位條目結(jié)構(gòu)體類型是 Elf32_Rela,所以附加數(shù)就存在于受重定位影響的地址單元中磷斧。重定位最后將計算得到的值patch到這個地址單元中振愿。
或許,咱們舉一個實際例子來闡述可能對你更有用弛饭。在我們的 test 程序中冕末,test.c 的 main 函數(shù)中需要調(diào)用定義在 sum.o 中的 sum_func 函數(shù),所以link editor 在將 test.o/sum.o 聯(lián)結(jié)成可執(zhí)行文件 test 的時候孩哑,必須處理一個重定位栓霜,這個重定位就是 R_386_PC32 類型的。我們先用 objdump 來查看 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 0xfffffff0,%esp
9: b8 00 00 00 00 mov 0xf,%eax
11: 83 c0 0f add 0x4,%eax
17: c1 e0 04 shl 0xa,0xfffffffc(%ebp)
23: c7 45 f8 2d 00 00 00 movl 0x3,0xfffffff4(%ebp)
31: c7 45 f0 48 00 00 00 movl 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 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é)果所示横蜒,在離開 .text section 開始 0x53 字節(jié)的地方胳蛮,有一條call指令销凑。這條指令是對 sum_func 函數(shù)的調(diào)用,objdump 將其反匯編成 call 54仅炊,這是因為偏移 0x54 字節(jié)的地方原本應(yīng)該放著 sum_func 函數(shù)的地址斗幼,但現(xiàn)在因為 sum_func 定義在 sum.o 中,所以這個地方就是重定位需要做 patch 的地址單元所在處抚垄。我們注意到蜕窿,這個地址單元的值為 0xfffffffc,也就是十進(jìn)制的 -4(計算機(jī)中數(shù)是用補(bǔ)碼表示的)呆馁。所以桐经,參與重定位運(yùn)算的變量A就確定了,即是 -4浙滤。
我們在 test.o 中找出影響該地址單元的重定位記錄如下:
[yihect@juliantec test_2]$ readelf -r ./test.o | grep 54
00000054 00000a02 R_386_PC32 00000000 sum_func
果然阴挣,如你所見,該條重定位記錄是 R_386_PC32 類型的纺腊。前面變量A確定了畔咧,那么另外兩個變量S和變量P呢?從正向去計算這兩個變量的值比較麻煩揖膜。盡管我們知道誓沸,在Linux里面,鏈接可執(zhí)行程序時所使用的默認(rèn)的鏈接器腳本將最后可執(zhí)行程序的 .text segment 起始地址設(shè)置在 0x08048000的位置壹粟。但是拜隧,從這個地址出發(fā),去尋找符號(函數(shù))sub_func 和 上面受重定位影響的地址單元的運(yùn)行時地址的話趁仙,需要經(jīng)過很多人工計算虹蓄,所以比較麻煩。
相反的幸撕,我們使用objdump工具像下面這樣分析最終鏈接生成的可執(zhí)行程序 ./test 的 .text segment 段,看看函數(shù) sum_func 和 那個受影響單元的運(yùn)行時地址到底是多少外臂,這是反向的查看鏈接器的鏈接結(jié)果坐儿。鏈接器在鏈接的過程中是正向的將正確的地址分配給它們的。
[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 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)行時地址是 0x0804860c貌矿,所以變量S的值就是 0x0804860c。那么變量P呢罪佳?它表示的是重定位所影響地址單元的運(yùn)行地址逛漫。如果要計算這個地址,我們可以先看看 main 函數(shù)的運(yùn)行時地址赘艳,再加上0x54字節(jié)的偏移來得到酌毡。從上面看出 main 函數(shù)的運(yùn)行時地址為 0x08048540克握,所以重定位所影響地址單元的運(yùn)行時地址為 0x08048540+0x54 = 0x08048594。所以重定位計算的最終結(jié)果為:
S+A-P = 0x0804860c+(-4)-0x08048594 = 0x00000074
從上面可以看出枷踏,鏈接器在鏈接過程中菩暗,確實也把這個計算得到的結(jié)果存儲到了上面 call 指令操作數(shù)所在的地址單元中去了。那么旭蠕,程序在運(yùn)行時停团,是如何憑借這樣一條帶有如此操作數(shù)的 call 指令來調(diào)用到(或者跳轉(zhuǎn)到)函數(shù) sum_func 中去的呢?
你看掏熬,調(diào)用者 main 和被調(diào)用者 sum_func 處在同一個text segment中佑稠。根據(jù)x86架構(gòu)或者IBM兼容機(jī)的匯編習(xí)慣,段內(nèi)轉(zhuǎn)移或者段內(nèi)跳轉(zhuǎn)時使用的尋址方式是PC相對尋址旗芬。也就是若要讓程序從一個段內(nèi)的A處舌胶,跳轉(zhuǎn)到同一段內(nèi)的B處,那么PC相對尋址會取程序在A處執(zhí)行時的PC值岗屏,再加上某一個偏移值(offset)辆琅,得到要跳轉(zhuǎn)的目標(biāo)地址(B處地址)。那么这刷,對于x86架構(gòu)來說婉烟,由于有規(guī)定,PC總是指向下一條要執(zhí)行的指令暇屋,那么當(dāng)程序執(zhí)行在call指令的時候似袁,PC指向的是下一條add指令,其值也就是 0x8048598咐刨。最后昙衅,尋址的時候再加上call指令的操作數(shù)0x74作為偏移,計算最終的 sum_func 函數(shù)目標(biāo)地址為 0x8048598+0x74 = 0x804860c定鸟。
有點(diǎn)意思吧:)而涉,如果能繞出來,那說明我們是真的明白了联予,其實啼县,繞的過程本身就充滿著趣味性,就看你自己的心態(tài)了沸久。說到這里季眷,本文行將結(jié)束。本文所介紹的很多內(nèi)容卷胯,可能在某些同學(xué)眼中會過于簡單子刮,但是為了體現(xiàn)知識的完整性、同時也為了讓大家先有個基礎(chǔ)以便更容易的看后續(xù)的文章窑睁,我們還是在這里介紹一下ELF格式的基礎(chǔ)知識挺峡。下面一篇關(guān)于ELF主題的文章葵孤,將詳細(xì)介紹動態(tài)連接的內(nèi)在實現(xiàn)。屆時沙郭,你將看到大量的實際代碼挖掘佛呻。