ELF(Executable and Linkable Format),即“可执行可链接格式”,Linux 系统上所运行的就是 ELF 格式的文件,相关定义在“/usr/include/elf.h”文件里。
测试代码如下
#include<stdio.h>
int global_init_var = 10;
int gloabal_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_val = 30;
int local_uninit_var;
func(global_init_var + local_init_val + local_static_init_var);
}
使用下面4条命令分别进行编译可以得到5个不同的目标文件( object file ),分别是elfDemo.dyn、elfDemo.exec、elfDemo pic.rel、elfDemo.rel 和 elfDemo_static.exec。
gcc elfDemo.c -o elfDemo.exec
gcc -static elfDemo.c -o elfDemo_static.exec
gcc -c elfDemo.c -o elfDemo.rel
gcc -c -fPIC elfDemo.c -o elfDemo_pic.rel && gcc -shared elfDemo_pic.rel -o elfDemo.dyn
从上面 file命令的输出以及文件后缀可以看到,ELF文件分为三种类型,可执行文件(.exec可重定位文件( .rel)和共享目标文件( .dyn ):
ELF文件被统称为 Object file,与之前的 .o 文件不同,遵循规范,提到目标文件就是指各种ELF文件。.o文件时重定位文件,包含有代码和数据,被链接成为可执行 .exec , .rel , .dyn
分析目标文件时,有两种视角可以选择:
从程序的不同状态在内存的形态来分析的,链接时和运行时
目标文件通常包含有 .text,.data,.bss文件 三个节,还有其他节以及文件头等
ELF文件头(ELF header )位于目标文件最开始的位置,包含描述整个文件的一些基本信息,例如ELF文件类型、版本/ABI版本、目标机器、程序人口、段表和节表的位置和长度等。文件头部存在魔术字符(7f 45 4c 46 ),即字符串“\177ELF”,当文件被映射到内存时,可以通过搜索该字符确定映射地址,这在 dump内存时非常有用。
查看elf文件头信息
readelf -h elfDemo.rel
ELF Header本质是一个结构体
Elf64_Ehdr结构体如下:
一个目标文件中包含许多节,节的信息保存在节头表( Section header table )中,表的每一项都是一个 Elf64_Shdr结构体(也称为节描述符),记录了节的名字、长度、偏移、读写权限等信息。节头表的位置记录在文件头的e_shoff域中。
节头表对于程序运行并不是必须的,因为它与程序内存布局无关,是程序头表的任务,所以常有程序去除节头表,以增加反编译器的分析难度。
查看目标文件节头表
readelf -S elfDemo.rel
接下来使用objdump工具分析程序,objdump是一个用于分析和显示目标文件(object file)或可执行文件的工具。它可以提供有关二进制文件的各种信息,包括头部信息、符号表、段信息、反汇编代码等。
Elf64_shdr结构体如下
objdump -x -s -d elfDemo.rel
Contents of section .text
section .text部分是.text数据的十六进制形式,总共0x40个字节,最左边一列是偏移量,中间四列是内容,最右边一列是ASCII码形式。
Disassembly of section .text
是反汇编的结果。
.data节保存已经初始化的全局变量和局部静态变量。源代码中共有两个这样的变量: global_init_var ( 0a000000 )和 local_static_init_var ( 14000000 )每个变量4个字节,一共8个字节。
.rodata节保存只读数据,包括只读变量和字符串常量。源代码中调用printf()函数时,用到了一个字符串“%d\n”,它是一种只读数据,因此保存在.rodata节中,可以看到字符串常量的ASCII形式.以“\0”结尾。
用于保存未初始化的全局变量和局部静态变量。如果仔细观察,会发现该节没有CONTENTS属性,这表示该节在文件中实际上并不存在,只是为变量预留了位置而已,因此该节的sh_offset域也就没有意义了。
字符串表中包含了以null结尾的字符序列,用来表示符号名和节名,引用字符串时只需给出字符序列在表中的偏移即可。字符串表的第一个字符和最后一个字符都是 null 字符,以确保所有字符串的开始和终止。
readelf -x .strtab elfDemo.rel
readelf -x .shstrtab elfDemo.rel
符号表记录了目标文件中所用到的所有符号信息,通常分为.dynsym和.symtab前者是后者的子集。
目标文件通过一个符号在表中的索引值来使用该符号。索引值从0开始计数,但值为0的表项不具有实际的意义,它表示未定义的符号。每个符号都有一个符号值( symbol value ),对于变量和函数,该值就是符号的地址。
raedelf -s elfDemo.rel
Elf64_sym结构体如下
重定位是连接符号定义与符号引用的过程。可重定位文件在构建可执行文件或共享目标文件时,需要把节中的符号引用换成这些符号在进程空间中的虚拟地址。包含这些转换信息的数据就是重定位项( relocation entries )。
raedelf -r elfDemo.rel
offest:表示在重定位时需要被修改符号的偏移
INFO:TYPE,SYMBOL部分
ADDEND:用于对被修改的引用做偏移调整
Elf64_Rel Elf64_Rela 结构体如下
当运行一个可执行文件时,首先需要将该文件和动态链接库装载到进程空间中,形成一个进程镜像。每个进程都拥有独立的虚拟地址空间,这个空间如何布局是由记录在段头表中的程序头( Program header)决定的。ELF文件头的e_phoff域给出了段头表的位置。
readelf -l elfDemo.exec
可以看到每个段都包含了一个或多个节,相当于是对这些节进行分组,段的出现也正是出于这个目的。随着节的数量增多,在进行内存映射时就产生空间和资源浪费的问题。实际上,系统并不关心每个节的实际内容,而是关心这些节的权限(读、写、执行),那么通过将不同权限的节分组,即可同时装载多个节,从而节省资源。例如.data和.bss 都具有读和写的权限,而.text和.plt.got则具有读和执行的权限。
常见的段:
Elf64_Phdr结构体