赞
踩
ELF (Executable and Linkable Format)是 UNIX 类操作系统中普遍采用的目标文件格式, 分为三种类型:
1、可重定位文件 (Relocatable File)包含适合于与其他目标文件链接来创建可执行文件或者共享目标文件的代码和数据。
这是由汇编器汇编生成的 .o 文件。后面的链接器(link editor)拿一个或一些 Relocatable object files 作为输入,经链接处理后,生成一个可执行的对象文件 (Executable file) 或者一个可被共享的对象文件(Shared object file)。我们可以使用 ar 工具将众多的 .o Relocatable object files 归档(archive)成 .a 静态库文件。如何产生 Relocatable file,你应该很熟悉了,请参见我们相关的基本概念文章和JulWiki。另外,可以预先告诉大家的是我们的内核可加载模块 .ko 文件也是 Relocatable object file。
2、可执行文件 (Executable File)包含适合于执行的一个程序,此文件规定了 exec() 如何创建一个程序的进程映像。
这我们见的多了。文本编辑器vi、调式用的工具gdb、播放mp3歌曲的软件mplayer等等都是Executable object file。你应该已经知道,在我们的 Linux 系统里面,存在两种可执行的东西。除了这里说的 Executable object file,另外一种就是可执行的脚本(如shell脚本)。注意这些脚本不是 Executable object file,它们只是文本文件,但是执行这些脚本所用的解释器就是 Executable object file,比如 bash shell 程序。
3、共享目标文件(Shared Object File)包含可在两种上下文中链接的代码和数据。首先链接编辑器可以将它和其它可重定位文件和共享目标文件一起处理, 生成另外一个目标文件。其次,动态链接器(Dynamic Linker)可能将它与某个可执行文件以及其它共享目标一起组合,创建进程映像。
这些就是所谓的动态库文件,也即 .so 文件。这也是我们研究的重点,因为大多数apk 中包含的elf 就是so格式的这一类文件。是如果拿前面的静态库来生成可执行程序,那每个生成的可执行程序中都会有一份库代码的拷贝。如果在磁盘中存储这些可执行程序,那就会占用额外的磁盘空 间;另外如果拿它们放到Linux系统上一起运行,也会浪费掉宝贵的物理内存。如果将静态库换成动态库,那么这些问题都不会出现。动态库在发挥作用的过程 中,必须经过两个步骤:
a) 链接编辑器(link editor)拿它和其他Relocatable object file以及其他shared object file作为输入,经链接处理后,生存另外的 shared object file 或者 executable file。
b)在运行时,动态链接器(dynamic linker)拿它和一个Executable file以及另外一些 Shared object file 来一起处理,在Linux系统里面创建一个进程映像。
目标文件全部是程序的二进制表示,目的是直接在某种处理器上直接执行。
文章介绍过程中会使用到两个elf 的解析工具,objdump 和 readelf,并在文章中穿插出现。
ELF 文件在不同的视图下有不同的文件结构,共有两种视图: 链接视图和执行视图。
链接视图就是在链接时用到的视图,而执行视图则是在执行时用到的视图。上图左侧的视角是从链接来看的,右侧的视角是执行来看的。什么意思呢?就是同样的一份文件,段中会包含一个或者多个节区。节区的概念是在链接的过程中使用到的,但是在程序运行的时候,是关注整个段的,而不是节区的内容。
整个文件大致可以分为下面几个部分:
1、文件头:
ELF header: 描述整个文件的组织。
2、程序头表:
Program Header Table: 描述文件中的各种segments,用来告诉系统如何创建进程映像的。在程序运行的时候程序头表示需要关注的,反倒节区头表不是必须的。
3、节区头表:
Section Header Table: 包含了文件各个segction的属性信息,我们都将结合例子来解释。程序链接的时候节区头表是需要关注的,但是程序头表却是非必须。节区头部表包含了文件中所有节区的基本信息, 每一个节区在表中都有一个项, 描述了该节区的名字、大小、 偏移位置等关键信息。 节区头部表在链接的过程中指导链接器将相同属性(比如flg)的节区合并成为一个段,因此需要进行链接的文件必须包含节区头部表。
4、节区/段:
sections 或者 segments:segments是从运行的角度来描述elf文件,sections是从链接的角度来描述elf文件,也就是说,在链接阶段,我们可以忽略program header table来处理此文件,在运行阶段可以忽略section header table来处理此程序(所以很多加固手段删除了section header table)。
从图中我们也可以看出,segments与sections是包含的关系,一个segment包含若干个section。sections按照一定规则映射到segment。那么为什么需要区分两种不同视图?
当ELF文件被加载到内存中后,系统会将多个具有相同权限(flg值)section合并一个segment。操作系统往往以页为基本单位来管理内存分配,一般页的大小为4096B,即4KB的大小。同时,内存的权限管理的粒度也是以页为单位,页内的内存是具有同样的权限等属性,并且操作系统对内存的管理往往追求高效和高利用率这样的目标。ELF文件在被映射时,是以系统的页长度为单位的,那么每个section在映射时的长度都是系统页长度的整数倍,如果section的长度不是其整数倍,则导致多余部分也将占用一个页。而我们从上面的例子中知道,一个ELF文件具有很多的section,那么会导致内存浪费严重。这样可以减少页面内部的碎片,节省了空间,显著提高内存利用率。
需要注意地是:尽管图中显示的各个组成部分是有顺序的,实际上除了 ELF 头部表以外,其他节区和段都没有规定的顺序。
首先,我们先来看下32位ELF文件中常用的数据格式:
名称 | 大小 | 对齐 | 目的 |
---|---|---|---|
Elf32_Addr | 4 | 4 | 无符号程序地址 |
Elf32_Half | 2 | 2 | 无符号中等整数 |
Elf32_Off | 4 | 4 | 无符号文件偏移 |
Elf32_SWord | 4 | 4 | 有符号大整数 |
Elf32_Word | 4 | 4 | 无符号大整数 |
unsigned char | 1 | 1 | 无符号小整数 |
然后我们来观察一下ELF Header的结构体:
-
- #define EI_NIDENT 16
- 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;
e_ident : ELF的一些标识信息,前四位为.ELF,其他的信息比如大小端等
e_machine : 文件的目标体系架构,比如ARM
e_version : 0为非法版本,1为当前版本
e_entry : 程序入口的虚拟地址
e_phoff : 程序头部表偏移地址
e_shoff : 节区头部表偏移地址
e_flags :保存与文件相关的,特定于处理器的标志
e_ehsize :ELF头的大小
e_phentsize : 每个程序头部表的大小
e_phnum :程序头部表的数量
e_shentsize:每个节区头部表的大小
e_shnum : 节区头部表的数量
e_shstrndx:节区字符串表位置
或者使用010Editor的ELF模板也可以看到ELF Header结构。对比以下三类ELF文件,我们得到了以下结论:
1、e_type标识了文件类型
2、Relocatable File(.o文件)不需要执行,因此e_entry字段为0,且没有Program Header Table等执行视图
3、不同类型的ELF文件的Section也有较大区别,比如只有Relocatable File有.strtab节。
Shared Object File(.so文件)
Executable File(可执行文件android_server)
Relocatable File(.o文件)
详细字段含义介绍:
其中需要注意地是e_ident是一个16字节的数组,这个数组按位置从左到右都是有特定含义,每个数组元素的下标在标准中还存在别称,如byte0的下标0别名为EI_MAG0,具体如下:
名称 | 元素下标值 | 含义 |
---|---|---|
EI_MAG0 | 0 | 文件标识 |
EI_MAG1 | 1 | 文件标识 |
EI_MAG2 | 2 | 文件标识 |
EI_MAG3 | 3 | 文件标识 |
EI_CLASS | 4 | 文件类 |
EI_DATA | 5 | 数据编码 |
EI_VERSION | 6 | 文件版本 |
EI_PAD | 7 | 补齐字节开始处 |
EI_NIDENT | 16 | e_ident[]大小 |
e_ident[EI_MAG0]e_ident[EI_MAG3]即e_ident[0]e_ident[3]被称为魔数(Magic Number),其值一般为0x7f,‘E’,‘L’,‘F’。
e_ident[EI_CLASS](即e_ident[4])识别目标文件运行在目标机器的类别,取值可为三种值:ELFCLASSNONE(0)非法类别;ELFCLASS32(1)32位目标;ELFCLASS64(2)64位目标。
e_ident[EI_DATA](即e_ident[5]):给出处理器特定数据的数据编码方式。即大端还是小端方式。取值可为3种:ELFDATANONE(0)非法数据编码;ELFDATA2LSB(1)高位在前;ELFDATA2MSB(2)低位在前。
e_type表示elf文件的类型,如下定义:
名称 | 取值 | 含义 |
---|---|---|
ET_NONE | 0 | 未知目标文件格式 |
ET_REL | 1 | 可重定位文件 |
ET_EXEC | 2 | 可执行文件 |
ET_DYN | 3 | 共享目标文件 |
ET_CORE | 4 | Core 文件(转储格式) |
ET_LOPROC | 0xff00 | 特定处理器文件 |
ET_HIPROC | 0xffff | 特定处理器文件 |
ET_LOPROC~ET_HIPROC | 0xff00~0xffff | 特定处理器文件 |
e_machine表示目标体系结构类型:
名称 | 取值 | 含义 |
---|---|---|
EM_NONE | 0 | 未指定 |
EM_M32 | 1 | AT&T WE 32100 |
EM_SPARC | 2 | SPARC |
EM_386 | 3 | Intel 80386 |
EM_68K | 4 | Motorola 68000 |
EM_88K | 5 | Motorola 88000 |
EM_860 | 7 | Intel 80860 |
EM_MIPS | 8 | MIPS RS3000 |
others | 9~ | 预留 |
e_entry表示程序入口地址
这 个sum.o的进入点是0x0(e_entry),这表面Relocatable objects不会有程序进入点。所谓程序进入点是指当程序真正执行起来的时候,其第一条要运行的指令的运行时地址。因为Relocatable objects file只是供再链接而已,所以它不存在进入点。而可执行文件test和动态库.so都存在所谓的进入点,且可执行文件的e_entry指向C库中的_start,而动态库.so中的进入点指向 call_gmon_start。
如上图中e_entry = 0xD8B0,我们用ida打开该文件看到确实是_start()函数的地址。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。