当前位置:Linux教程 - Linux综合 - 解读 ELF 文件

解读 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,表
[1] [2] [3] 下一页 

明它是 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
上一页 [1] [2] [3] 下一页 

.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 我们看到,这一段与数据段有交叉

(出处:http://www.sheup.com)


上一页 [1] [2] [3] 

.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 我们看到,这一段与数据段有交叉

(出处:http://www.sheup.com/)


上一页 [1] [2] [3] [4]