歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux綜合 >> Linux資訊 >> 更多Linux

解讀 ELF 文件

  本文敘述如何解讀 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 我們看到,這一段與數據段有交叉



Copyright © Linux教程網 All Rights Reserved