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] 的内容了。从文件偏移的角度来说,也就是文件偏移 
为 的字节确定了 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 后面的 个字节全部为零, 
暂时没有使用。 

现在确定了文件头的结构,我们就可以解读文件头了。下文中我们以 32 位的 
ELF 
文件为例来说明。对 64 位的,大同小异,把所有 Elf32_*** 结构换成 ET_EXEC 
对应的 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_shnume_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); 
untitled folder 2lseek(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; /* 
节名索引 */ 00000034
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 Interpreter

 

PT 2 Entry: Loadable to 0x8048000[0x424] from 0x0[0x424] align 0x1000

 

PT 3 Entry: Loadable to 0x8049424[0x10c] from 0x424[0x108] align 0x1000

 

PT 4 Entry: Dynamic


.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 Loadable to节。给出动态连接 
信息的程序段同样与数据段相交叉,只包含 .dynamic 节。另一个程序段,与 
文本段相交叉,包含 .note.ABI-tag 节,它给出辅助信息。此外,还有一个 
程序段,它指程序头表自身。同动态库文件一样,下面的一些节不属于任何程 
序段: 

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

 


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值