ELF文件格式學習總結
1. 概述
2. 目標文件結構
3. ELF文件頭
3.1 魔數
3.2 文件類型
3.3 機器類型
4. ELF文件內容
4.1段表
4.2字符串表(.**strtab)
4.3符號表
4.4重定位段(.rel.***)
1. 概述
ELF文件全稱是Executable Linking Format(可執行連接格式),最初由unix系統實驗室發布,它是應用程序二進制接口(Application Binary Interface,ABI)的一部分。在linux下,它是可執行文件的格式,由System V Release 4在COFF的基礎上引入的,與之相對應的就是大家所熟悉的windows下的exe文件,exe文件是由microsoft基於COFF格式改變而成。
在Linux下使用ELF格式作為文件格式的主要有以下4類:
- 1.可重定位文件(Relocatable File),這類文件包含了代碼和數據,可以被用來鏈接成可執行文件或共享目標文件,靜態鏈接庫也可以歸為這一類,如經過預編譯、編譯、匯編后的.o文件,具體如圖1所示。
圖1
- 2.可執行文件(Executable File),這類文件包含了直接執行的程序,如/bin/bash等,具體如圖2所示。
圖2
- 3.共享目標文件(Shared Object File),鏈接器可以使用這種文件跟其他的可重定位文件和共享目標文件鏈接,產生新的目標文件;動態鏈接器可以將幾個共享目標文件與可執行文件結合,作為進程映像的一部分來運行,如/lib/libc-2.15.so,具體如圖3是所示。
圖3
- 4.核心轉儲文件(Core Dump File),當進程意外終止時,系統可以將該進程的地址空間內容及終止時的一些其他信息轉儲到核心轉儲文件,如linux下的core dump文件。
在后面的總結中,主要是以目標文件(.o文件)為列子來對elf文件格式進行分析(其他的3種文件格式和目標文件格式差不多)。
2. 目標文件結構
目標文件,使用命令gcc從C源碼文件開始,經過預編譯、編譯和匯編直接輸出的文件,它和可執行文件的內容和結構十分相似。在目標文件中主要有編譯后的機器指令代碼、數據和鏈接時所需要的一些信息,具體如圖4所示。
圖4
從圖4中可以看到目標文件其實可以分成2個部分。
- 其一,是ELF文件頭,在該”文件頭”中主要包括整個文件的文件屬性、文件是否是可執行、是靜態鏈接or動態鏈接及入口地址(主要對應程序main方法)、目標硬件、目標操作系統等。從文件頭中還可以得到段表和段表字符串表的位置,通過它們可以解析整個ELF文件。
- 其二,是ELF內容部分,在文件頭后面的部分都是屬於ELF內容部分,它主要以”段”(section)的形式存儲,比如保存程序指令的代碼段(code section),代碼段常見的名字有”.code”或”.text”;保存程序靜態變量的數據段(data section),數據段的一般名字都叫”.data”。以C語言為例,執行語句進過編譯后的機器代碼保存在.test段;已經初始化的全局變量和局部靜態變量都保存在.data段;而未初始化的全局變量和局部變量放在.bss段里,具體如圖5所示。
圖片5
3. ELF文件頭
ELF文件頭主要通過結構體Elf32_Ehdr(32位版)來定義的,在文件/usr/include/elf.h中,我們可以看到該結構體的詳細定義,具體如圖6所示。
圖6
以32位版為例,在該結構體中自定義類型如表7所示。
字段名 | 描述 | 原始類型 | 字節 |
---|---|---|---|
Elf32_Addr | 本程序地址 | unit32_t | 4 |
Elf32_Half | 無符號短整型 | unit16_t | 2 |
Elf32_Off | 偏移地址 | unit32_t | 4 |
Elf32_Sword | 有符號整型 | unit32_t | 4 |
Elf32_Word | 無符號整型 | unit32_t | 4 |
通過readelf命令來對test.c文件編譯出的test.o目標文件的ELF文件頭進行查看,其結果如圖8所示。
在圖8中的各個參數項與ELF文件頭結構體中的字段對應關系具體如表9所示。
字段 | 描述 | 對應圖8中的位置 |
---|---|---|
e_ident | 包含5個參數,分別是class、data、version、os/ABI、ABI version | 0 |
e_type | Elf文件類型 | 1 |
e_machine | Elf文件的CPU平台 | 2 |
e_version | Elf版本號 | 3 |
e_entry | 程序的入口 | 4 |
e_phoff | 5 | |
e_shoff | 段表在文件中的偏移位置 | 6 |
e_word | Elf標志位 | 7 |
e_ehsize | Elf文件頭本身大小,值為64byte表示test.o的文件頭有64字節 | 8 |
e_phentsize | 9 | |
e_phnum | 10 | |
e_shentsize | 段表描述符的大小,即為 | 11 |
e_shnum | 段表描述符的數量,值為12表示test目標文件中有12個段 | 12 |
shstrndx | 段表字符串表所在段表數組的下標,值為9表示段表字符串表位於第9個段 | 13 |
3.1 魔數
在最前面的一項magic中的16個字節用來規定ELF文件的平台,最開始的4個字節是所有ELF文件都相同的標示碼,分別為7f、45、4c、46。它們分別對應ascii碼表中的delete、E、L、F字符,如圖參考表10。
表10(ascii碼表部分截圖)
二進制 | 進制 | 進制 | 進制 | ||
---|---|---|---|---|---|
01000101 | 105 | 69 | 45 | E | 大寫字母E |
01000110 | 106 | 70 | 46 | F | 大寫字母F |
01000111 | 107 | 71 | 47 | G | 大寫字母G |
01001000 | 110 | 72 | 48 | H | 大寫字母H |
01001001 | 111 | 73 | 49 | I | 大寫字母I |
01001010 | 112 | 74 | 4A | J | 大寫字母J |
01001011 | 113 | 75 | 4B | K | 大寫字母K |
01001100 | 114 | 76 | 4C | L | 大寫字母L |
01111111 | 177 | 127 | 7F | DEL | (delete)刪除 |
接下來的一個字節標示ELF文件類型,01表示32位的,02表示64位的,從圖8中可以知道test.o為64位ELF文件類型。之后的一個字節表示是大端(big-endian)還是小端(little-endian),第7字節表示ELF文件主版本號,再后面幾個字節無實際意義,默認填0。
3.2 文件類型
文件類型對應ELF文件頭結構體中的e_type字段,該字段表示ELF文件類型,系統通過它來判斷ELF文件的真正文件類型,其對應關系如表11所示。
常量 | 值 | 描述 |
---|---|---|
ET_REL | 1 | 可重定位文件,如.o目標文件 |
ET_EXEC | 2 | 可執行文件,如/bin/bash |
ET_DYN | 3 | 共享目標文件,如/lib/libc-2.15.so文件 |
3.3 機器類型
機器類型對應ELF文件頭結構體中的e_machine字段,ELF文件被設計成可以在多個平台下使用,但是同一個ELF文件只能在制定的平台下使用。如圖8中的e_machine的值為x86-64表示test.o文件只能在x86-64機器上是使用。
4. ELF文件內容
在ELF文件頭之后的部分都屬於ELF內容部分,在該部分主要包含的是機器指令,程序源碼經過編譯后生成的機器指令。它們主要以段來進行區分,如代碼段進過編譯后的機器指令存儲在.code段或.text段中,全局變量個局部靜態變量編譯后的機器指令存放.data段中,各個段在ELF文件中的結構如圖11所示。
而在ELF文件中具體有哪些段,各個段的具體結構是怎么組織的則是由段表(section table)決定的,通過段表可以找到各個段的具體位置、段的大小、讀寫權限等很多信息,反正ELF文件中的段的結構都是有段表決定給的,因此段表是ELF文件內容中非常重要的一部分。
4.1段表
段表是一個以Elf32_Shdr/Elf64_Shdr結構體(結構體具體定義見圖12)為元素的數組,即簡單的可以把它看成一個數組,數組的長度就表示在該ELf文件中有多少個段,如圖8中number of section headers為12,那么Elf64_Shdr結構體為元素的數組長度為12,表示test.o目標文件中有12段;size of section headers為64byte,那么就表示Elf64_Shdr結構體的大小為64byte;start of section headers為378byte,表示段表在test.o文件中的偏移位置,即從文件中第378byte開始,之后的768byte(12*64byte)內容都為段表內容,通過該偏移位置ELF文件頭就可以准確的找到段表位置。從另一個角度看段表也是ELf文件中的一個段,只不過這個段與前面提到的.text段或者.data段不同,它並沒有包含程序運行的機器指令,它里面的內容主要是用來描述其他段的結構,通過命令readelf -S來查看test.o目標文件的段表結構如圖13所示。
從圖13中的第二行可以看到,Starting at offset 0x178即表示段表的位置是從178為開始的,注意這是16進制的178,換成10進制剛好就等於文件頭記錄的段表偏移位置378。而且從圖13中可以看到該段表從0到11共12個Elf64_Shdr類型數組元素,其中數組除了第一個元素為無效的段描述符(它的類型為NULL),其他的都表示一個段,通過圖13的描述,test.o目標文件中各個段的結構示意圖如圖14所示。
從圖14中可以看到,該test.o文件的各個段的具體結構及段的大小,在圖14中2個綠色的部分是系統為了字節對齊而產生的間隔。而在段表中有部分段的size為0,所以在圖中並沒有畫出來,結束段.rela.en_frame的結束位置為0x00000690,即為1680字節,剛好是目標文件test.o的大小,具體如圖15所示。
結構體Elf64_Shdr各個字段的含義如下:
- 字段sh_name
sh_name表示段名,各個段的實際段名字符串存儲在.shstrtab段中,sh_name存的是在.shstrtab中的偏移。 - 字段sh_type
sh_type表示段的類型,對於編譯器和鏈接器來說,一個段的到底是什么類型是由字段sh_type決定的,具體對應關系如表16所示。
常量 | 值 | 描述 |
---|---|---|
SHT_NULL | 0 | 無效段 |
SHT_PROGBITS | 1 | 程序段,代碼段和數據段都是屬於程序段 |
SHT_SYMTAB | 2 | 表示該段的內容為符號表 |
SHT_STRTAB | 3 | 表示該段的內容為字符串表 |
SHT_RELA | 4 | 重定位表 |
SHT_HASH | 5 | 符號表 |
SHT_DYNAMIC | 6 | 動態鏈接信息 |
SHT_NOTE | 7 | 提示性信息 |
SHT_NOBITS | 8 | 表示該段在文件中沒有內容 |
SHT_REL | 9 | 表示該段包含重定位信息 |
SHT_SHLIB | 10 | 保留 |
SHT_DNYSYM | 11 | 動態鏈接的符號表 |
- 字段sh_flags
sh_flags表示段的標志位, - 字段sh_addr
sh_addr表示Section address段虛擬地址 - 字段sh_offset
sh_offset表示段在文件中的偏移位置 - 字段sh_size
sh_size表示段的大小 - 字段sh_link和sh_info
sh_link和sh_info和段的鏈接信息相關 - 字段sh_addralign
sh_addralign表示段地址對齊 - 字段sh_ensize
sh_ensize表示Section entry size項的長度
4.2字符串表(.**strtab
)
在ELF文件中用到的字段都集中的放在各個字段表中(命名為.**strtab
),字段表實際上也是一個段,里面存儲的都是段名、變量名等,通過使用偏移位來定位需要使用的字符。例如表17中字符串。
偏移 | +0 | +1 | +2 | +3 | +4 | +5 | +6 | +7 | +8 | +9 |
---|---|---|---|---|---|---|---|---|---|---|
+0 | \0 | h | e | l | l | o | w | o | r | l |
+10 | d | \0 | m | y | v | a | r | i | a | b |
+20 | l | e | \0 |
在目標文件test.o中,.shstrtab段(段表字符串表)內容如圖19所示,從118到171都為段表字符串表,段表字符串表中16進制數據對應ascii碼如圖中右邊部分。
目標文件test.o中,段表部分內容如圖20所示,如第一個段描述(NULL段的描述)位於0x00000178~0x000001b7,該部分內容全為0,而第二個段描述(.text段的描述)位於0x000001b8~0x000001f7,而在Elf64_Shdr結構體中的第一個字段為Elf64_word sh_name占用32bit,對應圖20中的值為20 00,高位在后低位在前則實際值為32。在圖19中查找偏移量為32的字符串則為.test(從118對應的字符串開始為第0個偏移量),剛好是第二個段的段名,其他的段名以此類推。
4.3符號表
源代碼進過編譯后,各個變量或者函數的機器指令都存在在不同的段中,通過多個目標文件進行鏈接就把多個目標文件拼在一起,其實質是目標之間的對地址的引用。如目標文件A中定義一個變量(或函數)var,目標文件B引用了該變量(或函數)var,為了避免鏈接過程中函數和變量之間的混淆,把函數和變量都統稱為符號,函數名和變量名稱為符號名,而整個鏈接過程都是基於這些符號進行的,所以目標文件中有一個相應的符號表。
符號表實際上是一個以結構體Elf32_Sym為元素的數組,同時它也是目標文件中的一個段(對應.symtab段),結構體Elf32_Sym的具體定義如圖21所示。
各個字段的含義如表22所示。
字段 | 描述 |
---|---|
st_name | 符號名,實質是對應在字符表中的偏移量 |
st_value | 符號對應的偏移 |
st_size | 符號的大小 |
st_info | 該字段表示2個屬性,符號類型和綁定信息 |
st_other | 未使用,默認為0 |
st_shndx | 符號所在的段位 |
通過命令readelf -s test.o查看到目標文件test.o的符號表(.symtab段)的描述如圖23所示。
從圖中看到有7個屬性(value、size、type、bind、vis、bdx、name)剛好與結構體Elf32_Sym中的6個字段相對應(字段st_info對應type、bind這2個屬性),總共14項及表示符號表對應的Elf32_Sym數組長度為14,而num為0的項表示是一個無效項。
- name屬性對應st_name表示該符號的名字,它實質是一個字符表中的偏移量。
- Ndx屬性對應st_shndx表示該符號的所在的段號,如num為13的項對應的段號為1即屬於.text段。
- type和Bing屬性對應st_info分別表示符號的類型和綁定信息,st_info的高28為表示綁定信息,低4為表示符號類型,其對應關系如表24所示。Num為12的項,對應的類型為STT_FUNC綁定信息為STB_GLOBAL即表示全局可見的函數。
符號綁定信息 | ||
---|---|---|
宏定義 | 值 | 說明 |
STB_LOCAL | 0 | 局部變量 |
STB_GLOBAL | 1 | 全局變量 |
STB_WEAK | 2 | 弱引用 |
符號類型 | ||
宏定義 | 值 | 說明 |
STT_NOTYPE | 0 | 表示未知類型符號 |
STT_OBJECT | 1 | 表示變量或數組 |
STT_FUNC | 2 | 表示函數或者其他可執行代碼 |
STT_SECTION | 3 | 表示為文件中某個段 |
STT_FILE | 4 | 表示是文件名 |
- value屬性和size屬性分別對應st_value和st_size表示符號所在段中的偏移和符號的大小,如num為10的項,大小為4byte即為一個int型。
4.4重定位段(.rel.***
)
對於目標文件來說,它需要包含一個重定位表用來描述如何修改相應的段內容,該重定位表就是文件中的一個段,也叫重定位段。比如代碼段.text有要重定位的地放,那么該文件中就會有一個名為.rel.text的段來保存重定位表。同理.data段對應的重定位表就位於.rel.text段(如果.rel.text段存在)。重定位表的結構實際上是一個以Elf32_Rel/Elf64_Rel結構體為元素的數組,數組中每個元素定義一個重定位入口。Elf32_Rel/Elf64_Rel結構定義如圖25所示
其中各個字段的含義:
- 字段r_offset
字段r_offset表示offset屬性即重定位入口的偏移 - 字段r_info
字段r_info表示2個屬性,即type屬性和name屬性,分別表示重定位入口的類型和重定位入口的符號(實質是在符號表的偏移量)。
通過命令查看目標文件test.o的重定位表結構如圖26所示。