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

Tags: