1. 程式人生 > 實用技巧 >ELF檔案格式解析

ELF檔案格式解析

目錄

資料型別

首先在解析之前, 必須對資料型別格式宣告一下

名稱 大小 說明
Elf32_Addr 4 無符號程式地址
Elf32_Half 2 無符號中等整數
Elf32_Off 4 無符號檔案偏移
Elf32_SWord 4 有符號大整數
Elf32_Word 4 無符號大整數
unsigned char 1 無符號笑整數

整體結構

概述

  • ELF頭部(ELF_Header): 每個ELF檔案都必須存在一個ELF_Header,這裡存放了很多重要的資訊用來描述整個檔案的組織,如: 版本資訊,入口資訊,偏移資訊等。程式執行也必須依靠其提供的資訊。
  • 程式頭部表(Program_Header_Table): 可選的一個表,用於告訴系統如何在記憶體中建立映像,在圖中也可以看出來,有程式頭部表才有段,有段就必須有程式頭部表。其中存放各個段的基本資訊(包括地址指標)。
  • 節區頭部表(Section_Header_Table): 類似與Program_Header_Table,但與其相對應的是節區(Section)。
  • 節區(Section): 將檔案分成一個個節區,每個節區都有其對應的功能,如符號表,雜湊表等。
  • 段(Segment): 嗯…就是將檔案分成一段一段對映到記憶體中。段中通常包括一個或多個節區

*注:每個節區都應該是前後相連的,且不可有重疊。即在一個地址上的位元組只能屬於一個節區*

詳解

ELF_Header

以下是ELF_Header的結構定義

typedef struct {
Elf32_Half e_type;      //Elf檔案型別 
Elf32_Half e_machine;	//ELF檔案的CPU平臺屬性 
Elf32_Word e_version;	//ELF版本資訊 
Elf32_Addr e_entry;	//入口地址 
Elf32_Off e_phoff;	//程式頭表的檔案偏移(以位元組為單位)。如果檔案沒有程式頭表,則此成員值為零。 
Elf32_Off e_shoff;	//節頭表的檔案偏移(以位元組為單位)。如果檔案沒有節頭表,則此成員值為零。 
Elf32_Word e_flags;	//與檔案關聯的特定於處理器的標誌。標誌名稱採用 EF_machine_flag 形式。 
Elf32_Half e_ehsize;	//ELF 頭的大小(以位元組為單位) 
Elf32_Half e_phentsize;	//檔案的程式頭表中某一項的大小(以位元組為單位)。所有項的大小都相同 
Elf32_Half e_phnum;	//程式頭表中的項數。 
Elf32_Half e_shentsize;	//節頭的大小(以位元組為單位)。節頭是節頭表中的一項。所有項的大小都相同 
Elf32_Half e_shnum;	//節頭表中的項數 
Elf32_Half e_shstrndx;	//與節名稱字串表關聯的項的節頭表索引。
} Elf32_Ehdr;

然後來逐一解釋下各個欄位:

  • e_ident

    這是一個數組,其每個位元組又都有所代表的含義:

    • EI_MAG0 - EI_MAG3 檔案標識就是平時所說的ELF頭,即 7F 45 4C 46(ELF)

    • EI_CLASS 檔案類,其實代表的是32位/64位程式

      取值 代表 含義
      01 ELFCLASS32 32位程式
      02 ELFCLASS64 64位程式
    • EI_DATA 資料編碼,一般都是01[td]

      取值 代表 含義
      01 ELFDATA2LSB 高位在前
      02 ELFDATA2MSB 低位在前
    • EI_VERSION 檔案版本,固定值01 EV_CURRENT

    • EI_PAD 呃…就是一堆全是00的用來補全大小的陣列

    • EI_NIDENT 說是e_ident陣列的大小,但我看了好幾個so都是00

  • e_type 標識檔案型別

    取值 代表 含義
    00 ET_NONE 未知檔案型別格式
    01 ET_REL 可重定位檔案
    02 ET_EXEC 可執行檔案
    03 ET_DYN 共享目標檔案(SO)
    04 ... ...
  • e_machine 宣告ABI

    取值 代表 含義
    01 ... ...
    03 EM_386 X86
    04 ... ...
    28h EM_ARM arm
    29h ... ...
  • e_version 跟ident[]裡的EI_VERSION一樣,為01

  • e_entry 可執行程式入口點地址。

  • e_phoff Program Header Offset,程式頭部表索引地址,沒有則為0。

  • e.shoff Section Header Offset,節區表索引地址,沒有則為0。

  • e_flags “儲存與檔案相關的,特定於處理器的標誌。”(不知道有什麼用,看了幾個arm都是00 00 00 05,x86都是0)。

  • e_ehsize ELF_Header Size,嗯..ELF頭部的大小

  • e_phentsize 程式頭部表的單個表項的大小

  • e_phnum 程式頭部表的表項數

  • e_shentsize 節區表的單個表項的大小

  • e_shnum 節區表的表項數

  • e_shstrndx String Table Index,在節區表中有一個儲存各節區名稱的節區(通常是最後一個),這裡表示名稱表在第幾個節區。

Program Header

在ELF_Header中,我們可以得到Program Header索引地址(e_phoff)段數量(e_phnum)表項大小(e_phentsize)
然後我們來看一下Program Header中表項的結構定義:

typedef struct {
    Elf32_Word p_type;
    Elf32_Off p_offset;
    Elf32_Addr p_vaddr;
    Elf32_Addr p_paddr;
    Elf32_Word p_filesz;
    Elf32_Word p_memsz;
    Elf32_Word p_flage;
    Elf32_Word p_align;
}
Elf32_phdr;
  • p_type 宣告此段的作用型別

    取值 代表 含義
    00 PT_NULL 此陣列元素未用。結構中其他成員都是未定義的
    01 PT_LOAD 此陣列元素給出一個可載入的段,段的大小由 p_filesz 和 p_memsz 描述。檔案中的位元組被對映到記憶體段開始處。如果 p_memsz 大於 p_filesz,“剩餘”的位元組要清零。p_filesz 不能大於 p_memsz。可載入的段在程式頭部表格中根據 p_vaddr 成員按升序排列
    02 PT_DYNAMIC 陣列元素給出動態連結資訊
    03 PT_INTERP 陣列元素給出一個 NULL 結尾的字串的位置和長度,該字串將被當作直譯器呼叫。這種段型別僅對與可執行檔案有意義(儘管也可能在共享目標檔案上發生)。在一個檔案中不能出現一次以上。如果存在這種型別的段,它必須在所有可載入段專案的前面
    04 PT_NOTE 此陣列元素給出附加資訊的位置和大小
    05 PT_SHLIB 此段型別被保留,不過語義未指定。包含這種型別的段的程式與 ABI不符
    06 PT_PHDR 此型別的陣列元素如果存在,則給出了程式頭部表自身的大小和位置,既包括在檔案中也包括在記憶體中的資訊。此型別的段在檔案中不能出現一次以上。並且只有程式頭部表是程式的記憶體映像的一部分時才起作用。如果存在此型別段,則必須在所有可載入段專案的前面
    0x70000000 PT_LOPROC 此範圍的型別保留給處理器專用語義
    0x7fffffff PT_HIPROC 此範圍的型別保留給處理器專用語義
    ... ... ...

    還有一些編譯器或者處理器標識的段型別,有待補充。

  • p_offset 段相對於檔案的索引地址

  • p_vaddr 段在記憶體中的虛擬地址

  • p_paddr 段的實體地址

  • p_filesz 段在檔案中所佔的長度

  • p_memsz 段在記憶體中所佔的長度

  • p_flage 段相關標誌(read、write、exec)

  • p_align 位元組對其,p_vaddr 和 p_offset 對 p_align 取模後應該等於0。

Section Header Table

與Progarm Header類似,我們同樣可以從ELF Header中得到索引地址(e_shoff)節區數量(e_shnum)表項大小(e_shentsize),還可以由名稱節區索引(e_shstrndx)得到各節區的名稱。

Section Header Table 表項結構定義:

typedef struct {
    Elf32_Word sh_name;
    Elf32_Word sh_type;
    Elf32_Word sh_flags;
    Elf32_Addr sh_addr;
    Elf32_Off sh_offset;
    Elf32_Word sh_size;
    Elf32_Word sh_link;
    Elf32_Word sh_info;
    Elf32_Word sh_addralign;
    Elf32_Word sh_entsize;
}
Elf32_Shdr;
  • sh_name 節區名稱,此處是一個在名稱節區的索引。

  • sh_type 節區型別

    名稱 取值 說明
    SHT_NULL 0 此值標誌節區頭部是非活動的,沒有對應的節區。此節區頭部中的其他成員取值無意義。
    SHT_PROGBITS 1 此節區包含程式定義的資訊,其格式和含義都由程式來解釋。
    SHT_SYMTAB 2 此節區包含一個符號表。目前目標檔案對每種型別的節區都只能包含一個,不過這個限制將來可能發生變化。一般,SHT_SYMTAB 節區提供用於連結編輯(指 ld 而言)的符號,儘管也可用來實現動態連結.
    SHT_STRTAB 3 此節區包含字串表。目標檔案可能包含多個字串表節區。
    SHT_RELA 4 此節區包含重定位表項,其中可能會有補齊內容(addend),例如 32 位目標檔案中的 Elf32_Rela 型別。目標檔案可能擁有多個重定位節區。
    SHT_HASH 5 此節區包含符號雜湊表。所有參與動態連結的目標都必須包含一個符號雜湊表。目前,一個目標檔案只能包含一個雜湊表,不過此限制將來可能會解除。
    SHT_DYNAMIC 6 此節區包含動態連結的資訊。目前一個目標檔案中只能包含一個動態節區,將來可能會取消這一限制。
    SHT_NOTE 7 此節區包含以某種方式來標記檔案的資訊。
    SHT_NOBITS 8 這種型別的節區不佔用檔案中的空間,其他方面和 SHT_PROGBITS 相似。儘管此節區不包含任何位元組,成員sh_offset 中還是會包含概念性的檔案偏移
    SHT_REL 9 此節區包含重定位表項,其中沒有補齊(addends),例如 32 位目標檔案中的 Elf32_rel 型別。目標檔案中可以擁有多個重定位節區。
    SHT_SHLIB 10 此節區被保留,不過其語義是未規定的。包含此型別節區的程式與 ABI 不相容。
    SHT_DYNSYM 11 作為一個完整的符號表,它可能包含很多對動態連結而言不必要的符號。因此,目標檔案也可以包含一個 SHT_DYNSYM 節區,其中儲存動態連結符號的一個最小集合,以節省空間。
    SHT_LOPROC 0X70000000 這一段(包括兩個邊界),是保留給處理器專用語義的。
    SHT_HIPROC 0X7FFFFFFF 這一段(包括兩個邊界),是保留給處理器專用語義的。
    SHT_LOUSER 0X80000000 此值給出保留給應用程式的索引下界。
    SHT_HIUSER 0X8FFFFFFF 此值給出保留給應用程式的索引上界。
  • sh_flags 同Program Header的p_flags

  • sh_addr 節區索引地址

  • sh_offset 節區相對於檔案的偏移地址

  • sh_size 節區的大小

  • sh_link 此成員給出節區頭部表索引連結。

  • sh_info 此成員給出附加資訊。

    sh_type sh_link sh_info
    SHT_DYNAMIC 此節區中條目所用到的字串表格的節區頭部索引 0
    SHT_HASH 此雜湊表所適用的符號表的節區頭部索引 0
    SHT_REL、SHT_RELA 相關符號表的節區頭部索引 重定位所適用的節區的節區頭部索引
    SHT_SYMTAB、SHT_DYNSYM 相關聯的字串表的節區頭部索引 最後一個區域性符號(繫結 STB_LOCAL)的符號表索引值加一
    其它 SHN_UNDEF 0

sh_addralign

某些節區帶有地址對齊約束。例如,如果一個節區儲存一個doubleword,那麼系統必須保證整個節區能夠按雙字對齊。sh_addr 對sh_addralign 取模,結果必須為 0。目前僅允許取值為 0 和 2的冪次數。數值 0 和 1 表示節區沒有對齊約束。

sh_entsize

某些節區中包含固定大小的專案,如符號表。對於這類節區,此成員給出每個表項的長度位元組數。 如果節區中並不包含固定長度表項的表格,此成員取值為 0

一般來說,節區索引為0,即第一個節區一般都是SHN_UNDEF,其各項值都固定為0
劃重點:

*1. 以“.”開頭的節區名稱是系統保留的。應用程式可以使用沒有字首的節區名稱,以避免與系統節區衝突。*

*2. 目標檔案中也可以包含多個名字相同的節區。*

*3. 保留給處理器體系結構的節區名稱一般構成為:處理器體系結構名稱簡寫 + 節區名稱。*

*4. 處理器名稱應該與 e_machine 中使用的名稱相同。例如 .FOO.psect 街區是由 FOO 體系結構定義的 psect 節區。*

部分系統節區作用詳解

字串表

在一個ELF檔案中通常擁有一個或以上的字串表,即型別為 SHT_STRTAB 的節區,如: ELF Header 中 e_shstrndx 索引的節區名稱表(.shstrtab)、符號名稱表(.dynstr)等。

對於字串的定義,是以NULL(\0)開頭,以NULL結尾。

以一個.shstrtab表的內容為例:

00 2E 73 68 73 74 72 74 61 62 00 2E 69 6E 74 65 72 70 00 2E 64 79 6E 73 79 6D 00 ...

從這裡可以得到3個字串即:

  • 2E 73 68 73 74 72 74 61 62 (.shstrtab);
  • 2E 69 6E 74 65 72 70 (.interp);
  • 2E 64 79 6E 73 79 6D (.dynsym);

假如索引為0,那麼字串的內容就是 2E 73 68 73 74 72 74 61 62 (.shstrtab)

符號表

符號: 指函式或者資料物件等。
既然叫做表,那麼也分為一個一個表項,其表項也有自己的結構定義:

typedef struct {
    Elf32_Word st_name;
    Elf32_Addr st_value;
    Elf32_Word st_size;
    unsigned char st_info;
    unsigned char st_other;
    Elf32_Half st_shndx;
}
Elf32_sym;
  • st_name 符號名稱,給出的是一個在符號名稱表(.dynstr)中的索引

  • st_value 一般都是函式地址,或者是一個常量值

  • st_size 從 st_value 地址開始,共佔的長度大小

  • st_info

    用於標示此符號的屬性,佔一個位元組(2個字),兩個標示位,第一個標示位(低四位)標誌作用域,第二個標示位(高四位)標示符號型別

    取值 代表 含義
    0 STB_LOCAL 區域性符號在包含該符號定義的目標檔案以外不可見。相同名稱的區域性符號可以存在於多個檔案中,互不影響。
    1 STB_GLOBAL 全域性符號對所有將組合的目標檔案都是可見的。一個檔案中對某個全域性符號的定義將滿足另一個檔案對相同全域性符號的未定義引用。
    2 STB_WEAK 弱符號與全域性符號類似,不過他們的定義優先順序比較低。

    ​ (以我的理解..就是LOACL是區域性變數,GLOBAL 和 WEAK 是全域性量, 兩者的差別在於是不是常量?猜的。)

    取值 代表 含義
    1 STT_OBJECT 符號與某個資料物件相關,比如一個變數、陣列等等
    2 STT_FUNC 符號與某個函式或者其他可執行程式碼相關
    3 STT_SECTION 符號與某個節區相關。這種型別的符號表項主要用於重定位,通常具有 STB_LOCAL 繫結
    4 STT_FILE 傳統上,符號的名稱給出了與目標檔案相關的原始檔的名稱。檔案符號具有 STB_LOCAL 繫結,其節區索引是SHN_ABS,並且它優先於檔案的其他 STB_LOCAL 符號(如果有的話)

    ​ 舉個栗子: 比如此處數值為0x12,那麼他就對應著 STB_GLOBAL 和 STT_FUNC。從程式設計的方面來說,就是一個 public 的函式。

  • st_other 固定值為0。

  • st_shndx

    每個符號表項都以和其他節區間的關係的方式給出定義。此成員給出相關的節區頭部表索引。某些索引具有特殊含義。

    唔..我也不知道有什麼用…

程式碼段
程式碼段就是存放指令的節區(.text),符號表中的 st_value 指向程式碼段中具體的函式地址,以其地址的指令為函式開頭。

全域性偏移表
指.got節區,.got內的值均為 Elf32_Addr。其為全域性符號提供偏移地址(指向過程連結表)。

過程連結表
.plt節區,其每個表項都是一段程式碼,作用是跳轉至真實的函式地址

雜湊表
指.hash節區。雜湊表的結構:

其中nchain為符號表表項數,nchain 和 nbucket 是 chain 和 bucket 的數量。

資料段
.data、.bss、.rodata都屬於資料段。其中,

  • .data 存放已初始化的全域性變數、常量。
  • .bss 存放未初始化的全域性變數,所以此段資料均為0,僅作佔位。
  • .rodata 是隻讀資料段,此段的資料不可修改,存放常量。

.init_array .fini_array
程式執行時,執行.init_array中的指令。
程式退出時,執行.fini_array中的指令。