现代Linux采用ELF做为其可连接和可执行文件的格式,因此ELF格式也向我们透出了一点Linux核内的情景,就像戏台维幕留下的一条未拉严的缝。本文着重讲述32位ELF的同时附带了64位的信息,这两种格式如此雷同,以致于初次接触ELF的读者不必兼顾左右。如果你对Windows比较熟悉,本文还将时时把你带回到PE中,在它们的相似之处稍做比较。ELF文件以“ELF头”开始,后面可选择的跟随着程序头和节头。地理学用等高线与等温线分别展示同一地区的地势和气候,程序头和节头则分别从加载与连接角度来描述EFL文件的组织方式。下面我们进入正文。
一、ELF头
ELF头也叫ELF文件头,它位于文件中最开始的地方。我用系统的是Fedora Core 2,它在elf.h文件中同时给出了ELF头在32位系统和64位系统下的结构,我们先来看一下:
typedef struct
{
unsigned char e_ident[EI_NIDENT];
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;
Elf32_Half e_phentsize;
Elf32_Half e_phnum;
Elf32_Half e_shentsize;
Elf32_Half e_shnum;
Elf32_Half e_shstrndx;
} Elf32_Ehdr;
typedef struct
{
unsigned char e_ident[EI_NIDENT];
Elf64_Half e_type;
Elf64_Half e_machine;
Elf64_Word e_version;
Elf64_Addr e_entry;
Elf64_Off e_phoff;
Elf64_Off e_shoff;
Elf64_Word e_flags;
Elf64_Half e_ehsize;
Elf64_Half e_phentsize;
Elf64_Half e_phnum;
Elf64_Half e_shentsize;
Elf64_Half e_shnum;
Elf64_Half e_shstrndx;
} Elf64_Ehdr;
elf.h中关于ELF格式所有结构给出的定义,其成员字段的类型声名都是C语言基本类型的别名,不会再嵌套结构。可以看出32位系统和64位系统下ELF头的结构基本相同,不同的是两种结构中的某个成员字段占用字节个数有所变化。比如e_entry由32位下占4个字节的Elf32_Addr变为64位下占8个字节的Elf64_Addr,这是因为两种系统下CPU寻址能力不同造成的。同理文件偏移也从4字节的Elf32_Off变为8字节的Elf64_Off。有些成员字段虽然类型声名从Elf32_XXXX变成了Elf64_XXXX,该域所占的字节个数并未改变。如Elf32_Half和Elf64_Half都占两个字节,Elf32_Word、Elf32_Sword、Elf64_Word、Elf64_Sword全都是4个字节。尽量使用elf.h中的现有定义将使我们写的程序具有很强的可移植性。另外ELF格式在两种系统下的这种雷同也使得我们可放心的抛弃它们的差别,专心研究其中的一种,然后再轻松的掌握另一种。
ELF头中每个字段的含意如下:
e_ident:
这个字段是ELF头结构中的第一个字段,在elf.h中EI_NIDENT被定义为16,因此它占用16个字节。
e_ident的前四个字节顺次应该是0x7f、0x45、0x4c、0x46,也就是"\177ELF"。这是ELF文件的标志,任何一个ELF文件这四个字节都完全相同。它让熟悉Windows的人想起'MZ'和'PE\O\O'。
第5个字节标志了ELF格式是32位还是64位,32位是1,64位是2。
第6个字节,在0x86系统上是1,表明数据存储方式为低字节优先。
第10个字节,指明了在e_ident中从第几个字节开始后面的字节未使用。
e_type:
ELF文件的类型,1表示此文件是重定位文件,2表示可执行文件,3表示此文件是一个动态连接库。
e_machine:
CPU类型,它指出了此文件使用何种指令集。如果是Intel 0x386 CPU此值为3,如果是AMD 64 CPU此值为62也就是16进制的0x3E。
e_version:
ELF文件版本。为1。
e_entry:
可执行文件的入口虚拟地址。此字段指出了该文件中第一条可执行机器指令在进程被正确加载后的内存地址!(PE可执行文件指出的是入口的相对虚拟地址RVA,它是相对于文件加载起始地址的一个偏移值,因此理论上PE文件可被加载到进程序空间任何位置,而ELF可执行文件只能被加载到固定位置)。
e_phoff:
程序头在ELF文件中的偏移量。如果程序头不存在此值为0。
e_shoff:
节头在ELF文件中的偏移量。如果节头不存在此值为0。
e_ehsize:
它描述了“ELF头”自身占用的字节数。
e_phentsize:
程序头中的每一个结构占用的字节数。程序头也叫程序头表,可以被看做一个在文件中连续存储的结构数组,数组中每一项是一个结构,此字段给出了这个结构占用的字节大小。e_phoff指出程序头在ELF文件中的起始偏移。
e_phnum:
此字段给出了程序头中保存了多少个结构。如果程序头中有3个结构则程序头在文件中占用了3×e_phentsize个字节的大小。
e_shentsize:
节头中每个结构占用的字节大小。节头与程序头类似也是一个结构数组,关于这两个结构的定义将分别在讲述程序头和节头的时候给出。
e_shnum:
节头中保存了多少个结构。
e_shstrndx:
这是一个整数索引值。节头可以看作是一个结构数组,用这个索引值做为此数组的下标,它在节头中指定的一个结构进一步给出了一个“字符串表”的信息,而这个字符串表保存着节头中描述的每一个节的名称,包括字符串表自己也是其中的一个节。
至此为止我们已经讲述了“ELF头”,在此过程中提前提到的一些将来才用的概念,不必急于了解。现在读者可自己编写一个小程序来验证刚学到的知识,这有助于进一步的学习。ELF.elf.h文件一般会存在于/usr/include目录下,直接include它就可以。但我们能够验证的知识有限,当更多知识联系在一起的时候我们的理解正误才可以得到更好的验证。接下来我们再学习程序头。