LF指的是Executable and Linkable Format。最初是由UNIX系统实验室作为应用程序二进制接口开发和发行的,后来逐渐发展成为了可执行文件的格式标准,在很多操作系统和非操作系统环境中都有非常广泛的应用。完整的ELF格式标准涉及了三个方面的内容。在这里我们只需要关心一个方面,那就是一个ELF格式可执行程序的组成结构。
一个ELF可执行文件格式如图8-1所示。
像图8-1那样,一个ELF可执行文件包含了一个描述全局信息的ELF文件头、若干个Program头、若干个Segment以及若干个可有可无的Section头。在Segment中保存的正是程序的运行代码,而Program头描述了各个Segment和其他必要信息,如链接库信息、文件辅助信息等。在代码真正运行时,我们往往只关心实际的代码部分,而与运行无关的其他信息则可以忽略掉。
ELF文件头
program头表
segment 1
segment 2
.............
section头表(可选)
图8-1 ELF文件结构 |
当操作系统需要执行一个ELF格式的文件时,只需要分析ELF文件头,然后找到Program头的位置,依次分析这些Program头,找到代表代码和数据的Segment,最后将这些Segment复制到指定的地址处,便可以运行程序了。
一个ELF文件头由如下部分组成:
代码8-4
- typedef unsigned int elf32_addr;
- typedef unsigned int elf32_word;
- typedef signed int elf32_sword;
- typedef unsigned short elf32_half;
- typedef unsigned int elf32_off;
- struct elf32_ehdr{
- unsigned char e_ident[16];
- 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;
- };
因为ELF可执行程序格式可以支持不同位数的处理器,所以ELF标准中自定义了一些专有的数据类型。无论是8位的处理器还是32位的处理器,这些数据类型的大小都是一致的,从而保证了文件与处理器格式的无关性。这些数据类型的大小和含义如表8-1所示。
表8-1 ELF格式中的数据规定
数据类型 |
大小 |
对齐 |
含义 |
elf32_addr |
4 |
4 |
用于描述程序运行地址 |
elf32_word |
4 |
4 |
描述无符号大整数 |
elf32_sword |
4 |
4 |
描述有符号大整数 |
elf32_half |
2 |
2 |
描述无符号中等大小的整数 |
elf32_off |
4 |
4 |
描述无符号的文件偏移量 |
让我们结合表8-1的描述,逐个分析一下ELF文件中各部分的含义。
一个ELF文件头总是出现在文件的最开始处。其中,第一个成员是一个16个字节数组,里边记录了文件的标识、版本、编码格式等信息。
之后的两个字节是e_type成员,记录了目标文件属于ELF格式标准下的哪种类型,比如是可执行的文件还是可重定位文件,等等。根据ELF格式标准,这个值是2,代表了这个文件是一个可执行的文件,这也正是我们需要的。
接下来的两个字节e_machine描述的是该程序运行的硬件平台。在我们的例子中,这个值必须是40,表示这个应用程序是在ARM中运行的。
然后,4个字节的空间e_version用于描述应用程序的版本,通常可以是1。
接下来的4个字节就相当重要了,结构体成员e_entry记录了程序运行时的入口地址,也就是说,应用程序的第一条指令就应该出现在这个地址处。
e_phoff成员记录了第一个Program头在文件内的偏移。e_phentsize成员则代表了每一个Program头大小,再结合能够描述文件中共有多少个Program头的e_phnum成员,我们就可以遍历每一个Program头,并可以从中找到代码和数据在文件中的位置。
代码8-4中的其他成员与程序的执行关系不大,这里就不多介绍了。读者朋友们如果还对这些内容感兴趣,可以去查阅相关文档。
另外,还有一个问题需要解决。在遍历每一个Program头时,如何才能知道这个头信息所描述的Segment就是数据或代码,而不是与运行程序无关的其他信息呢?我们在Program头结构体中可以找到答案。
代码8-5
- struct elf32_phdr{
- 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;
- };
代码8-5定义了elf32_phdr结构体用于描述Program头信息。在该结构体中,与段类型直接相关的是p_type成员,只有当该成员的值为1时,才表示该Segment是要运行的代码或数据,需要在执行时加载到内存中去。
一旦确定需要进行内存加载,之后要做的就是读取p_offset成员的值,它表示要加载到内存中的这个Segment在文件中的偏移量,再结合描述Segment大小的成员p_filesz,就可以精确地定位程序的代码和数据了。
最后,程序还要读取p_vaddr成员,它表示这个Segment应该出现在内存的哪个位置。这样就可以将该Segment从文件中复制到正确的内存地址处了。
ELF格式的组成结构