目录
本节内容:
- ELF文件类型
- ELF文件结构 从链接视角,运行视角分析
ELF文件类型
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 ):
- 可执行文件( executable file,.exec):经过链接的、可执行的目标文件,通常也被称为程序。
- 可重定位文件( relocatable file ,.rel):由源文件编译而成且尚未链接的目标文件,通常以“.o”作为扩展名。用于与其他目标文件进行链接以构成可执行文件或动态链接库,通常是一段位置独立的代码( Position Independent Code, PIC )。
- 共享目标文件( shared object file,.dyn):动态链接库文件。用于在链接过程中与其他动态链接库或可重定位文件一起构建新的目标文件,或者在可执行文件加载时,链接到进程中作为运行代码的一部分。
ELF文件结构
ELF文件被统称为 Object file,与之前的 .o 文件不同,遵循规范,提到目标文件就是指各种ELF文件。.o文件时重定位文件,包含有代码和数据,被链接成为可执行 .exec , .rel , .dyn
分析目标文件时,有两种视角可以选择:
- 链接视角:通过节(Section)划分目标文件结构;
- 运行视角:通过段(Segment)划分目标文件结构;
从程序的不同状态在内存的形态来分析的,链接时和运行时
通过链接视角分析目标文件
目标文件通常包含有 .text,.data,.bss文件 三个节,还有其他节以及文件头等
- .text:用于保存机器指令
- .data:用于保存已经初始化的全局变量和静态局部变量
- .bss:用于保存未初始化的全局变量和静态局部变量
ELF文件头(ELF Header)
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结构体如下
.text代码节
objdump -x -s -d elfDemo.rel
-x
:显示更详细的头部信息、段(section)信息和符号表等。-s
:显示每个段的内容,包括十六进制字节和对应的ASCII字符。-d
:进行反汇编,显示机器代码的汇编指令。
Contents of section .text
section .text部分是.text数据的十六进制形式,总共0x40个字节,最左边一列是偏移量,中间四列是内容,最右边一列是ASCII码形式。
Disassembly of section .text
是反汇编的结果。
.data 数据节 .rodata 只读数据节
.data节保存已经初始化的全局变量和局部静态变量。源代码中共有两个这样的变量: global_init_var ( 0a000000 )和 local_static_init_var ( 14000000 )每个变量4个字节,一共8个字节。
.rodata节保存只读数据,包括只读变量和字符串常量。源代码中调用printf()函数时,用到了一个字符串“%d\n”,它是一种只读数据,因此保存在.rodata节中,可以看到字符串常量的ASCII形式.以“\0”结尾。
.bss节
用于保存未初始化的全局变量和局部静态变量。如果仔细观察,会发现该节没有CONTENTS属性,这表示该节在文件中实际上并不存在,只是为变量预留了位置而已,因此该节的sh_offset域也就没有意义了。
其他常见的节
字符串节
字符串表中包含了以null结尾的字符序列,用来表示符号名和节名,引用字符串时只需给出字符序列在表中的偏移即可。字符串表的第一个字符和最后一个字符都是 null 字符,以确保所有字符串的开始和终止。
readelf -x .strtab elfDemo.rel
readelf -x .shstrtab elfDemo.rel
符号表
符号表记录了目标文件中所用到的所有符号信息,通常分为.dynsym和.symtab前者是后者的子集。
- .dynsym保存了引用自外部文件的符号,只能在运行时被解析;
- 而.symtab还保存了本地符号,用于调试和链接。
目标文件通过一个符号在表中的索引值来使用该符号。索引值从0开始计数,但值为0的表项不具有实际的意义,它表示未定义的符号。每个符号都有一个符号值( symbol value ),对于变量和函数,该值就是符号的地址。
raedelf -s elfDemo.rel
Elf64_sym结构体如下
重定位表
重定位是连接符号定义与符号引用的过程。可重定位文件在构建可执行文件或共享目标文件时,需要把节中的符号引用换成这些符号在进程空间中的虚拟地址。包含这些转换信息的数据就是重定位项( relocation entries )。
raedelf -r elfDemo.rel
offest:表示在重定位时需要被修改符号的偏移
INFO:TYPE,SYMBOL部分
- TYPE表示如何修改引用
- SYMBOL表示应该修改引用为哪个符号
ADDEND:用于对被修改的引用做偏移调整
Elf64_Rel Elf64_Rela 结构体如下
通过运行视角分析目标文件
当运行一个可执行文件时,首先需要将该文件和动态链接库装载到进程空间中,形成一个进程镜像。每个进程都拥有独立的虚拟地址空间,这个空间如何布局是由记录在段头表中的程序头( Program header)决定的。ELF文件头的e_phoff域给出了段头表的位置。
readelf -l elfDemo.exec
可以看到每个段都包含了一个或多个节,相当于是对这些节进行分组,段的出现也正是出于这个目的。随着节的数量增多,在进行内存映射时就产生空间和资源浪费的问题。实际上,系统并不关心每个节的实际内容,而是关心这些节的权限(读、写、执行),那么通过将不同权限的节分组,即可同时装载多个节,从而节省资源。例如.data和.bss 都具有读和写的权限,而.text和.plt.got则具有读和执行的权限。
常见的段:
- LOAD段:用于描述可装载的节,动态链接的可执行文件有两个:.data和.text文件分开存放。
- DYNAMIC段:包含一些动态链接器必须的信息,如共享列表,GOT表,重定位表
- NOTE段:保存系统相关的附加信息
- INSERT段:把位置和大小信息存放在一个字符串中,是对程序解释器的描述
- PHDR段:保存程序头表本身位置和大小
Elf64_Phdr结构体