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