ELF文件詳解—初步認識

一、  引言

在講解ELF文件格式之前,我們來回顧一下,一個用C語言編寫的高級語言程序是從編寫到打包、再到編譯執行的基本過程,我們知道在CPU上執行的是低級別的機器語言,從高級語言到低級別的機器語言肯定是要經過翻譯過程,這個過程大體的過程如下圖所示:

 


在Unix系統中,從源文件到可執行目標文件是由編譯驅動程序完成的,如大名鼎鼎的gcc,翻譯過程包括圖中的是個階段;

Ø  預處理階段

預處理器(cpp)根據以字符#開頭的命令修給原始的C程序,結果得到另一個C程序,通常以.i作爲文件擴展名。主要是進行文本替換、宏展開、刪除註釋這類簡單工作。

對應的命令:linux> gcc -E hello.c hello.i 

Ø  編譯階段

編譯器將文本文件hello.i翻譯成hello.s,包含相應的彙編語言程序

對應的命令:linux> gcc -S hello.c hello.s 

Ø  彙編階段

.s文件翻譯成機器語言指令,把這些指令打包成一種叫做可重定位目標程序的格式,並將結果保存在目標文件.o中(把彙編語言翻譯成機器語言的過程)。

把一個源程序翻譯成目標程序的工作過程分爲五個階段:詞法分析;語法分析;語義檢查和中間代碼生成;代碼優化;目標代碼生成。主要是進行詞法分析和語法分析,又稱爲源程序分析,分析過程中發現有語法錯誤,給出提示信息。

對應的命令:linux> gcc -c hello.c hello.o

Ø  鏈接階段

此時hello程序調用了printf函數。 printf函數存在於一個名爲printf.o的單獨的預編譯目標文件中。 鏈接器(ld)就負責處理把這個文件併入到hello.o程序中,結果得到hello文件,一個可執行文件。最後可執行文件加載到儲存器後由系統負責執行,  函數庫一般分爲靜態庫和動態庫兩種。靜態庫是指編譯鏈接時,把庫文件的代碼全部加入到可執行文件中,因此生成的文件比較大,但在運行時也就不再需要庫文件了。其後綴名一般爲.a。動態庫與之相反,在編譯鏈接時並沒有把庫文件的代碼加入到可執行文件中,而是在程序執行時由運行時鏈接文件加載庫,這樣可以節省系統的開銷。動態庫一般後綴名爲.so,gcc在編譯時默認使用動態庫。

二、目標文件

由上面的過程,我們可以看出在經過彙編器和連接器作用後都會輸出一個目標文件,那這兩個目標文件有什麼樣的區別呢?說到這裏我們先引入目標文件的形式

2.1 三種目標文件形式

(1)可重定位目標文件:包含二進制代碼和數據,其形式可以和其他目標文件進行合併,創建一個可執行目標文件

(2)可執行目標文件:包含二進制代碼和數據,可直接被加載器加載執行

(3)共享目標文件:可被動態的加載和鏈接(本文暫時不討論)

由此我們可知由彙編器生成的就是可重定位目標文件,經過鏈接器作用後才生成可執行目標文件,鏈接器的作用就是以一組可重定位目標文件作爲輸入,生成可加載和運行的可執行目標文件,具體需要完成以下兩個工作:

Ø  符號解析:符號解析的目的是將目標文件中每個符號(靜態變量、函數、全局變量)和其定義進行關聯

Ø  重定位:將每個符號的定義與具體在虛擬內存中的位置進行關聯

最終生成可執行目標文件

說到這裏好像還是沒有說清楚這兩種目標文件有什麼區別,我們還是先把這個問題放一下,相信你看完下一節,應該會有答案,下面我們開始引入目標文件ELF文件。

三、ELF文件

目標文件再不同的系統或平臺上具有不同的命名格式,在Unix和X86-64 Linux上稱爲ELF(Executable and Linkable Format, ELF)。

ELF文件格式提供了兩種不同的視角,在彙編器和鏈接器看來,ELF文件是由Section Header Table描述的一系列Section的集合,而執行一個ELF文件時,在加載器(Loader)看來它是由Program Header Table描述的一系列Segment的集合


左邊是從彙編器和鏈接器的視角來看這個文件,開頭的ELF Header描述了體系結構和操作系統等基本信息,並指出Section Header Table和Program Header Table在文件中的什麼位置,Program Header Table在彙編和鏈接過程中沒有用到,所以是可有可無的,Section Header Table中保存了所有Section的描述信息。右邊是從加載器的視角來看這個文件,開頭是ELF Header,Program Header Table中保存了所有Segment的描述信息,Section Header Table在加載過程中沒有用到,所以是可有可無的。注意Section Header Table和Program Header Table並不是一定要位於文件開頭和結尾的,其位置由ELF Header指出,上圖這麼畫只是爲了清晰。


我們在彙編程序中用.section聲明的Section會成爲目標文件中的Section,此外匯編器還會自動添加一些Section(比如符號表)。Segment是指在程序運行時加載到內存的具有相同屬性的區域,由一個或多個Section組成,比如有兩個Section都要求加載到內存後可讀可寫,就屬於同一個Segment。有些Section只對彙編器和鏈接器有意義,在運行時用不到,也不需要加載到內存,那麼就不屬於任何Segment。


目標文件需要鏈接器做進一步處理,所以一定有Section Header Table;可執行文件需要加載運行,所以一定有Program Header Table;而共享庫既要加載運行,又要在加載時做動態鏈接,所以既有Section Header Table又有Program Header Table。

關於目標文件的具體節的數據結構,有興趣的讀者參照北大的一個資料寫的非常詳細

點擊打開鏈接


下面用readelf工具讀出目標文件max.o的ELF Header和Section Header Table,然後我們逐段分析。



接下來我們來看Section Header Table格式



從Section Header中讀出各Section的描述信息,其中.text.data是我們在彙編程序中聲明的Section,而其它Section是彙編器自動添加的。Addr是這些段加載到內存中的地址(我們講過程序中的地址都是虛擬地址),加載地址要在鏈接時填寫,現在空缺,所以是全0。OffSize兩列指出了各Section的文件地址,比如.data從文件地址0x60開始,一共0x38個字節,回去翻一下程序,.data中定義了14個4字節的整數,一共是56個字節,也就是0x38個。根據以上信息可以描繪出整個目標文件的佈局。

起始文件地址

SectionHeader

0

ELF Header

0x34

.text

0x60

.data

0x98

.bss(此段爲空)

0x98

.shstrtab

0xc8

Section Header Table

0x208

.symtab

0x288

.strtab

0x2b0

.rel.text

 

這個文件不大,我們直接用hexdump或者使用010 Editor工具把目標文件的字節全部打印出來看。



3.1 .shstrtab.strtab

.shstrtab.strtab這兩個Section中存放的都是ASCII碼:



可見.shstrtab中保存着各Section的名字,.strtab中保存着程序中用到的符號的名字。每個名字都是以'\0'結尾的字符串。

我們知道,C語言的全局變量如果在代碼中沒有初始化,就會在程序加載時用0初始化。這種數據屬於.bss段,在加載時它和.data段一樣都是可讀可寫的數據,但是在ELF文件中.data段需要佔用一部分空間保存初始值,而.bss段則不需要。也就是說,.bss段在文件中只佔一個Section Header而沒有對應的Section,程序加載時.bss段佔多大內存空間在Section Header中描述。在我們這個例子中沒有用到.bss段,以後我們會看到這樣的例子。


3.2.rel.text.symtab

我們繼續分析readelf輸出的最後一部分,是從.rel.text.symtab這兩個Section中讀出的信息。


.rel.text告訴鏈接器指令中的哪些地方需要重定位,我們在下一節討論。

.symtab是符號表。Ndx列是每個符號所在的Section編號,例如data_items在第3個Section裏(也就是.data),各Section的編號見Section Header Table。Value列是每個符號所代表的地址,在目標文件中,符號地址都是相對於該符號所在Section的相對地址,比如data_items位於.data段的開頭,所以地址是0,_start位於.text段的開頭,所以地址也是0,但是start_looploop_exit相對於.text段的地址就不是0了。從Bind這一列可以看出_start這個符號是GLOBAL的,而其它符號是LOCAL的,GLOBAL符號是在彙編程序中用.globl指示聲明過的符號。

3.3 .text節

通過使用objdump工具可以把程序中的機器指令進行反彙編(Disassemble),得到其彙編代碼




四、可執行文件

先看可執行文件header的變化




在看section header的變化



.text.data的加載地址分別改成了0x08048074和0x0804 90a0。.bss段沒有用到,所以被刪掉了。.rel.text段就是用於鏈接過程的,鏈接完了就沒用了,所以也刪掉了。

在看多出來的兩個program header




多出來的Program Header Table描述了兩個Segment的信息。.text段和前面的ELFHeader、Program Header Table一起組成一個Segment(FileSiz指出總長度是0x9e),.data段組成另一個Segment(總長度是0x38)。VirtAddr列指出第一個Segment加載到虛擬地址0x0804 8000(注意在x86平臺上後面的PhysAddr列是沒有意義的),第二個Segment加載到地址0x0804 90a0。Flg列指出第一個Segment的訪問權限是可讀可執行,第二個Segment的訪問權限是可讀可寫。最後一列Align的值0x1000(4K)是x86平臺的內存頁面大小。在加載時要求文件中的一頁對應內存中的一頁,對應關係如下圖所示。



這個可執行文件很小,總共也不超過一頁大小,但是兩個Segment必須加載到內存中兩個不同的頁面,因爲MMU的權限保護機制是以頁爲單位的,一個頁面只能設置一種權限。此外還規定每個Segment在文件頁面內偏移多少加載到內存頁面仍然偏移多少,比如第二個Segment在文件中的偏移是0xa0,在內存頁面0x0804 9000中的偏移仍然是0xa0,所以是從0x0804 90a0開始,這樣規定是爲了簡化鏈接器和加載器的實現。從上圖也可以看出.text段的加載地址應該是0x0804 8074,也正是_start符號的地址和程序的入口地址。

原來目標文件符號表中的Value都是相對地址,現在都改成絕對地址了。此外還多了三個符號__bss_start_edata_end,這些是在鏈接過程中添進去的,加載器可以利用這些信息把.bss段初始化爲0。

再看一下反彙編的結果:




到此爲止ELF文件的問題已介基本介紹,關於共享目標文件的格式和加載過程將在後續補上。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章