在分析Binonic的Linker之前,我们先介绍Android的可执行文件格式,linker本事并不是很复杂,但是,如果对可执行文件格式不了解,就不太容易理解程序的逻辑。了解可执行文件的另一个好处是能开发出一些特殊功能的软件。Hook系统API软件
Android的可执行文件和动态库就是Linux的ELF文件格式,但是,由于使用了Android自己的linker,因此,和普通的Linux系统不完全兼容。
ELF是executable and Linking Format (可执行连接格式)的缩写,最初由UNIX系统实验室发布,它是应用程序二进制接口(Application Binary Interface,ABI)的一部分。ELF 标准的目的是为软件开发人员提供一组二进制接口定义,这些接口可以在多种操作系统下生效,从而减少开发的工作量。
ELF文件以节(Section)的方式组织在一起,“节”描述了文件的各项信息,例如代码,数据,符号表,重定位表,全局偏移表等。
可执行文件装载进内存时,并不是被“完整”的映射进内存,而是根据ELF文件中格式的定义,一段一段地装载进去。因此,可执行文件的格式和内存的映象并不完全相同,文件装载进内存后是以‘段’的方式来组织的,如代码段,数据段,动态段等。
ELF格式的文件有3种:可执行文件,动态库(.so文件)和重定位(.o文件)。
这3中文件都有一个ELF头,描述了整个可执行文件的基本信息,如目标代码的格式,体系结构,各种段或节的偏移和大小等。可执行文件和动态库中会有“程序头部表(porgram Header Table)、但是,重定位文件中没有程序头部表。此外。ELF文件中还会有一个“节区头部表”(Section Header Table) ,描述文件中各个 节区的内容。这个表的内容和程序头部表的内容有点重复,这是因为两张表的用途不一样。在编译和链接阶段(符号地址,不执行库文件)。也就在可执行文件的生成阶段,需要使用“节区头部表”,而可执行文件装载的时候使用的是程序头部表。
分析ELF格式文件的目的,是为了了解可执行文件的装载过程,因此,下面重点介绍,“程序头部表”及其相关的数据结构,对“节区头部表”有兴趣的读取可以了解本节的内容后自行分析。
3.5 图
XXX.C---->GCC----xxx.ELF(此时使用节区头部表)-----./(装载时使用程序头部表)------执行
本节的内容主要介绍32为可执行文件额格式,64位的格式除了一些字段的长度不同外,文件的组织方式是一样的。
ELF文件格式的数据结构和常量的文件是exec_elf.h,位于目录bionic/libc/include/sys下。其中ELF头定义如下:
typeof struct {
unsigned char e_ident[ELF_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_world e_flags; //文件相关的,特定于处理器的标志。
Elf32_Half e_ehsize; //ELF头部的字节大小
Elf32_Half e_phentsize; //程序头部表的表项的字节大小
Elf32_Half e_shentsize; //程序头部表的表项数目
Elf32_Half e_shnum; //节区头部表的表项的字节大小
Elf32_Half e_shstrndxl //节区头部表的表项数目
} Elf32_Ehdr;
在程序头部表里,最重要的是记录“程序头部表”和“节区头部表”的位置,表示表项数目和表项大小的字段。余下的字段中。
e_ident的16个字节标明了ELF文件的标志(7F+'E'+'L'+'F');
e_type表示文件类型,2表示可执行文件。
e_machine 表示机器类别,3表示386机器、8表示mips机器
e_entry表示程序的入口地址。
查看头程序的文件readelf和objdump这两个工具在prebuild目录下有多份,分别对应不同的平台。在Android5.0的源码里,可以使用的是目录prebuilts/gcc/linux-x86/arm/arm-eabi-4.8/bin下的工具
。
arm-eabi-readelf 有很多参数,其中-h参数能查看ELF文件的头部信息,
程序头部表
程序头部表的作用是记录文件中各种段的地址,大小等信息。在程序装在和连接时都需要它。
程序头部表示一个结构Elf32_Phdr的数组,每个结构中记录了装入内存中的各个段的信息,包括类型,地址,大小等。结构Elf32_Phdr的定义如下:
typedef struct{
Elf32_Word p_tpye; //段的类型
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;
同样,可以使用工具arm-eabi-readelf 来查看程序头部表的信息,这次需要使用的参数是"-l"
虽然“程序头部表”可能包含多个段,但是,只有类型为"PT_LOAD"的“段”才会从文件映射到内存中。其余类型的"段"如果有实际的节区,这些“节区”也会出现在"PT_LOAD"类型的“段”中。
图3.5中上半部分是程序头部表,可以看到它有8个“段”,图3.5的下半部分是这些“段”包含的“节区”。这些段中只有两个段的类型是“PT_LOAD”,因此,装载这个文件时,实际mmap进内存的也只有这两个“段”。它们就是所谓的代码段和数据段,从它们的属性也可以看出一个是“只读”,另一个是“读写”。
这两个“PT_LOAD段"在图3.5下半部分的对应关系位于第02项和第03项,从图3.5中可以看到他们包含了可执行文件的所有"节区"。而DYNAMIC段(第04项)只"包含"了一个".dynamic节区",这个"节区"和第03项中的".dynamic"是同一个。只不过".dynamic节区"的起始地址和大小等数据保存在“Dynamic段”中,只能通过“DYNAMIC”段来找到“.dynamic”节区,从而再找到“.plt”、“.dynsym”、“.got”等“节区”。相反,虽然“PT_LOAD”类型的“段”的地址空间范围覆盖了".dynmic节区"
但是无法通过它来找到“.dynamic节区”。这样设计的目的是,当系统装在可执行文件时只需要将"PT_LOAD"类型的"段"完整的映射进内存就完成了,而访问各个“节区”还是通过相应的段所记录的地址来完成。
与重定位相关的"节区"的信息------DYNAMIC段
"DYNAMIC段"描述的是与重定位相关的"节区"的信息。"DYNAMIC段"也是个数组,每项描述了"节区"的一些信息,一些复杂的"节区"需要好几项来共同说明,如".plt节区"就用了3项分别来描述"节区"的地址、大小、和"节区条目"的大小
“DYNAMIC段”的项数不是在文件的某个地方指定的,而是通过数组将最后一项的数据置为NULL来表示数组的结尾。
typedef struct{
Elf32_Word d_tag;
union {
Elf32_Addr d_ptr;
Elf32_Word d_val;
}d_un;
} Elf32_Dyn;
其中,d_tag 表示每项的类型。d_un是个联合,根据类型使用,器字段的作用是。
d_val 表示一个整数值,根据d_tag的类型不同有多种解释,如偏移、尺寸等。
d_ptr 表示"节对象"的虚拟地址