ELF文件详解—初步认识
一、? 引言
在講解ELF文件格式之前,我們來(lái)回顧一下,一個(gè)用C語(yǔ)言編寫的高級(jí)語(yǔ)言程序是從編寫到打包、再到編譯執(zhí)行的基本過(guò)程,我們知道在CPU上執(zhí)行的是低級(jí)別的機(jī)器語(yǔ)言,從高級(jí)語(yǔ)言到低級(jí)別的機(jī)器語(yǔ)言肯定是要經(jīng)過(guò)翻譯過(guò)程,這個(gè)過(guò)程大體的過(guò)程如下圖所示:
?
在Unix系統(tǒng)中,從源文件到可執(zhí)行目標(biāo)文件是由編譯驅(qū)動(dòng)程序完成的,如大名鼎鼎的gcc,翻譯過(guò)程包括圖中的是個(gè)階段;
?? 預(yù)處理階段
預(yù)處理器(cpp)根據(jù)以字符#開(kāi)頭的命令修給原始的C程序,結(jié)果得到另一個(gè)C程序,通常以.i作為文件擴(kuò)展名。主要是進(jìn)行文本替換、宏展開(kāi)、刪除注釋這類簡(jiǎn)單工作。
對(duì)應(yīng)的命令:linux> gcc -E hello.c hello.i?
?? 編譯階段
編譯器將文本文件hello.i翻譯成hello.s,包含相應(yīng)的匯編語(yǔ)言程序
對(duì)應(yīng)的命令:linux> gcc -S hello.c hello.s?
?? 匯編階段
將.s文件翻譯成機(jī)器語(yǔ)言指令,把這些指令打包成一種叫做可重定位目標(biāo)程序的格式,并將結(jié)果保存在目標(biāo)文件.o中(把匯編語(yǔ)言翻譯成機(jī)器語(yǔ)言的過(guò)程)。
把一個(gè)源程序翻譯成目標(biāo)程序的工作過(guò)程分為五個(gè)階段:詞法分析;語(yǔ)法分析;語(yǔ)義檢查和中間代碼生成;代碼優(yōu)化;目標(biāo)代碼生成。主要是進(jìn)行詞法分析和語(yǔ)法分析,又稱為源程序分析,分析過(guò)程中發(fā)現(xiàn)有語(yǔ)法錯(cuò)誤,給出提示信息。
對(duì)應(yīng)的命令:linux> gcc -c hello.c hello.o
?? 鏈接階段
此時(shí)hello程序調(diào)用了printf函數(shù)。 printf函數(shù)存在于一個(gè)名為printf.o的單獨(dú)的預(yù)編譯目標(biāo)文件中。 鏈接器(ld)就負(fù)責(zé)處理把這個(gè)文件并入到hello.o程序中,結(jié)果得到hello文件,一個(gè)可執(zhí)行文件。最后可執(zhí)行文件加載到儲(chǔ)存器后由系統(tǒng)負(fù)責(zé)執(zhí)行, ?函數(shù)庫(kù)一般分為靜態(tài)庫(kù)和動(dòng)態(tài)庫(kù)兩種。靜態(tài)庫(kù)是指編譯鏈接時(shí),把庫(kù)文件的代碼全部加入到可執(zhí)行文件中,因此生成的文件比較大,但在運(yùn)行時(shí)也就不再需要庫(kù)文件了。其后綴名一般為.a。動(dòng)態(tài)庫(kù)與之相反,在編譯鏈接時(shí)并沒(méi)有把庫(kù)文件的代碼加入到可執(zhí)行文件中,而是在程序執(zhí)行時(shí)由運(yùn)行時(shí)鏈接文件加載庫(kù),這樣可以節(jié)省系統(tǒng)的開(kāi)銷。動(dòng)態(tài)庫(kù)一般后綴名為.so,gcc在編譯時(shí)默認(rèn)使用動(dòng)態(tài)庫(kù)。
二、目標(biāo)文件
由上面的過(guò)程,我們可以看出在經(jīng)過(guò)匯編器和連接器作用后都會(huì)輸出一個(gè)目標(biāo)文件,那這兩個(gè)目標(biāo)文件有什么樣的區(qū)別呢?說(shuō)到這里我們先引入目標(biāo)文件的形式
2.1 三種目標(biāo)文件形式
(1)可重定位目標(biāo)文件:包含二進(jìn)制代碼和數(shù)據(jù),其形式可以和其他目標(biāo)文件進(jìn)行合并,創(chuàng)建一個(gè)可執(zhí)行目標(biāo)文件
(2)可執(zhí)行目標(biāo)文件:包含二進(jìn)制代碼和數(shù)據(jù),可直接被加載器加載執(zhí)行
(3)共享目標(biāo)文件:可被動(dòng)態(tài)的加載和鏈接(本文暫時(shí)不討論)
由此我們可知由匯編器生成的就是可重定位目標(biāo)文件,經(jīng)過(guò)鏈接器作用后才生成可執(zhí)行目標(biāo)文件,鏈接器的作用就是以一組可重定位目標(biāo)文件作為輸入,生成可加載和運(yùn)行的可執(zhí)行目標(biāo)文件,具體需要完成以下兩個(gè)工作:
?? 符號(hào)解析:符號(hào)解析的目的是將目標(biāo)文件中每個(gè)符號(hào)(靜態(tài)變量、函數(shù)、全局變量)和其定義進(jìn)行關(guān)聯(lián)
?? 重定位:將每個(gè)符號(hào)的定義與具體在虛擬內(nèi)存中的位置進(jìn)行關(guān)聯(lián)
最終生成可執(zhí)行目標(biāo)文件
說(shuō)到這里好像還是沒(méi)有說(shuō)清楚這兩種目標(biāo)文件有什么區(qū)別,我們還是先把這個(gè)問(wèn)題放一下,相信你看完下一節(jié),應(yīng)該會(huì)有答案,下面我們開(kāi)始引入目標(biāo)文件ELF文件。
三、ELF文件
目標(biāo)文件再不同的系統(tǒng)或平臺(tái)上具有不同的命名格式,在Unix和X86-64 Linux上稱為ELF(Executable and Linkable Format, ELF)。
ELF文件格式提供了兩種不同的視角,在匯編器和鏈接器看來(lái),ELF文件是由Section Header Table描述的一系列Section的集合,而執(zhí)行一個(gè)ELF文件時(shí),在加載器(Loader)看來(lái)它是由Program Header Table描述的一系列Segment的集合
左邊是從匯編器和鏈接器的視角來(lái)看這個(gè)文件,開(kāi)頭的ELF Header描述了體系結(jié)構(gòu)和操作系統(tǒng)等基本信息,并指出Section Header Table和Program Header Table在文件中的什么位置,Program Header Table在匯編和鏈接過(guò)程中沒(méi)有用到,所以是可有可無(wú)的,Section Header Table中保存了所有Section的描述信息。右邊是從加載器的視角來(lái)看這個(gè)文件,開(kāi)頭是ELF Header,Program Header Table中保存了所有Segment的描述信息,Section Header Table在加載過(guò)程中沒(méi)有用到,所以是可有可無(wú)的。注意Section Header Table和Program Header Table并不是一定要位于文件開(kāi)頭和結(jié)尾的,其位置由ELF Header指出,上圖這么畫只是為了清晰。
我們?cè)趨R編程序中用.section聲明的Section會(huì)成為目標(biāo)文件中的Section,此外匯編器還會(huì)自動(dòng)添加一些Section(比如符號(hào)表)。Segment是指在程序運(yùn)行時(shí)加載到內(nèi)存的具有相同屬性的區(qū)域,由一個(gè)或多個(gè)Section組成,比如有兩個(gè)Section都要求加載到內(nèi)存后可讀可寫,就屬于同一個(gè)Segment。有些Section只對(duì)匯編器和鏈接器有意義,在運(yùn)行時(shí)用不到,也不需要加載到內(nèi)存,那么就不屬于任何Segment。
目標(biāo)文件需要鏈接器做進(jìn)一步處理,所以一定有Section Header Table;可執(zhí)行文件需要加載運(yùn)行,所以一定有Program Header Table;而共享庫(kù)既要加載運(yùn)行,又要在加載時(shí)做動(dòng)態(tài)鏈接,所以既有Section Header Table又有Program Header Table。
關(guān)于目標(biāo)文件的具體節(jié)的數(shù)據(jù)結(jié)構(gòu),有興趣的讀者參照北大的一個(gè)資料寫的非常詳細(xì)
點(diǎn)擊打開(kāi)鏈接
下面用readelf工具讀出目標(biāo)文件max.o的ELF Header和Section Header Table,然后我們逐段分析。
接下來(lái)我們來(lái)看Section Header Table格式
從Section Header中讀出各Section的描述信息,其中.text和.data是我們?cè)趨R編程序中聲明的Section,而其它Section是匯編器自動(dòng)添加的。Addr是這些段加載到內(nèi)存中的地址(我們講過(guò)程序中的地址都是虛擬地址),加載地址要在鏈接時(shí)填寫,現(xiàn)在空缺,所以是全0。Off和Size兩列指出了各Section的文件地址,比如.data從文件地址0x60開(kāi)始,一共0x38個(gè)字節(jié),回去翻一下程序,.data中定義了14個(gè)4字節(jié)的整數(shù),一共是56個(gè)字節(jié),也就是0x38個(gè)。根據(jù)以上信息可以描繪出整個(gè)目標(biāo)文件的布局。
| 起始文件地址 | Section或Header |
| 0 | ELF Header |
| 0x34 | .text |
| 0x60 | .data |
| 0x98 | .bss(此段為空) |
| 0x98 | .shstrtab |
| 0xc8 | Section Header Table |
| 0x208 | .symtab |
| 0x288 | .strtab |
| 0x2b0 | .rel.text |
?
這個(gè)文件不大,我們直接用hexdump或者使用010 Editor工具把目標(biāo)文件的字節(jié)全部打印出來(lái)看。
3.1 .shstrtab和.strtab
.shstrtab和.strtab這兩個(gè)Section中存放的都是ASCII碼:
可見(jiàn).shstrtab中保存著各Section的名字,.strtab中保存著程序中用到的符號(hào)的名字。每個(gè)名字都是以'\0'結(jié)尾的字符串。
我們知道,C語(yǔ)言的全局變量如果在代碼中沒(méi)有初始化,就會(huì)在程序加載時(shí)用0初始化。這種數(shù)據(jù)屬于.bss段,在加載時(shí)它和.data段一樣都是可讀可寫的數(shù)據(jù),但是在ELF文件中.data段需要占用一部分空間保存初始值,而.bss段則不需要。也就是說(shuō),.bss段在文件中只占一個(gè)Section Header而沒(méi)有對(duì)應(yīng)的Section,程序加載時(shí).bss段占多大內(nèi)存空間在Section Header中描述。在我們這個(gè)例子中沒(méi)有用到.bss段,以后我們會(huì)看到這樣的例子。
3.2.rel.text和.symtab
我們繼續(xù)分析readelf輸出的最后一部分,是從.rel.text和.symtab這兩個(gè)Section中讀出的信息。
.rel.text告訴鏈接器指令中的哪些地方需要重定位,我們?cè)谙乱还?jié)討論。
.symtab是符號(hào)表。Ndx列是每個(gè)符號(hào)所在的Section編號(hào),例如data_items在第3個(gè)Section里(也就是.data),各Section的編號(hào)見(jiàn)Section Header Table。Value列是每個(gè)符號(hào)所代表的地址,在目標(biāo)文件中,符號(hào)地址都是相對(duì)于該符號(hào)所在Section的相對(duì)地址,比如data_items位于.data段的開(kāi)頭,所以地址是0,_start位于.text段的開(kāi)頭,所以地址也是0,但是start_loop和loop_exit相對(duì)于.text段的地址就不是0了。從Bind這一列可以看出_start這個(gè)符號(hào)是GLOBAL的,而其它符號(hào)是LOCAL的,GLOBAL符號(hào)是在匯編程序中用.globl指示聲明過(guò)的符號(hào)。
3.3 .text節(jié)
通過(guò)使用objdump工具可以把程序中的機(jī)器指令進(jìn)行反匯編(Disassemble),得到其匯編代碼
四、可執(zhí)行文件
先看可執(zhí)行文件header的變化
在看section header的變化
.text和.data的加載地址分別改成了0x08048074和0x0804 90a0。.bss段沒(méi)有用到,所以被刪掉了。.rel.text段就是用于鏈接過(guò)程的,鏈接完了就沒(méi)用了,所以也刪掉了。
在看多出來(lái)的兩個(gè)program header
多出來(lái)的Program Header Table描述了兩個(gè)Segment的信息。.text段和前面的ELFHeader、Program Header Table一起組成一個(gè)Segment(FileSiz指出總長(zhǎng)度是0x9e),.data段組成另一個(gè)Segment(總長(zhǎng)度是0x38)。VirtAddr列指出第一個(gè)Segment加載到虛擬地址0x0804 8000(注意在x86平臺(tái)上后面的PhysAddr列是沒(méi)有意義的),第二個(gè)Segment加載到地址0x0804 90a0。Flg列指出第一個(gè)Segment的訪問(wèn)權(quán)限是可讀可執(zhí)行,第二個(gè)Segment的訪問(wèn)權(quán)限是可讀可寫。最后一列Align的值0x1000(4K)是x86平臺(tái)的內(nèi)存頁(yè)面大小。在加載時(shí)要求文件中的一頁(yè)對(duì)應(yīng)內(nèi)存中的一頁(yè),對(duì)應(yīng)關(guān)系如下圖所示。
這個(gè)可執(zhí)行文件很小,總共也不超過(guò)一頁(yè)大小,但是兩個(gè)Segment必須加載到內(nèi)存中兩個(gè)不同的頁(yè)面,因?yàn)镸MU的權(quán)限保護(hù)機(jī)制是以頁(yè)為單位的,一個(gè)頁(yè)面只能設(shè)置一種權(quán)限。此外還規(guī)定每個(gè)Segment在文件頁(yè)面內(nèi)偏移多少加載到內(nèi)存頁(yè)面仍然偏移多少,比如第二個(gè)Segment在文件中的偏移是0xa0,在內(nèi)存頁(yè)面0x0804 9000中的偏移仍然是0xa0,所以是從0x0804 90a0開(kāi)始,這樣規(guī)定是為了簡(jiǎn)化鏈接器和加載器的實(shí)現(xiàn)。從上圖也可以看出.text段的加載地址應(yīng)該是0x0804 8074,也正是_start符號(hào)的地址和程序的入口地址。
原來(lái)目標(biāo)文件符號(hào)表中的Value都是相對(duì)地址,現(xiàn)在都改成絕對(duì)地址了。此外還多了三個(gè)符號(hào)__bss_start、_edata和_end,這些是在鏈接過(guò)程中添進(jìn)去的,加載器可以利用這些信息把.bss段初始化為0。
再看一下反匯編的結(jié)果:
到此為止ELF文件的問(wèn)題已介基本介紹,關(guān)于共享目標(biāo)文件的格式和加載過(guò)程將在后續(xù)補(bǔ)上。
總結(jié)
以上是生活随笔為你收集整理的ELF文件详解—初步认识的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 《DIY四轴飞行器》读书笔记1
- 下一篇: ELF文件解析