ELF文件结构
ELF即“可执行可链接格式”,是COFF格式的变种。
Linux系统上所运行的就是ELF格式的文件,相关的定义在“/usr/include/elf.h”
偏移:是指段内某个存储单元相对该段首位置的差值,是一个16为的二进制代码
ELF文件的类型
编写一个C语言程序如下 elfDemo.c文件
#include<stdio.h>
int global_init_var = 10;
int global_uninit_var;
void func(int sum)
{
printf("%d\n",sum);
}
void main(){
static int local_static_init_var = 20;
static int local_static_uninit_var;
int local_init_var = 30;
int local_uninit_var;
func(global_init_var + local_init_var + local_static_init_var);
}
用下面四条命令分别进行编译,得到5个不同的目标文件(object file),分别是elfDemo.dyn、elfDemo.exec、elfDemo_pic.rel、elfDemo.rel和elfDemo_static.exec
从上面file命令的输出以及文件后缀可以看到,ELF文件分为三种类型,可执行文件(.exec)、可重定位文件(.rel)和共享目标文件(.dyn):
-
可执行文件:经过链接的、可执行的目标文件,通常也被称为程序
-
可重定位文件:由源文件编译而成且尚未链接的目标文件,通常以“.o”作为拓展名。用来与其他目标文件进行连接以构成可执行文件或动态链接库,通常是一段位置独立的代码(Position Independent Code,PIC)。
-
共享目标文件(shared object file):动态链接库文件。用于在链接过程中与其他动态链接或可重定位文件一起构建新的目标文件,或者在可执行文件加载时,链接到进程中作为运行代码的一部分。
除了上面三种类型,核心转储文件(Core Dump file)作为进程意外终止时进程地址空间的转存,也是ELF文件的一种。使用gdb读取这类文件可以辅助调试和查找程序崩溃的原因。
ELF文件的结构
在审视一个目标文件时,有两种视角可以选择,一种是链接视角,通过节来进行划分;另一种是运行视角,通过段来进行划分。
链接视角进行分析
通常目标文件(ELF)都会包括代码(.text)、数据(.data)、BSS(.bss)三个节。
代码节用于保存可执行的机器指令
数据节用于保存已初始化的全局变量和局部静态变量
BSS节则用于保存未初始化的全局变量和局部静态变量
除了上述三个节,简化的目标文件还应包括一个文件头部(ELF header)
File Header |
---|
.text section |
.data section |
.BSS section |
拓展:为什么将程序指令和程序数据分开存放?
从安全的角度讲,当程序被加载后,数据和指令分别被映射到两个虚拟区域。由于数据区域对于进程来说是可读写的,而指令区域对于进程来说是只读的,所以这两个虚拟区域的权限可以被分别设置成可读写和只读,防止程序的指令被改写和利用。
ELF文件头
值得注意的是文件头部存在魔术字符(7f 45 4c 46),即字符串"\177ELF",当文件被映射时到内存时,可以通过搜索该字符确定映射地址,这在dump(Linux dump功能是备份文件系统)内存时非常有用。
Elf64_Ehdr结构体如下所示
节头表
一个目标文件包括许多节,这些节的信息保存在节头表(section header table)中,表的每一项都是一个ELF64_Shdr结构体(也成为字描述符),记录了节的名字、长度、偏移、读写权限等信息。
**拓展:**节头表对于程序运行并不是必须的,因为它与程序内在布局无关,是程序头表的任务,所以常有程序取出节头表,以增加反编译器的分析难度。
ELF64_Shdr结构体如下
下面观察例子程序的.text、.data和.bss节。
首先是代码节。可以看到,Contents of section .text部分是.text数据的十六进制形式,总共0x50个字节,最左边一列是偏移量,中间四列是内容,最右边一列是ASCII码形式。Disassembly of section .text部分则是反汇编的结果。
接下来是数据节和只读数据节。可以看到.data节保存已经初始化的全局变量和局部静态变量。源代码中共有两个这样的变量:global_init_var(oa000000)和local_static_init_var(14000000),每一个变量四个字节,共八个字节。
.rodata节保存只读数据,包括只读变量和字符串常量。源代码中调用printf函数时,用到了一个字符串“%d\n“,它是一种只读数据,因此保存在.rodata中,可以看到字符串常量的ASCII形式,以”\0“结尾。
最后是BSS节,用于保存未初始化的全局变量和局部静态变量。如果仔细观察,会发现该节没有CONTENTS属性,这表示该节在文件中实际上并不存在,只是为变量预留了位置而已,因此该节的sh_offset域也就没有意义了。
节 名 | 说 明 |
---|---|
.comment | 版本控制信息,如编译器的版本 |
.debug_XXX | DWARF格式的调试信息 |
.strtab | 字符串表(string table) |
.shstrtab | 节名的字符串表 |
.symtab | 符号表(symbol table) |
.dynamic | ld.so使用的动态链接的信息 |
.dynsym | 动态链接的符号表 |
.dynstr | 动态链接的字符串表 |
.got | 全局偏移量表(global offest table),用于保存全局变量引用的地址 |
.got.plt | 全局偏移量表,用于保存函数引用的地址 |
.plt | 过程链接表(procedure linkage table),用于延迟绑定(lazy binding) |
.hash | 符号哈希表 |
.rela.dyn | 变量的动态重定位表(relocation table) |
.rel.text/rela.text | 静态重定位表 |
.rel.XXX/rela.XXX | 其他节的静态重定位表 |
.rela.plt | 函数的动态重定位表 |
.note.XXX | 额外的编译信息 |
.eh_frame | 用于操作异常的frame unwind信息 |
.init/.fini | 程序初始化和终止的代码 |
字符串表中包含了以null结尾的字符序列,用来表示符号名和节名,引用字符串时只需给出字符串序列在表中的偏移即可。字符串表的第一个字符和最后一个字符都是null字符,以确保所有字符串的开始和终止。
符号表记录了目标文件中所用到的所有符号信息,通常分为.dynsym和.symtab,前者是后者的子集。.dynsym保存了引用自外部文件的符号,只能在运行时被解析,而.symtab还保存了本地符号,用于调试和链接。
目标文件(ELF文件)通过一个符号在表中的索引值(字段里面存储的数据)来使用该符号。索引值从0开始计数,但值为0的表项不具有实际的意义,它表示未定义的符号。每个符号都有一个符号值(symbol value)。对于变量和函数,该值就是符号的地址。
Elf64_Sym结构体如下所示
重定位是连接字符定义与符号引用的过程。可重定位文件在构建可执行文件或共享目标文件时,需要把节中的符号引用换成这些符号在进程空间中的虚拟地址。包含这些转换信息的数据就是重定位项。
Elf64_Rel和Elf64_Rela结构体
r_offset是在重定位时需要被修改的符号的偏移。r_info分为两个部分:type指示如何修改引用,symbol指示应该修改引用为那个符号。r_addend用于对被修改的引用做偏移调整。
可执行文件的装载
从运行视角来进行审视。当运行一个可执行文件时,首先需要讲该文件和动态链接库装载到进程空间中,形成一个进程镜像。每个进程都拥有独立的虚拟地址空间,这个空间如何布局是由记录在段头表中的程序头(Program header)决定的ELF文件头的e_phoff域给出了段头表的位置
可以看到一个段可以包含多了节,相当于对这些节进行了分类,段的出现也时处于这个目的。
通常一个可执行文件至少有一个PT_LOAD类型的段,用于描述可装载的节,而动态链接的可执行文件则包括两个,将.data
和.text分开存放。动态段PT_DTNAMIC包括了一些动态连接器所必须的信息,如共享库列表、GOT表和重定位表等。PT_NOTE类型的段保存了系统相关的附加信息,虽然程序运行并不需要这些。PT-INTERP段将位置和大小信息存放在一个字符串中,是对程序解释器位置的描述。PT_PHDR段保存了程序头表本身的位置和大小。