ELF文件格式学习总结
1. 概述
2. 目标文件结构
3. ELF文件头
3.1 魔数
3.2 文件类型
3.3 机器类型
4. ELF文件内容
4.1段表
4.2字符串表(.**strtab)
4.3符号表
4.4重定位段(.rel.***)
1. 概述
ELF文件全称是Executable Linking Format(可执行连接格式),最初由unix系统实验室发布,它是应用程序二进制接口(Application Binary Interface,ABI)的一部分。在linux下,它是可执行文件的格式,由System V Release 4在COFF的基础上引入的,与之相对应的就是大家所熟悉的windows下的exe文件,exe文件是由microsoft基于COFF格式改变而成。
在Linux下使用ELF格式作为文件格式的主要有以下4类:
- 1.可重定位文件(Relocatable File),这类文件包含了代码和数据,可以被用来链接成可执行文件或共享目标文件,静态链接库也可以归为这一类,如经过预编译、编译、汇编后的.o文件,具体如图1所示。
图1
- 2.可执行文件(Executable File),这类文件包含了直接执行的程序,如/bin/bash等,具体如图2所示。
图2
- 3.共享目标文件(Shared Object File),链接器可以使用这种文件跟其他的可重定位文件和共享目标文件链接,产生新的目标文件;动态链接器可以将几个共享目标文件与可执行文件结合,作为进程映像的一部分来运行,如/lib/libc-2.15.so,具体如图3是所示。
图3
- 4.核心转储文件(Core Dump File),当进程意外终止时,系统可以将该进程的地址空间内容及终止时的一些其他信息转储到核心转储文件,如linux下的core dump文件。
在后面的总结中,主要是以目标文件(.o文件)为列子来对elf文件格式进行分析(其他的3种文件格式和目标文件格式差不多)。
2. 目标文件结构
目标文件,使用命令gcc从C源码文件开始,经过预编译、编译和汇编直接输出的文件,它和可执行文件的内容和结构十分相似。在目标文件中主要有编译后的机器指令代码、数据和链接时所需要的一些信息,具体如图4所示。
图4
从图4中可以看到目标文件其实可以分成2个部分。
- 其一,是ELF文件头,在该”文件头”中主要包括整个文件的文件属性、文件是否是可执行、是静态链接or动态链接及入口地址(主要对应程序main方法)、目标硬件、目标操作系统等。从文件头中还可以得到段表和段表字符串表的位置,通过它们可以解析整个ELF文件。
- 其二,是ELF内容部分,在文件头后面的部分都是属于ELF内容部分,它主要以”段”(section)的形式存储,比如保存程序指令的代码段(code section),代码段常见的名字有”.code”或”.text”;保存程序静态变量的数据段(data section),数据段的一般名字都叫”.data”。以C语言为例,执行语句进过编译后的机器代码保存在.test段;已经初始化的全局变量和局部静态变量都保存在.data段;而未初始化的全局变量和局部变量放在.bss段里,具体如图5所示。
图片5
3. ELF文件头
ELF文件头主要通过结构体Elf32_Ehdr(32位版)来定义的,在文件/usr/include/elf.h中,我们可以看到该结构体的详细定义,具体如图6所示。
图6
以32位版为例,在该结构体中自定义类型如表7所示。
字段名 | 描述 | 原始类型 | 字节 |
---|---|---|---|
Elf32_Addr | 本程序地址 | unit32_t | 4 |
Elf32_Half | 无符号短整型 | unit16_t | 2 |
Elf32_Off | 偏移地址 | unit32_t | 4 |
Elf32_Sword | 有符号整型 | unit32_t | 4 |
Elf32_Word | 无符号整型 | unit32_t | 4 |
通过readelf命令来对test.c文件编译出的test.o目标文件的ELF文件头进行查看,其结果如图8所示。
在图8中的各个参数项与ELF文件头结构体中的字段对应关系具体如表9所示。
字段 | 描述 | 对应图8中的位置 |
---|---|---|
e_ident | 包含5个参数,分别是class、data、version、os/ABI、ABI version | 0 |
e_type | Elf文件类型 | 1 |
e_machine | Elf文件的CPU平台 | 2 |
e_version | Elf版本号 | 3 |
e_entry | 程序的入口 | 4 |
e_phoff | 5 | |
e_shoff | 段表在文件中的偏移位置 | 6 |
e_word | Elf标志位 | 7 |
e_ehsize | Elf文件头本身大小,值为64byte表示test.o的文件头有64字节 | 8 |
e_phentsize | 9 | |
e_phnum | 10 | |
e_shentsize | 段表描述符的大小,即为 | 11 |
e_shnum | 段表描述符的数量,值为12表示test目标文件中有12个段 | 12 |
shstrndx | 段表字符串表所在段表数组的下标,值为9表示段表字符串表位于第9个段 | 13 |
3.1 魔数
在最前面的一项magic中的16个字节用来规定ELF文件的平台,最开始的4个字节是所有ELF文件都相同的标示码,分别为7f、45、4c、46。它们分别对应ascii码表中的delete、E、L、F字符,如图参考表10。
表10(ascii码表部分截图)
二进制 | 进制 | 进制 | 进制 | ||
---|---|---|---|---|---|
01000101 | 105 | 69 | 45 | E | 大写字母E |
01000110 | 106 | 70 | 46 | F | 大写字母F |
01000111 | 107 | 71 | 47 | G | 大写字母G |
01001000 | 110 | 72 | 48 | H | 大写字母H |
01001001 | 111 | 73 | 49 | I | 大写字母I |
01001010 | 112 | 74 | 4A | J | 大写字母J |
01001011 | 113 | 75 | 4B | K | 大写字母K |
01001100 | 114 | 76 | 4C | L | 大写字母L |
01111111 | 177 | 127 | 7F | DEL | (delete)删除 |
接下来的一个字节标示ELF文件类型,01表示32位的,02表示64位的,从图8中可以知道test.o为64位ELF文件类型。之后的一个字节表示是大端(big-endian)还是小端(little-endian),第7字节表示ELF文件主版本号,再后面几个字节无实际意义,默认填0。
3.2 文件类型
文件类型对应ELF文件头结构体中的e_type字段,该字段表示ELF文件类型,系统通过它来判断ELF文件的真正文件类型,其对应关系如表11所示。
常量 | 值 | 描述 |
---|---|---|
ET_REL | 1 | 可重定位文件,如.o目标文件 |
ET_EXEC | 2 | 可执行文件,如/bin/bash |
ET_DYN | 3 | 共享目标文件,如/lib/libc-2.15.so文件 |
3.3 机器类型
机器类型对应ELF文件头结构体中的e_machine字段,ELF文件被设计成可以在多个平台下使用,但是同一个ELF文件只能在制定的平台下使用。如图8中的e_machine的值为x86-64表示test.o文件只能在x86-64机器上是使用。
4. ELF文件内容
在ELF文件头之后的部分都属于ELF内容部分,在该部分主要包含的是机器指令,程序源码经过编译后生成的机器指令。它们主要以段来进行区分,如代码段进过编译后的机器指令存储在.code段或.text段中,全局变量个局部静态变量编译后的机器指令存放.data段中,各个段在ELF文件中的结构如图11所示。
而在ELF文件中具体有哪些段,各个段的具体结构是怎么组织的则是由段表(section table)决定的,通过段表可以找到各个段的具体位置、段的大小、读写权限等很多信息,反正ELF文件中的段的结构都是有段表决定给的,因此段表是ELF文件内容中非常重要的一部分。
4.1段表
段表是一个以Elf32_Shdr/Elf64_Shdr结构体(结构体具体定义见图12)为元素的数组,即简单的可以把它看成一个数组,数组的长度就表示在该ELf文件中有多少个段,如图8中number of section headers为12,那么Elf64_Shdr结构体为元素的数组长度为12,表示test.o目标文件中有12段;size of section headers为64byte,那么就表示Elf64_Shdr结构体的大小为64byte;start of section headers为378byte,表示段表在test.o文件中的偏移位置,即从文件中第378byte开始,之后的768byte(12*64byte)内容都为段表内容,通过该偏移位置ELF文件头就可以准确的找到段表位置。从另一个角度看段表也是ELf文件中的一个段,只不过这个段与前面提到的.text段或者.data段不同,它并没有包含程序运行的机器指令,它里面的内容主要是用来描述其他段的结构,通过命令readelf -S来查看test.o目标文件的段表结构如图13所示。
从图13中的第二行可以看到,Starting at offset 0x178即表示段表的位置是从178为开始的,注意这是16进制的178,换成10进制刚好就等于文件头记录的段表偏移位置378。而且从图13中可以看到该段表从0到11共12个Elf64_Shdr类型数组元素,其中数组除了第一个元素为无效的段描述符(它的类型为NULL),其他的都表示一个段,通过图13的描述,test.o目标文件中各个段的结构示意图如图14所示。
从图14中可以看到,该test.o文件的各个段的具体结构及段的大小,在图14中2个绿色的部分是系统为了字节对齐而产生的间隔。而在段表中有部分段的size为0,所以在图中并没有画出来,结束段.rela.en_frame的结束位置为0x00000690,即为1680字节,刚好是目标文件test.o的大小,具体如图15所示。
结构体Elf64_Shdr各个字段的含义如下:
- 字段sh_name
sh_name表示段名,各个段的实际段名字符串存储在.shstrtab段中,sh_name存的是在.shstrtab中的偏移。 - 字段sh_type
sh_type表示段的类型,对于编译器和链接器来说,一个段的到底是什么类型是由字段sh_type决定的,具体对应关系如表16所示。
常量 | 值 | 描述 |
---|---|---|
SHT_NULL | 0 | 无效段 |
SHT_PROGBITS | 1 | 程序段,代码段和数据段都是属于程序段 |
SHT_SYMTAB | 2 | 表示该段的内容为符号表 |
SHT_STRTAB | 3 | 表示该段的内容为字符串表 |
SHT_RELA | 4 | 重定位表 |
SHT_HASH | 5 | 符号表 |
SHT_DYNAMIC | 6 | 动态链接信息 |
SHT_NOTE | 7 | 提示性信息 |
SHT_NOBITS | 8 | 表示该段在文件中没有内容 |
SHT_REL | 9 | 表示该段包含重定位信息 |
SHT_SHLIB | 10 | 保留 |
SHT_DNYSYM | 11 | 动态链接的符号表 |
- 字段sh_flags
sh_flags表示段的标志位, - 字段sh_addr
sh_addr表示Section address段虚拟地址 - 字段sh_offset
sh_offset表示段在文件中的偏移位置 - 字段sh_size
sh_size表示段的大小 - 字段sh_link和sh_info
sh_link和sh_info和段的链接信息相关 - 字段sh_addralign
sh_addralign表示段地址对齐 - 字段sh_ensize
sh_ensize表示Section entry size项的长度
4.2字符串表(.**strtab
)
在ELF文件中用到的字段都集中的放在各个字段表中(命名为.**strtab
),字段表实际上也是一个段,里面存储的都是段名、变量名等,通过使用偏移位来定位需要使用的字符。例如表17中字符串。
偏移 | +0 | +1 | +2 | +3 | +4 | +5 | +6 | +7 | +8 | +9 |
---|---|---|---|---|---|---|---|---|---|---|
+0 | \0 | h | e | l | l | o | w | o | r | l |
+10 | d | \0 | m | y | v | a | r | i | a | b |
+20 | l | e | \0 |
在目标文件test.o中,.shstrtab段(段表字符串表)内容如图19所示,从118到171都为段表字符串表,段表字符串表中16进制数据对应ascii码如图中右边部分。
目标文件test.o中,段表部分内容如图20所示,如第一个段描述(NULL段的描述)位于0x00000178~0x000001b7,该部分内容全为0,而第二个段描述(.text段的描述)位于0x000001b8~0x000001f7,而在Elf64_Shdr结构体中的第一个字段为Elf64_word sh_name占用32bit,对应图20中的值为20 00,高位在后低位在前则实际值为32。在图19中查找偏移量为32的字符串则为.test(从118对应的字符串开始为第0个偏移量),刚好是第二个段的段名,其他的段名以此类推。
4.3符号表
源代码进过编译后,各个变量或者函数的机器指令都存在在不同的段中,通过多个目标文件进行链接就把多个目标文件拼在一起,其实质是目标之间的对地址的引用。如目标文件A中定义一个变量(或函数)var,目标文件B引用了该变量(或函数)var,为了避免链接过程中函数和变量之间的混淆,把函数和变量都统称为符号,函数名和变量名称为符号名,而整个链接过程都是基于这些符号进行的,所以目标文件中有一个相应的符号表。
符号表实际上是一个以结构体Elf32_Sym为元素的数组,同时它也是目标文件中的一个段(对应.symtab段),结构体Elf32_Sym的具体定义如图21所示。
各个字段的含义如表22所示。
字段 | 描述 |
---|---|
st_name | 符号名,实质是对应在字符表中的偏移量 |
st_value | 符号对应的偏移 |
st_size | 符号的大小 |
st_info | 该字段表示2个属性,符号类型和绑定信息 |
st_other | 未使用,默认为0 |
st_shndx | 符号所在的段位 |
通过命令readelf -s test.o查看到目标文件test.o的符号表(.symtab段)的描述如图23所示。
从图中看到有7个属性(value、size、type、bind、vis、bdx、name)刚好与结构体Elf32_Sym中的6个字段相对应(字段st_info对应type、bind这2个属性),总共14项及表示符号表对应的Elf32_Sym数组长度为14,而num为0的项表示是一个无效项。
- name属性对应st_name表示该符号的名字,它实质是一个字符表中的偏移量。
- Ndx属性对应st_shndx表示该符号的所在的段号,如num为13的项对应的段号为1即属于.text段。
- type和Bing属性对应st_info分别表示符号的类型和绑定信息,st_info的高28为表示绑定信息,低4为表示符号类型,其对应关系如表24所示。Num为12的项,对应的类型为STT_FUNC绑定信息为STB_GLOBAL即表示全局可见的函数。
符号绑定信息 | ||
---|---|---|
宏定义 | 值 | 说明 |
STB_LOCAL | 0 | 局部变量 |
STB_GLOBAL | 1 | 全局变量 |
STB_WEAK | 2 | 弱引用 |
符号类型 | ||
宏定义 | 值 | 说明 |
STT_NOTYPE | 0 | 表示未知类型符号 |
STT_OBJECT | 1 | 表示变量或数组 |
STT_FUNC | 2 | 表示函数或者其他可执行代码 |
STT_SECTION | 3 | 表示为文件中某个段 |
STT_FILE | 4 | 表示是文件名 |
- value属性和size属性分别对应st_value和st_size表示符号所在段中的偏移和符号的大小,如num为10的项,大小为4byte即为一个int型。
4.4重定位段(.rel.***
)
对于目标文件来说,它需要包含一个重定位表用来描述如何修改相应的段内容,该重定位表就是文件中的一个段,也叫重定位段。比如代码段.text有要重定位的地放,那么该文件中就会有一个名为.rel.text的段来保存重定位表。同理.data段对应的重定位表就位于.rel.text段(如果.rel.text段存在)。重定位表的结构实际上是一个以Elf32_Rel/Elf64_Rel结构体为元素的数组,数组中每个元素定义一个重定位入口。Elf32_Rel/Elf64_Rel结构定义如图25所示
其中各个字段的含义:
- 字段r_offset
字段r_offset表示offset属性即重定位入口的偏移 - 字段r_info
字段r_info表示2个属性,即type属性和name属性,分别表示重定位入口的类型和重定位入口的符号(实质是在符号表的偏移量)。
通过命令查看目标文件test.o的重定位表结构如图26所示。