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戰隊 審校,電子工業出版社)