1. 程式人生 > 其它 >ELF檔案結構

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