本文叙述如何解读 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
[未完待续]