ELF文件結構
ELF文件結構
ELF文件的全稱是Executable and Linkable Format,直譯為「可執行可鏈接格式」,包括目標文件(.o)、可執行文件(可以直接運行)、靜態鏈接庫、動態鏈接庫、核心轉儲文件(core dump)。ELF文件的定義可以在/usr/include/elf.h
中找到,本文主要介紹ELF64,ELF文件通常由下列部分組成:
-
ELF頭(ELF header):放在ELF文件開頭,描述該文件信息。
-
節頭表(Section header table):包含對節(section)的描述,對於可重定位文件(relocatable files)是必須的,對於可裝載文件(loadable files)是可選的。
-
程序頭表(Program header table):對於可裝載文件(loadable files)是必須的,對於可重定位文件(relocatable files)是可選的。用來描述加載程序或動態鏈接庫所需要的段(segments)和其他數據結構。
-
節或段的內容,包括符號表等。
ELF頭
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf64_Half e_type; /* Object file type */
Elf64_Half e_machine; /* Architecture */
Elf64_Word e_version; /* Object file version */
Elf64_Addr e_entry; /* Entry point virtual address */
Elf64_Off e_phoff; /* Program header table file offset */
Elf64_Off e_shoff; /* Section header table file offset */
Elf64_Word e_flags; /* Processor-specific flags */
Elf64_Half e_ehsize; /* ELF header size in bytes */
Elf64_Half e_phentsize; /* Program header table entry size */
Elf64_Half e_phnum; /* Program header table entry count */
Elf64_Half e_shentsize; /* Section header table entry size */
Elf64_Half e_shnum; /* Section header table entry count */
Elf64_Half e_shstrndx; /* Section header string table index */
} Elf64_Ehdr;
Elf64_Ehdr中的數據結構含義如下:
數據結構名稱 | 大小(byte) | 對齊(byte) | 目標 |
---|---|---|---|
Elf64_Addr | 8 | 8 | Unsigned program address |
Elf64_Off | 8 | 8 | Unsigned file offset |
Elf64_Half | 2 | 2 | Unsigned medium integer |
Elf64_Word | 4 | 4 | Unsigned integer |
Elf64_Sword | 4 | 4 | Signed integer |
Elf64_Xword | 8 | 8 | Unsigned long integer |
Elf64_Sxword | 8 | 8 | Signed long integer |
unsigned char | 1 | 1 | Unsigned small integer |
我們用readelf -h hello.o
看一下從源文件到可執行文件:源文件的預處理、編譯、彙編、鏈接中生成的hello.o
文件的ELF頭(因為我機器上顯示的結果是中文,所以接下來就按照中文來說明,比如ELF頭中類別對應Class,類型對應Type)。
ELF 頭:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
類別: ELF64
數據: 2 補碼,小端序 (little endian)
Version: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
類型: REL (可重定位文件)
系統架構: Advanced Micro Devices X86-64
版本: 0x1
入口點地址: 0x0
程序頭起點: 0 (bytes into file)
Start of section headers: 864 (bytes into file)
標誌: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 14
Section header string table index: 13
ELF頭一開始的位置是魔術字符(Magic),在ASCII碼中,’E’、’L’、’F’分別對應45、4c、46。當文件被映射到內存中,可以通過魔術字符確定映射地址。 ELF頭與Elf64_Ehdr存在對應關係:
Elf64_Ehdr成員 | ELF頭 | 含義 |
---|---|---|
e_ident | Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 | |
e_type | 類型: REL (可重定位文件) | ELF文件類型(包括Relocatable/Executable/Shared/Core等類型) |
e_machine | 系統架構: Advanced Micro Devices X86-64 | |
e_version | 版本: 0x1 | 版本號,通常為0x1 |
e_entry | 入口點地址: 0x0 | 程序入口點的虛擬地址,操作系統在加載完程序後從該地址開始執行進程的指令。可重定位文件沒有入口地址,所以為0。用readelf命令查看前文生成的可執行文件可以看到入口地址 |
e_phoff | 程序頭起點:0 (bytes into file) | 程序頭表偏移(單位:byte) |
e_shoff | Start of section headers: 864 (bytes into file) | 節頭表偏移(單位:byte) |
e_flags | 標誌:0x0 | 特定於處理器的標識 |
e_ehsize | Size of this header: 64 (bytes) | ELF頭本身的大小(單位:byte) |
e_phentsize | Size of program headers: 0 (bytes) | 程序頭大小(單位:byte) |
e_phnum | Number of program headers: 0 | 程序頭個數 |
e_shentsize | Size of section headers: 64 (bytes) | 節頭大小(單位:byte) |
e_shnum | Number of section headers: 14 | 節頭個數 |
e_shstrndx | Section header string table index: 13 | 字符串表在節頭表中索引 |
對於魔數字符,再展開介紹一下。Magic共16個位元組(Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00),第0~3個位元組標識文件;第4個位元組標識文件類別(類別: ELF64),1表示32位,2標識64位;第5個位元組標識文件數據編碼(數據: 2 補碼,小端序 (little endian)),1標識小端序,2表示大端序;第6個位元組標識文件版本(Version: 1 (current)),值為1;第7個位元組標識操作系統和ABI(OS/ABI: UNIX – System V),0表示System V ABI,1表示HP-UX operating system,255表示Standalone (embedded) application;第8個位元組標識ABI版本(ABI 版本: 0),值為1;剩餘位元組被保留為將來使用,設置為0。
節頭表
一個目標文件(包括Relocatable/Executable/Shared/Core等類型)中包含很多節,這些節的信息保存在節頭表中,表的每一項都是一個Elf64_Shdr結構體(也稱為節描述符),節點信息包括節名、節大小、在文件中的偏移、讀寫權限等,編譯器、鏈接器、裝載器都是通過節頭表來定位和訪問各個節的屬性的。/usr/include/elf.h
中的Elf64_Shdr內容如下:
typedef struct
{
Elf64_Word sh_name; /* Section name (string tbl index) */
Elf64_Word sh_type; /* Section type */
Elf64_Xword sh_flags; /* Section flags */
Elf64_Addr sh_addr; /* Section virtual addr at execution */
Elf64_Off sh_offset; /* Section file offset */
Elf64_Xword sh_size; /* Section size in bytes */
Elf64_Word sh_link; /* Link to another section */
Elf64_Word sh_info; /* Additional section information */
Elf64_Xword sh_addralign; /* Section alignment */
Elf64_Xword sh_entsize; /* Entry size if section holds table */
} Elf64_Shdr;
通過readelf -S hello.o
命令查看hello.o文件的節頭表。
There are 14 section headers, starting at offset 0x360:
節頭:
[號] 名稱 類型 地址 偏移量
大小 全體大小 旗標 鏈接 信息 對齊
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
0000000000000027 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 00000270
0000000000000060 0000000000000018 I 11 1 8
[ 3] .data PROGBITS 0000000000000000 00000067
0000000000000000 0000000000000000 WA 0 0 1
[ 4] .bss NOBITS 0000000000000000 00000067
0000000000000000 0000000000000000 WA 0 0 1
[ 5] .rodata PROGBITS 0000000000000000 00000067
0000000000000019 0000000000000000 A 0 0 1
[ 6] .comment PROGBITS 0000000000000000 00000080
000000000000002c 0000000000000001 MS 0 0 1
[ 7] .note.GNU-stack PROGBITS 0000000000000000 000000ac
0000000000000000 0000000000000000 0 0 1
[ 8] .note.gnu.propert NOTE 0000000000000000 000000b0
0000000000000020 0000000000000000 A 0 0 8
[ 9] .eh_frame PROGBITS 0000000000000000 000000d0
0000000000000038 0000000000000000 A 0 0 8
[10] .rela.eh_frame RELA 0000000000000000 000002d0
0000000000000018 0000000000000018 I 11 9 8
[11] .symtab SYMTAB 0000000000000000 00000108
0000000000000138 0000000000000018 12 10 8
[12] .strtab STRTAB 0000000000000000 00000240
0000000000000029 0000000000000000 0 0 1
[13] .shstrtab STRTAB 0000000000000000 000002e8
0000000000000074 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
正如ELF頭中內容所示,hello.o中包含14個節。節名(sh_name)是一個單位為位元組的偏移量,表示相對於節名字符串表(section name string table,也就是.shstrtab)起點的偏移,節名實際存放在節名字符串表中,通過查表得到。節類型(sh_type)分為以下幾類,readelf命令的結果省略了前綴SHT_。
節類型 | 含義 |
---|---|
SHT_NULL | 無效節 |
SHT_PROGBITS | 程序節。代碼節、數據節都是這種類型 |
SHT_SYMTAB | 符號表 |
SHT_STRTAB | 字符串表 |
SHT_RELA | 重定位表 |
SHT_HASH | 符號表的哈希表 |
SHT_DYNAMIC | 動態鏈接信息 |
SHT_NOTE | 提示性信息 |
SHT_NOBITS | 表示該節在文件中沒有內容,不佔用空間 |
SHT_REL | 重定位信息 |
SHT_SHLIB | 保留 |
SHT_DYNSYM | 動態鏈接的符號表 |
SHT_LOOS/SHT_HIOS | 特定環境使用 |
SHT_LOPROC/SHT_HIPROC | 特定處理器使用 |
節標誌位(sh_flags)表示該節在進程虛擬地址空間中的屬性,1表示可寫,2表示該節在進程空間中需要分配空間,有些包含指示或者控制信息的節不需要在進程分配空間,就沒有這個標誌,4表示該節在進程空間中可以被執行。
節鏈接信息(sh_link、sh_info),如果節的類型是與鏈接相關的(無論是動態鏈接還是靜態鏈接),如重定位表、符號表等,則sh_link、sh_info兩個成員所包含的意義如下所示。其他類型的節,這兩個成員沒有意義。
sh_type | sh_link | sh_info |
---|---|---|
SHT_DYNAMIC | 該節所使用的字符串表在節頭表中的下標 | 0 |
SHT_HASH | 該節所使用的符號表在節頭表中的下標 | 0 |
SHT_REL | 該節所使用的相應符號表在節頭表中的下標 | 該重定位表所作用的節在節頭表中的下標 |
SHT_RELA | 該節所使用的相應符號表在節頭表中的下標 | 該重定位表所作用的節在節頭表中的下標 |
SHT_SYMTAB | 操作系統相關 | 操作系統相關 |
SHT_DYNSYM | 操作系統相關 | 操作系統相關 |
other | SHN_UNDEF | 0 |
重要的節
-
.text節
.text節是保存了程序代碼指令的代碼節。一段可執行程序,如果存在Phdr,則.text節就會存在於text段中。由於.text節保存了程序代碼,所以節類型為SHT_PROGBITS。 -
.rodata節
rodata節保存了只讀的數據,如一行C語言代碼中的字符串。由於.rodata節是只讀的,所以只能存在於一個可執行文件的只讀段中。因此,只能在text段(不是data段)中找到.rodata節。由於.rodata節是只讀的,所以節類型為SHT_PROGBITS。 -
.plt節(過程鏈接表)
.plt節也稱為過程鏈接表(Procedure Linkage Table),其包含了動態鏈接器調用從共享庫導入的函數所必需的相關代碼。由於.plt節保存了代碼,所以節類型為SHT_PROGBITS。 -
.data節
.data節存在於data段中,其保存了初始化的全局變量和局部靜態變量等數據。由於.data節保存了程序的變量數據,所以節類型為SHT_PROGBITS。 -
.bss節
.bss節存在於data段中,佔用空間不超過4位元組,僅表示這個節本身的空間。.bss節保存了未進行初始化的全局數據和局部靜態變量,程序加載時數據被初始化為0,在程序執行期間可以進行賦值。由於.bss節未保存實際的數據,所以節類型為SHT_NOBITS。 -
.got.plt節(全局偏移表-過程鏈接表)
.got節保存了全局偏移表。.got節和.plt節一起提供了對導入的共享庫函數的訪問入口,由動態鏈接器在運行時進行修改。由於.got.plt節與程序執行有關,所以節類型為SHT_PROGBITS。 -
.dynsym節(動態鏈接符號表)
.dynsym節保存在text段中。其保存了從共享庫導入的動態符號表。節類型為SHT_DYNSYM。 -
.dynstr節(動態鏈接字符串表)
.dynstr保存了動態鏈接字符串表,表中存放了一系列字符串,這些字符串代表了符號名稱,以空字符作為終止符。 -
.rel.*節(重定位表)
重定位表保存了重定位相關的信息,這些信息描述了如何在鏈接或運行時,對ELF目標文件的某部分或者進程鏡像進行補充或修改。由於重定位表保存了重定位相關的數據,所以節類型為SHT_REL。 -
.hash節
.hash節也稱為.gnu.hash,其保存了一個用於查找符號的散列表。 -
.symtab節(符號表)
.symtab節是一個ElfN_Sym的數組,保存了符號信息。節類型為SHT_SYMTAB。 -
.strtab節(字符串表)
.strtab節保存的是符號字符串表,表中的內容會被.symtab的ElfN_Sym結構中的st_name引用。節類型為SHT_STRTAB。 -
.ctors節和.dtors節
.ctors(構造器)節和.dtors(析構器)節分別保存了指向構造函數和析構函數的函數指針,構造函數是在main函數執行之前需要執行的代碼;析構函數是在main函數之後需要執行的代碼。
符號表包括.dynsym
和.symtab
,前者是後者的子集。.dynsym
保存了引用自外部文件的符號,只能在運行時被解析(flag為Alloc),而.symtab
還保存了本地符號,用於調試和鏈接,不會被裝載到內存中。
程序頭表
在/usr/include/elf.h
,程序頭表的結構如下:
typedef struct
{
Elf64_Word p_type; /* Segment type */
Elf64_Word p_flags; /* Segment flags */
Elf64_Off p_offset; /* Segment file offset */
Elf64_Addr p_vaddr; /* Segment virtual address */
Elf64_Addr p_paddr; /* Segment physical address */
Elf64_Xword p_filesz; /* Segment size in file */
Elf64_Xword p_memsz; /* Segment size in memory */
Elf64_Xword p_align; /* Segment alignment */
} Elf64_Phdr;
由於hello.o沒有程序頭表,所以通過readelf -l hello
來讀取可執行文件hello的程序頭表,如下所示。
Elf 文件類型為 DYN (共享目標文件)
Entry point 0x1060
There are 13 program headers, starting at offset 64
程序頭:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040
0x00000000000002d8 0x00000000000002d8 R 0x8
INTERP 0x0000000000000318 0x0000000000000318 0x0000000000000318
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x00000000000005f8 0x00000000000005f8 R 0x1000
LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000
0x00000000000001f5 0x00000000000001f5 R E 0x1000
LOAD 0x0000000000002000 0x0000000000002000 0x0000000000002000
0x0000000000000170 0x0000000000000170 R 0x1000
LOAD 0x0000000000002db8 0x0000000000003db8 0x0000000000003db8
0x0000000000000258 0x0000000000000260 RW 0x1000
DYNAMIC 0x0000000000002dc8 0x0000000000003dc8 0x0000000000003dc8
0x00000000000001f0 0x00000000000001f0 RW 0x8
NOTE 0x0000000000000338 0x0000000000000338 0x0000000000000338
0x0000000000000020 0x0000000000000020 R 0x8
NOTE 0x0000000000000358 0x0000000000000358 0x0000000000000358
0x0000000000000044 0x0000000000000044 R 0x4
GNU_PROPERTY 0x0000000000000338 0x0000000000000338 0x0000000000000338
0x0000000000000020 0x0000000000000020 R 0x8
GNU_EH_FRAME 0x0000000000002020 0x0000000000002020 0x0000000000002020
0x0000000000000044 0x0000000000000044 R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x0000000000002db8 0x0000000000003db8 0x0000000000003db8
0x0000000000000248 0x0000000000000248 R 0x1
Section to Segment mapping:
段節...
00
01 .interp
02 .interp .note.gnu.property .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt
03 .init .plt .plt.got .plt.sec .text .fini
04 .rodata .eh_frame_hdr .eh_frame
05 .init_array .fini_array .dynamic .got .data .bss
06 .dynamic
07 .note.gnu.property
08 .note.gnu.build-id .note.ABI-tag
09 .note.gnu.property
10 .eh_frame_hdr
11
12 .init_array .fini_array .dynamic .got
與節類似,段也有幾種不同的類型(p_type),如下:
段類型 | 含義 |
---|---|
PT_NULL | 無效段 |
PT_LOAD | 可加載段 |
PT_DYNAMIC | 動態鏈接表 |
PT_INTERP | 程序解釋器路徑名 |
PT_NOTE | 信息段 |
PT_SHLIB | 保留 |
PT_PHDR | 程序頭表 |
PT_LOOS / PT_HIOS | 特定環境使用 |
PT_LOPROC / PT_HIPROC | 特定處理機使用 |
段標誌位(p_flags)表示權限,X表示執行允許,W表示寫允許,R表示讀允許。
最後再說一下節(section)和(segment)的關係。每個段包括一個或多個節,因為系統不關心這些節的具體內容,只關心這些節的權限(讀、寫、執行),將具有相同權限的節放到同一個段中。節是鏈接視角下的ELF文件,段是運行視角下的ELF文件。
參考資料
ELF-64 Object File Format
計算機那些事(4)—— ELF文件結構
CTF競賽權威指南(Pwn篇)(楊超 編著,吳石 eee戰隊 審校,電子工業出版社)