在Linux系統(tǒng)使用過(guò)程中,我們經(jīng)常會(huì)看到elf32-i386、ELF 64-bit LSB等字樣。那么究竟ELF是什么呢?
當(dāng)我們使用gcc編譯工具編譯c程序會(huì)得到一個(gè)二進(jìn)制的文件,想當(dāng)然的使用vim編輯工具將其打開(kāi),結(jié)果看到如下內(nèi)容:
當(dāng)然了,大部分同學(xué)不會(huì)這樣做。數(shù)據(jù)是以二進(jìn)制形式存儲(chǔ)的,而vi只是一個(gè)文本編輯工具。那么數(shù)據(jù)究竟是怎樣存儲(chǔ),以什么樣的格式存儲(chǔ)成二進(jìn)制文件呢?是一個(gè)一個(gè)挨著排嗎?從左向右,還是從右向左?這就需要我們深入了解下ELF文件了。
ELF文件格式是一個(gè)開(kāi)放標(biāo)準(zhǔn),各種UNIX系統(tǒng)的可執(zhí)行文件都采用ELF格式,它有三種不同的類(lèi)型:
- 可重定位的目標(biāo)文件(Relocatable,或者Object File)
- 可執(zhí)行文件(Executable)
- 共享庫(kù)(Shared Object,或者Shared Library)
從我們最不畏懼的hello world入手吧。
很常見(jiàn)的,當(dāng)我們gcc hello.c -o hello 編譯這個(gè)c源程序的時(shí)候就得到了一個(gè)ELF格式的文件??梢允褂胒ile命令來(lái)查看。數(shù)據(jù)顯示,該文件是一個(gè)64位的,小尾端存儲(chǔ)的,可執(zhí)行文件。
而當(dāng)我們使用gcc -c hello.c -o hello.o編譯生成的則是一個(gè)可重定位的目標(biāo)文件,也可以使用file命令來(lái)查看它。
同樣,我們也得到了一個(gè)ELF格式的文件。但是兩者略有不同,前者是Executable可執(zhí)行文件,而后者是可重定位的Relocatable。如果你感興趣也可以試試共享庫(kù)文件,其格式依然是ELF,或許會(huì)是這樣ELF 32-bit LSB shared object。
那么ELF文件內(nèi)部是怎樣存儲(chǔ)數(shù)據(jù)的呢?當(dāng)然不能再使用vi啦,我們可以使用readelf工具來(lái)查看下,以目標(biāo)文件hello.o為例:readelf -a hello.o
輸出結(jié)果大致可分為四個(gè)部分:ELF Header(ELF頭)、Section Headers(節(jié)頭表)、Relocation section(重定位節(jié))、Symbol table(符號(hào)表),我們依次來(lái)看。
第一部分,ELF Header描述整個(gè)ELF文件的數(shù)據(jù)存儲(chǔ)概況,如操作系統(tǒng)是UNIX,體系結(jié)構(gòu)是Advanced Micro Devices X86-64,數(shù)據(jù)存儲(chǔ)是二進(jìn)制補(bǔ)碼,小尾端法存儲(chǔ),類(lèi)型是可重定位文件,Section Header Table中有13個(gè)Section Header,從文件地址304開(kāi)始,每個(gè)Section Header占64字節(jié),這個(gè)目標(biāo)文件沒(méi)有程序頭(Program Header)。
第二部分,挨著ELF頭的數(shù)據(jù)信息是Section Headers(節(jié)頭表),顧名思義,它由一定數(shù)量的Section Header組成,可從中讀出各個(gè)Section的描述信息,其中不乏我們編寫(xiě)的C程序源碼、全局變量、常量等數(shù)據(jù)的存儲(chǔ)位置。.text Section、.data Section、bss Section、.rodata Section都與我們的程序直接相關(guān),而其它Section是匯編器自動(dòng)添加的。 Address 是這些Section加載到內(nèi)存中的地址(當(dāng)然,程序中的地址都是虛擬地址),加載地址要在鏈接時(shí)填寫(xiě),現(xiàn)在空缺,由于目標(biāo)文件尚未做鏈接操作,所以是全0。 Offset 和 Size 列指出了各Section的起始文件地址和長(zhǎng)度。比如 .data 段從文件地址0x55開(kāi)始,一共0個(gè)字節(jié),因?yàn)闇y(cè)試的程序中沒(méi)有定義全局變量,只使用printf函數(shù)打印了“hello world…\n”所以后面的 .rodata Section大小為0xf也就是15個(gè)字節(jié)。
我們知道,C語(yǔ)言的全局變量如果在代碼中沒(méi)有初始化,就會(huì)在程序加載時(shí)用0初始化。這種數(shù)據(jù)屬于 .bss ,在加載時(shí)它和 .data一樣都是可讀可寫(xiě)的數(shù)據(jù),但是在ELF文件中 .data中若有數(shù)據(jù)則需要占用一部分空間保存初始值,而 .bss卻不需要。也就是說(shuō),.bss在文件中只占一個(gè)Section Header而沒(méi)有對(duì)應(yīng)的Section,程序加載時(shí) .bss 占多大內(nèi)存空間在Section Header中描述。在我們這個(gè)例子中沒(méi)有用到 .bss ,因此size也是0。
特別指出的是,.shstrtab 和 .strtab 這兩個(gè)Section中存放的都是ASCII碼,因此,在本文起始使用vi打開(kāi)的ELF文件,如果仔細(xì)看,是能夠看到字符串的,而并非通篇皆是“^@”等怪異字符。.shstrtab的全稱(chēng)應(yīng)該是“Section Header String Table”用來(lái)保存各個(gè)Section的名字。.strtab Section保存程序中用到的符號(hào)的名字,每個(gè)名字都是以 '\0' 結(jié)尾的字符串。
第三部分,可重定位節(jié)。該內(nèi)容主要針對(duì)鏈接器設(shè)定,旨在告訴鏈接器指令中的哪些地方需要做重定位。當(dāng)鏈接器完成鏈接工作后會(huì)自動(dòng)將該Section刪除。
第四部分,.symtab 是符號(hào)表。 我們?cè)诰帉?xiě)程序時(shí)定義的變量、函數(shù)都是符號(hào),main就是符號(hào)的典型代表。當(dāng)然為了保證程序能正常的編譯、加載執(zhí)行,編譯器還幫助我們加入了其他許多必要的符號(hào)。這些符號(hào)都在.symtab中有所體現(xiàn)。
Ndx 列是每個(gè)符號(hào)所在的Section編號(hào),各Section的編號(hào)在Section Header Table中有列出。 Value 列是每個(gè)符號(hào)所代表的地址,在目標(biāo)文件中,符號(hào)地址都是相對(duì)于該符號(hào)所在Section的相對(duì)地址,如定義全局變量var,那么該符號(hào)在.symtab中的Value則是相對(duì)于.data Section開(kāi)頭的位置。 main 位于 .text 段的開(kāi)頭,所以地址也是0。但是上例中所有的Value都是0不易看出差異,所以我們適當(dāng)?shù)男薷南挛覀兊臏y(cè)試程序,添加一個(gè)初始化為非0的全局變量var和一個(gè)函數(shù)func。
這時(shí).data Section的Size已經(jīng)不再為0了,因?yàn)槲覀兌x了全局變量var,它是一個(gè)int類(lèi)型的變量,存儲(chǔ)于.data Section上,因此 .data Section的Size應(yīng)該是4,請(qǐng)大家自己驗(yàn)證吧。
我們繼續(xù)來(lái)看.symtab的變化。由于加入了兩個(gè)符號(hào)var和func,所以 .symtab表的成員多了兩個(gè)。var是全局變量,存儲(chǔ)于.data Section中,編號(hào)在Ndx中指出,為3,由于只有這一個(gè)全局變量,所以var在的Value為0,相對(duì)于 .data Section開(kāi)頭的位置;符號(hào)main發(fā)生了變化,main是函數(shù)名,保存于.text Section中,編號(hào)為1,但其Value卻不再是0,由于程序中還有另外一個(gè)符號(hào)func,所以符號(hào)main的Value由原來(lái)的0變?yōu)?5,依然是相對(duì)于.text Section 起始位置而言。
但請(qǐng)大家注意,Symbol table ‘.symtab’ 中Value記錄的是符號(hào)對(duì)應(yīng)的值的位置。var是一個(gè)變量,值是數(shù)據(jù)位于.data中,func和main是函數(shù),對(duì)應(yīng)的值是函數(shù)入口地址(或者說(shuō)函數(shù)首行指令的地址),位于.text中。而“var”、“func”、“main”這些符號(hào)名本身存在哪里呢?其實(shí)這個(gè)問(wèn)題我們?cè)谇拔年U述過(guò),這些字符串本身保存在 .strtab中。這樣來(lái)看 .strtab和 .shstrtab的地位是等同的,差別是前者保存程序中用到的符號(hào),而后者保存Section名稱(chēng)。
其實(shí),ELF格式提供了兩種不同的視角,鏈接器把ELF文件看成是Section的集合,而加載器把ELF文件看
成是Segment的集合。這里以Relocatable 的Section為例帶大家分析了ELF的數(shù)據(jù)存儲(chǔ)。大家可以結(jié)合可重定位Relocatable 的ELF文件數(shù)據(jù)存儲(chǔ)的形式來(lái)了解Executable可執(zhí)行文件的數(shù)據(jù)存儲(chǔ)形式。而二者的關(guān)系可以從下圖看出。
左邊是從鏈接器的視角來(lái)看ELF文件,開(kāi)頭的ELF Header描述了體系結(jié)構(gòu)和操作系統(tǒng)等基本信息,并指出Section Header Table和Program Header Table在文件中的位置,Program Header Table在鏈接過(guò)程中用不到,所以是可有可無(wú)的,Section Header Table中保存了所有Section的描述信息,通過(guò)Section Header Table可以找到每個(gè)Section在文件中的位置。
右邊是從加載器的視角來(lái)看ELF文件,開(kāi)頭是ELF Header,Program Header Table中保存了所有Segment的描述信息,Section Header Table在加載過(guò)程中用不到,所以是可有可無(wú)的。從上圖可以看出,一個(gè)Segment由一個(gè)或多個(gè)Section組成,這些Section加載到內(nèi)存時(shí)具有相同的訪(fǎng)問(wèn)權(quán)限,如 .text Section會(huì)和 .rodata Section合并為一個(gè)Segment,同時(shí)分配只讀訪(fǎng)問(wèn)權(quán)限,而.data Section通常和 .bss Section合并為一個(gè)Segment,分配讀寫(xiě)權(quán)限。
有些Section只對(duì)鏈接器有意義,在運(yùn)行時(shí)用不到,也不需要加載到內(nèi)存,那么它可以不屬于任何Segment, 如 .rela.text Section 在Executable文件中就消失了。另外,Section Header Table和Program Header Table并不是一定要位于文件的開(kāi)頭和結(jié)尾,其位置由ELF Header指出,上圖這么畫(huà)只是為了清晰。目標(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。
本文版權(quán)歸傳智播客C++培訓(xùn)學(xué)院所有,歡迎轉(zhuǎn)載,轉(zhuǎn)載請(qǐng)注明作者出處。謝謝!
作者:傳智播客C/C++培訓(xùn)學(xué)院
首發(fā):http://m.metathetuscanyresort.com/c/