深入理解Android:Java虚拟机ART
上QQ阅读APP看书,第一时间看更新

4.2.1 ELF文件头结构介绍

ELF文件支持64位和32位平台,规范为此定义了不同的ELF文件头结构,它们对应的数据结构如图4-2所示。

图4-2 ELF文件头结构介绍

由图4-2可知,64位和32位ELF文件头结构包含了同名的成员域,只是某些成员域的长度不同罢了。下面以64位ELF文件头结构为例来介绍它的各个成员域。

首先,ELF文件头结构前16个字节由e_ident数组描述。

·e_ident[0-3]:前4个元素构成魔幻数(Magic Number),取值分别为'0x7f'、'E'、'L'、'F'。

·e_ident[EL_CLASS=4]:该元素表示ELF文件是32位ELF文件(取值为1)还是64位ELF文件(取值为2)。

·e_ident[EL_DATA=5]:该元素表示ELF文件的数据的字节序是小端(Little Endian,取值为1)还是大端(Big Endian,取值为2)。

·e_ident[EL_VERSION=6]:ELF文件版本,正常情况下该元素取值为1。

·e_ident其余元素为字节对齐用。

紧接e_ident的成员信息如下。

·e_type:该成员域的长度为2个字节(类型为Elf64_Half),指明ELF文件的类型。

·e_machine:该成员域长度也为2个字节,指明该ELF文件对应哪种CPU架构。

·e_version:该成员取值同e_ident[EL_VERSION]。

·e_entry:如果ELF文件是一个可执行程序的话,操作系统加载它后将跳转到e_entry的位置去执行该程序的代码。简单点说,对可执行程序而言,e_entry是这个程序的入口地址。这里要特别指出的是,e_entry是虚拟内存地址,不是实际内存地址。

·e_phoff:ph是program header的缩写。由图4-1可知,program header table是执行视图中必须要包含的信息。e_phoff指明ph table在该ELF文件的起始位置(从文件头开始算起的偏移量)。

·e_shoff:sh是section header的缩写。同e_phoff类似,如果该ELF文件包含sh table的话,该成员域指明sh table在文件的起始位置。

·e_flags:和处理器相关的标识。

·e_ehsize:eh是elf header的缩写。该成员域表示ELF文件头结构的长度,64位ELF文件头结构长度为64。

·e_phentsize和e_phnum:这两个成员域指明ph table中每个元素的长度和该table中包含多少个元素。注意,ph表元素的长度是固定的,由此可计算ph table的大小是e_phentsize(ph entry size,每个元素的长度)×e_phnum(entry number,元素个数)。

·e_shentsize和e_shum:说明sh table中每个元素的长度以及sh table中包含多少个元素。

·e_shstrndx:根据ELF规范,每个section都会有一个名字(用字符串表示)。这些字符串存储在一个类型为String的section里。这个section在sh table中的索引号就是e_shstrndx。

另外,图4-2中的成员域并没有使用int、long这样的常见数据类型,而是使用Elfxx_Half、Elfxx_Word、Elfxx_Addr和Elfxx_Off来表示,它们的含义如下。

·Elf64_Addr(作用为Unsigned program address,表示程序内的地址,无符号)为8字节长,ELF32_Addr为4字节,等同于64或32位平台的指针类型。

·Elf64_Off(作用为Unsigned file offset,表示文件偏移量,无符号)为8字节长(等同于64位平台的long),Elf32_Off为4字节(等同于32位平台的int)。

·Elf64_Half(作用为Unsigned medium integer,表示中等大小的整数,无符号)和Elf32_Half都是2字节,等同于short。

·Elf64_Word(作用为Unsigned integer,无符号整型)和Elf32_Word都是4字节,等同于int。

现在来看e_type,e_machine,e_entry的含义。

4.2.1.1 e_type介绍

根据规范,ELF文件分好几种类型,它们通过e_type来区别。表4-1列出了常见的e_type取值和对应的说明。

表4-1 e_type取值说明

4.2.1.2 e_machine介绍

ELF文件是Unix平台上一种比较通用的文件格式。要想做到平台通用,不仅仅是数据结构要定义好,还得综合考虑不同平台、处理器之间的差异。e_machine字段表示该ELF文件适应于哪种CPU平台。图4-3所示为笔者收集的e_machine取值情况和对应说明。

图4-3 e_machine取值和说明

图4-3中,第一列EM_XXX为标识符,第二列的数字为e_machine的取值,第三列为该标识符的说明。以图中深色标记的一行为例。

·EM_X86_64:标记符,取值为62。

·AMD x86-64 architecture:该标记符的解释,表示为AMD x8664位平台。

4.2.1.3 e_flags介绍

同e_machine一样,e_flags也跟因平台不同而有差异,其取值和解释依赖e_machine。笔者此处以ARM平台为例,介绍它的取值情况。

·在ARM32位平台(e_machine被定义为标记符EM_ARM,值为40)上,e_flags取值为0x02(标记符为EF_ARM_HASENTY),表示该ELF文件包含有效e_entry值。为什么头结构中已经定义了e_entry,而ARM平台上还需要这个参数呢?原来,在ARM平台上,e_entry取值可以为0。而这和ELF规范中ELF文件头结构的e_entry为0表示没有e_entry的含义相冲突。所以在ARM平台上,e_entry为0的真正含义就由e_flags来决定。

·在ARM64位平台(e_machine取值为183,标记符为EM_AARCH64)上,e_flags就没有特殊的取值。

4.2.1.4 ELF的重要性

本节只是初步介绍了ELF文件的头结构。相信很多读者可能和笔者刚接触ELF一样,觉得它没有什么特别之处。实际不然,ELF文件非常重要,它涉及了程序 这里的程序指用C或C++编写的native程序。的编译链接及运行的方方面面,但同时它又太基础,以至于我们编程的时候几乎每天都接触它,但从来没有细致打量过它。那么,ELF到底有什么重要作用呢?

·ELF文件参与了源代码的编译和链接。比如一个C源文件,先被编译成Relocatable的.o文件。然后编译器处理这些.o文件。由于.o文件是可重定位的,所以里边的函数、变量、符号表等都可以调整到合适的位置。最终,这些.o文件会组成.so动态库文件或可执行文件。

·可执行程序的运行依赖ELF文件里的信息。比如,ELF文件头结构的e_entry就指明了可执行程序的入口地址。后面还将看到,ELF文件里还可以包含程序的代码段和数据段。这些内容都会按ELF文件对应成员字段所指定的位置加载到内存中去。当然,动态库文件的加载就更复杂了。

·ELF文件还可以包括丰富的调试信息以帮助我们调试程序。

再来看一个更深层次的问题。假设用C++源代码编译出一个ELF可执行程序,绝大部分情况下这个程序运行时:

·将调用操作系统提供的某些动态库中的函数。这些动态库可以是用C编写的,也可以是C++编写的。

·将借助system call调用操作系统提供的功能。

读者有没有想过,系统的动态库不是我们编写的,编写动态库的语言可以是C(而我们的程序是C++编写),OS更是早就存在了。为什么这个由我们后来创建的“物种”可以与那些早就存在的“物种”们完美合作

原来,参与这项合作的模块以及OS之间还需要遵循一种标准。这就是专门用于二进制模块之间以及模块和操作系统之间交互的ABI标准。ABI是Application Binary Interface(应用程序二进制接口)的缩写,它实际上是ELF相关规范在不同硬件平台上的进一步拓展和补充。比如,ABI会规定应用程序调用动态库的函数时,栈帧该如何创建,参数该如何传递。ABI和ELF是紧密相关的,它需要利用ELF中的某些信息。

综上,ELF不仅仅是一种文件格式,和它紧密相关的还有编译、链接、运行、ABI、调试等诸多内容。由此可见ELF的重要性真是非同一般。

在继续介绍ELF文件格式前,笔者先介绍下与ELF相关的规范。关于这些规范的详细说明,请读者阅读参考资料[3]

ELF和ABI相关的文档大体分为如下三块。

·通用ELF和ABI文档:ELF自身的文档叫Tool Interface Standard(TIS)Portable Formats Specificatoin。另外,通用的ABI标准名叫System V ABI,也被称为generic ABI(简写为gABI)。

·特定处理器(Processor specific,简写为ps)相关ELF和ABI文档:例如PA-RISC(HP公司的RISC芯片)平台的ELF补充文档、ARM平台的ABI文档、x86-64平台的ABI文档。

·特定平台/语言相关的ABI文档,比如Intel Itanium ABI文档、C++ABI For Intel Itanium平台。

接下来笔者将结合一个非常简单的示例程序来进一步介绍ELF,该示例程序如图4-4所示。

图4-4 示例代码

图4-4中左边为示例代码,右边是编译用的Makefile文件。示例代码本身非常简单,就是一个main函数,里边调用libc库提供的printf函数,然后返回。本示例中,先用"make exe"命令生成一个可执行程序main.out。

Linux系统中有一个工具叫readelf,它可以非常方便地查看ELF文件内容。图4-5所示为使用readelf查看main.out可执行程序ELF文件头结构的结果。

图4-5 readelf-h main.out

文件头结构比较简单,笔者不拟赘述。请读者结合本节对ELF文件头结构的介绍来分析图4-5所示的结果。

提示 readelf有很多选项,其中“-h”专门用来查看ELF文件头结构的信息。