本文敘述如何解讀 ELF 文件。
打開一個 ELF 文件解讀時,我們首先遇到的是一個 ELF 文件頭。ELF 文件頭
給出解讀整個 ELF 文件的路徑圖,它是一個固定的結構。文件頭的結構在系統
頭文件 elf.h 中定義,如果是 32 位的二進制文件,它是一個 Elf32_Ehdr
結構,如果是 64 位的二進制文件,則是一個 Elf64_Ehdr 結構。無論是何種
結構,結構的第一個成員是一個 16 字節的 e_ident,它給出了整個 ELF 文
件的解讀方式。究竟是 32 位的 Elf32_Ehdr 結構還是 64 位的 Elf64_Ehdr
結構,就看 e_ident[4] 的內容了。從文件偏移的角度來說,也就是文件偏移
為 4 的字節確定了 ELF 文件究竟是 32 位的還是 64 位的。這裡我們遵從習
慣把文件開頭的起始第一個字節的文件偏移約定為 0,下面的所有敘述都遵從
這個約定。
於是我們要做的第一件事是解讀這個 e_ident,確定 ELF 文件是 32 位的還
是 64 位的,或者是其他位數的,從而確定 ELF 文件頭的結構。為此,假定
打開ELF 文件時返回的文件描述符是 fd,
lseek(fd, 0, SEEK_SET);
read(fd, buf, 16);
讀出的 buf 裡前四個字節是 Magic Number(對應文件偏移 0-3)。如果
buf[0] = 0x7f、buf[1] = 'E'、buf[2] = 'L'、buf[3] = 'F'
則表明這是一個 ELF 格式的二進制文件,否則不是。如前面所述,我們首先關
注的是 buf[4]。如果 buf[4] 的值是 1,則是 32 位的;如果是 2,則是 64
位的。接下來是 buf[5],它給出字節序特性。如果它的值是 1,則是 LSB 的;
如果是 2,則是 MSB 的。對 Intel x86 機器,buf[5] = 1;對 Sun Sparc,
buf[5] = 2。跟著 buf[5] 的 buf[6] 給出 ELF 文件頭的版本信息,當前它
的值是 EV_CURRENT(參見 elf.h 中的宏定義)。對 buf[6] = EV_CURRENT
的 ELF 文件頭,從 buf[7] 開始,也即 e_ident 後面的 9 個字節全部為零,
暫時沒有使用。
現在確定了文件頭的結構,我們就可以解讀文件頭了。下文中我們以 32 位的
ELF 文件為例來說明。對 64 位的,大同小異,把所有 Elf32_*** 結構換成
對應的 Elf64_*** 結構,看看 elf.h 就什麼都清楚了。32 位的 ELF 文件頭
結構定義如下:
#define EI_NIDENT (16)
typedef uint16_t Elf32_Half;
typedef uint32_t Elf32_Word;
typedef uint32_t Elf32_Addr;
typedef uint32_t Elf32_Off;
typedef strUCt {
unsigned char e_ident[EI_NIDENT]; /* 上文所說的 e_ident */
Elf32_Half e_type; /* 文件類型 */
Elf32_Half e_machine; /* 機器類型 */
Elf32_Word e_version; /* 文件版本 */
Elf32_Addr e_entry; /* 程序入口虛地址 */
Elf32_Off e_phoff; /* 程序頭表文件偏移 */
Elf32_Off e_shoff; /* 節頭表文件偏移*/
Elf32_Word e_flags; /* 處理器相關的標志 */
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;
結構的各個成員的含義如注釋中所解釋的。對 ELF 文件,有兩個視圖,一個是
從裝載運行角度的,另一個是從連接角度的。從裝載運行角度,我們關注的是程
序頭表,由程序頭表的指引把 ELF 文件加載進內存運行它。從連接的角度,我
們關注節頭表,由節頭表的指引把各個節連接組裝起來。e_type 的值與這兩個
視圖相聯系,由它我們可以知道能夠從哪個視圖去解讀。如果 e_type = 1,表
明它是重定位文件,可以從連接視圖去解讀它;如果 e_type = 2,表明它是可
執行文件,至少可以從裝載運行視圖去解讀它;如果 e_type = 3,表明它是共
享動態庫文件,同樣可以至少從裝載運行視圖去解讀它;如果 e_type = 4,表
明它是 Core dump 文件,可以從哪個視圖去解讀依賴於具體的實現。
按照這兩個視圖,整個 ELF 文件的內容這樣來組織:首先是 ELF 文件頭,也
就是上面的 Elf32_Ehdr 結構。或者對 64 位的 ELF 文件,是 Elf64_Ehdr
結構。ELF 文件頭位於文件開始處,無論 e_type 的值是什麼,它是必須有的。
其次是程序頭表,對可執行文件(e_type = 2)和動態庫文件(e_type = 3),它
是必須有的。對重定位文件(e_type = 1),程序頭表的有無是可選的。例如用
gcc 的 -c 選項生成的 .o 文件,就沒有程序頭表。但無論如何,e_phoff 和
e_phnum、e_phentsize 給出了 ELF 文件的程序頭表信息。沒有程序頭表時它
們的值為零。然後就是就是節頭表,對可執行文件和動態庫文件,它的有無是
可選的,對重定位文件,它是必須有的。e_shoff 和 e_shnum、e_shentsize
給出節頭表信息。最後就是文件的代碼和數據這些具體內容了。如果有節頭表,
從連接視圖去解讀,ELF 文件的具體代碼和數據內容是以節為單位組織的。所
有的代碼和數據都分屬於某一節,並且不能同時屬於兩個節。各個節不能交叉,
不能有同時兩個節覆蓋同一內容。每一節在節頭表中有一個表項與之對應,給
出該節的相關信息。如果有程序頭表,從裝載運行視圖去解讀,所有代碼和數
據都分屬於某一程序段。與連接視圖不同,此時有交叉的情況。某些內容可能
同時屬於幾個程序段,也即可能有幾個段覆蓋同一內容。同時,從程序頭表來
看,可能某些段不包含任何具體的代碼和數據內容。例如,給出動態連接信息
的程序段的所有內容都同時數據段。注意不要把這裡所說的程序段與我們通常
所說的文本段、數據段和堆棧段這幾個概念相混淆,雖然它們有聯系。程序加
載進內存時,根據程序頭表信息來就解讀。
從連接視圖來解讀,其中有一節的內容是一些以零結尾的字符串。e_shstrndx
給出該節在節頭表中的表項索引。這些字符串是各節的名字。
了解了這些後,我們可以分別從兩個視圖來解讀 ELF 文件了。先看連接視圖,
於是我們
Elf32_Ehdr e_hdr;
void *SecHdrTbl;
lseek(fd, 0, SEEK_SET);
read(fd, &e_hdr, sizeof(e_hdr));
SecHdrTbl = malloc(e_hdr.e_shnum * e_hdr.e_shentsize);
lseek(fd, e_hdr.e_shoff, SEEK_SET);
read(fd, SecHdrTbl, e_hdr.e_shnum * e_hdr.e_shentsize);
我們看看節頭表是什麼樣的,因為節頭表的各個表項給出了如何從連接視圖
解讀 ELF 文件的路徑圖。節頭表的每個表項是一個如下的結構:
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;
再看裝載運行視圖:
void *ProHdrTbl;
ProHdrTbl = malloc(e_hdr.e_phnum * e_hdr.e_phentsize);
lseek(fd, e_hdr.e_phoff, SEEK_SET);
read(fd, SecHdrTbl, e_hdr.e_phnum * e_hdr.e_phentsize);
每個程序頭表的每個表項的結構為:
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_flags; /* 標志 */
Elf32_Word p_align; /* 字節對齊 */
} Elf32_Phdr;
我們看一看這兩個視圖之間的相互關聯,對動態庫文件,共有三個程序段,如
果是用 gcc 編譯生成的,按文件偏移和虛地址增長次序排列,文本段包含如下
這些節:
.hash
.dynsym
.dynstr
.gnu.version
.gnu.version_d
.gnu.version_r
.rel.data
.rel.got
.rel.plt
.init
.plt
.text
.fini
.rodata
同樣是按文件偏移和虛地址增長次序排列,數據段包含如下這些節:
.data
.eh_frame
.ctors
.dtors
.got
.dynamic
.bss:
另外還有一個程序段,它給出動態連接信息,它只包含有一節
.dynamic
我們看到,這一段與數據段有交叉
.dynstr
.gnu.version
.gnu.version_d
.gnu.version_r
.rel.data
.rel.got
.rel.plt
.init
.plt
.text
.fini
.rodata
同樣是按文件偏移和虛地址增長次序排列,數據段包含如下這些節:
.data
.eh_frame
.ctors
.dtors
.got
.dynamic
.bss:
另外還有一個程序段,它給出動態連接信息,它只包含有一節
.dynamic
我們看到,這一段與數據段有交叉