歡迎您光臨本站 註冊首頁

解讀 ELF 文件

←手機掃碼閱讀     火星人 @ 2014-03-12 , reply:0
  作者:wangdb < wangdb@nsfocus.com>
日期:2001-4-16


本文敘述如何解讀 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

我們看到,這一段與數據段有交叉了。此外還有一些節它們不屬於任何一個程
序段,這些節是:

.comment
.note
.shstrtab
.symtab
.strtab

對可執行文件,共有六個程序段,如果是用 gcc 編譯生成的,按文件偏移和虛
地址增長次序排列,文本段包含如下這些節:

.interp
.note.ABI-tag
.hash
.dynsym
.dynstr
.gnu.version
.gnu.version_r
.rel.got
.rel.plt
.init
.plt
.text
.fini
.rodata

同樣是按文件偏移和虛地址增長次序排列,可執行文件的數據段包含如下這些
節:

.data
.eh_frame
.ctors
.dtors
.got
.dynamic
.bss

程序解釋段(INTERP)與文本段相交叉,只包含 .interp 一節。給出動態連接
信息的程序段同樣與數據段相交叉,只包含 .dynamic 節。另一個程序段,與
文本段相交叉,包含 .note.ABI-tag 節,它給出輔助信息。此外,還有一個
程序段,它指程序頭表自身。同動態庫文件一樣,下面的一些節不屬於任何程
序段:

.stab
.stabstr
.comment
.note
.shstrtab
.symtab
.strtab

[未完待續]


[火星人 ] 解讀 ELF 文件已經有673次圍觀

http://coctec.com/docs/linux/show-post-69843.html