ELF是Executable andLinkable Format的缩写,它是Linux下可执行文件、目标文件和库文件的格式标准(Windows系统中可执行文件、目标文件和库文件的格式标准为PE格式)。ELF一个特别的优点在于同一文件格式可以用于Linux内核支持的几乎所有的体系结构上。该格式标准不仅用于用户空间应用程序和库文件,还用于构建模块,且内核本身也是ELF格式。
可执行文件一般是由源代码编译为目标文件(或称为链接文件),后通过链接器链接以后形成的,因此存在两种形式的ELF文件,即目标文件的链接视图和可执行文件的执行视图:
其中各部分的作用分别为:
1. ELF文件头。用于标识该文件为ELF文件,并且包含了有关文件类型和大小的信息,以及文件加载进入内存后的入口点信息。
2. 程序头表(program header table)向系统提供了可执行文件的数据在进程虚拟地址空间中组织方式,还表示了文件包含的段的数目、位置和用途。
3. 节保存与文件相关的各种形式的数据、代码、资源等,一般一个节只存储一种形式的内容。链接过程中,各个目标文件中的节会选择相应的段进行存储,也就是说,不同目标文件的同类型的节会存储在可执行文件的同一个段当中,且不同类型的节也可能存在于同一个段当中,这需要根据节的性质进行分类存储。后面介绍程序头表时会进行说明。
4. 节头表(section header table)包含了与各节相关的附加信息。
本文在32位系统下,以一个简单的程序为例,对该程序编译产生的目标文件,链接产生的可执行文件进行分析,进一步认识ELF文件的结构。在分析各部分结构时,除了展示分析工具的结果外,同时也会展示相应的数据结构。所有涉及到的数据结构,均从内核为2.6.24版本的源代码当中获取,部分结构或宏定义在高于该版本的内核源代码中可能有所改变或扩展。
对ELF文件分析需要借助一个工具:readelf。ELF文件中各个头、表一般都有相应的数据结构来表示,Linux内核中做了详细的定义。在描述这些数据结构之前,首先需要了解ELF中用到的基础数据类型:
/* 32-bit ELF base types. */
typedef __u32 Elf32_Addr; //地址,32位
typedef __u16 Elf32_Half; //无符号字,2字节
typedef __u32 Elf32_Off; //地址偏移量,32位
typedef __s32 Elf32_Sword; //有符号整型,4字节
typedef __u32 Elf32_Word; //无符号整型/双字,4字节
这些基础数据类型将会在后面的结构体当中用到。
一、ELF文件头:
1. 数据结构
#define EI_NIDENT 16
typedef struct elf32_hdr {
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;
(1) e_ident成员有16个字节,这16个字节被分为好几个部分,分别标识不同信息。
ELF_NAME:e_ident[0]—e_ident[3]分别为0x7f和E、L、F三个字母的ASCII码,即前四个字节固定为0x7f、0x45、0x4c、0x46。这四个字节即标识了该文件为ELF文件。
EI_CLASS:位于e_ident[4],标识文件的类别,目前定义的值只有ELFCLASS32(0x1)和ELFCLASS64(0x2),分别表示32位和64位程序。
EI_DATA:位于e_ident[5],指定了格式使用的字节顺序,对应的值可以是ELFDATA2LSB(0x1)和ELFDATA2MSB(0x2),分别表示小端序和大端序。
EI_VERSION:位于e_ident[6],表示ELF文件头版本,目前只有EV_CURRENT(0x1)。
e_ident[7]—e_ident[15]暂不使用。
(2) e_type用于区分ELF文件类型,目前有如下值:
#define ET_REL 1
#define ET_EXEC 2
#define ET_DYN 3
#define ET_CORE 4
(3) e_machine指定了文件所需的体系结构,常见值如下:
#define EM_SPARC 2 //32位sparc
#define EM_386 3 //IA-32
#define EM_MIPS 8 //MIPS
#define EM_MIPS_RS3_LE 10 //MIPS R3000 小端序
#define EM_MIPS_RS4_BE 10 //MIPS R4000 大端序
#define EM_SPARC32PLUS 18 //32位sparc "v8plus"
#define EM_PPC 20 //PowerPC
#define EM_PPC64 21 //PowerPC64
#define EM_SPARCV9 43 //64位sparc
#define EM_IA_64 50 //IA-64
#define EM_X86_64 62 //AMD64
(4) e_version保存了版本信息,目前只有EV_CURRENT(0x1)。
(5) e_entry给出了程序在虚拟内存当中的入口点,是程序开始执行的位置。这一值只在程序映射到内存当中后才有效。
(6) e_phoff给出了在文件状态时程序头表起始位置的偏移量。
(7) e_shoff给出了在文件状态时节头表起始位置的偏移量。
(8) e_flags可以保存特定于处理器的标志,目前不使用。
(9) e_ehsize指定了ELF头的长度,以字节为单位,因为ELF头长度固定,因此该值一般为52(十进制)。
(10) e_phentsize指定了程序头表中一项长度,以字节为单位。
(11) e_phnum指定了程序头表中项的数量。
(12) e_shentsize指定了节头表中一项长度,以字节为单位。
(13) e_shnum指定了节头表中项的数量。
(14) e_shstrndx保存了包含各节名称的字符串表在节头表中的索引位置。
下面根据实例进行实际分析。
2. 目标文件中ELF头信息
(1) Magic所示内容为e_ident,其中前四个字节固定为0x7f、0x45、0x4c、0x46。
(2) Class所示内容为e_ident[4],本例中为32位程序。
(3) Data所示内容为e_ident[5],本例中为小端序。
(4) Vsersion所示内容为e_ident[6],固定为0x1,如图上所示。
(5) Type所示内容为e_type,本例中为ET_REL,即可重定位文件。
(6) Machine所示内容为e_machine,本程序是在Intel80386架构下编译的。
(7) Version所示内容为e_version,固定为0x1,如图上所示。
(8) Entry point address所示内容为e_entry,目标文件中该值无意义。
(9) 图中剩余字段根据名称即可判断与之对应的结构体中的项。
某些字段值为0,如程序头大小、数量和起始位置,程序入口点,因为对于目标文件来说这些值是不需要的。
3. 可执行文件中ELF头信息
对比目标文件ELF头信息,可以发现某些字段的值变化了:如Type变为ET_EXEC,表示可执行程序;Entry point address有了相应的值;程序头的大小不再为0;节头的数量也增加了。
根据以上信息就能对ELF文件可执行环境,入口点,节的数量有个初步的了解。因篇幅限制,在《Linux系统ELF文件二进制格式分析(二)》中将继续对剩余部分做分析。