241 lines
14 KiB
Markdown
241 lines
14 KiB
Markdown
![]() |
# [ELF文件格式]
|
|||
|
|
|||
|
ELF是程序文件的一种类型,它并非由Linux研发,但Linux采用这种类型管理程序文件,具体又分为以下四类
|
|||
|
|
|||
|
1. 可执行文件,可以自己在操作系统中执行的程序。
|
|||
|
|
|||
|
2. 可重定向文件,不能自己在操作系统中执行的程序,其只有程序本身的数据,没有操作系统识别其各种属性以及辅助执行的数据,需要使用连接器添加属性信息、以及辅助执行代码制作为可执行文件才能在操作系统中执行,或用于被其它程序调用,与其它程序一起组合为可执行文件。
|
|||
|
|
|||
|
3. 动态连接库文件,类似可重定向文件,不能自己执行,需要被其它程序调用执行,但是在执行期间与其组合为一体
|
|||
|
|
|||
|
4. 核心转储文件,进程执行出错后操作系统将其终止执行,同时将进程在内存中的数据保存一份到辅存,用户可以读取核心转储文件查询进程终止前的执行状态与终止原因。
|
|||
|
__静态连接库不归类于ELF文件__
|
|||
|
|
|||
|
## ELF文件内的数据可以分为四类:
|
|||
|
|
|||
|
1. 文件头,文件内容起始数据,用于说明文件类型、文件属性。
|
|||
|
|
|||
|
2. 程序节属性表,同节属性表,但只用于存储指令数据节、指令执行相关节的属性
|
|||
|
|
|||
|
3. 节,英文名为section,也有人称为段,程序的指令数据、数学数据、运行相关属性数据会按作用分为多组,每一组使用一个节存储。
|
|||
|
|
|||
|
4. 节属性表,是一个元素为结构体的数组,每个元素存储一个节的属性信息。
|
|||
|
|
|||
|
### 文件头
|
|||
|
文件头使用一个结构体定义,以64位ELF文件为例,文件头原型如下:
|
|||
|
```c
|
|||
|
typedef struct
|
|||
|
{
|
|||
|
unsigned char e_ident[16];
|
|||
|
//前4个字节数据分别为:7f 45 4c 46,第一个字节为删除键编码、后三个字节为ELF字母的编码,表示这是ELF文件
|
|||
|
//第5个字节说明ELF文件运行在32位处理器还是64位处理器,值为1表示32位,值为2表示64位
|
|||
|
//第6个字节说明数学数据使用的字节序,值为1表示小端序,值为2表示大端序
|
|||
|
//第7个字节说明ELF文件的版本,目前只能设置为1
|
|||
|
//第8个字节指定程序使用的ABI的类型,通常设置为0
|
|||
|
//第9个字节指定程序使用的ABI的版本,通常设置为0
|
|||
|
//第10-16字节保留不用,全部赋值为0
|
|||
|
|
|||
|
u16 e_type; //指定ELF文件的具体类型,值为1-4,1表示可重定向文件,2表示可执行文件,3表示动态连接库,4表示核心转储文件
|
|||
|
u16 e_machine; //指定程序使用的CPU类型,值为62表示64位X86处理器,值为40表示32位ARM处理器,值为183表示64位ARM处理器
|
|||
|
u32 e_version; //指定ELF文件的版本,目前只有一个版本,值固定为1
|
|||
|
|
|||
|
u64 e_entry; //指定程序执行入口虚拟地址,虚拟地址从0x400000开始算起,若为可重定向文件、动态连接库,则此值为0
|
|||
|
u64 e_phoff; //指定程序节属性表的文件内部地址,文件内部地址从0开始分配,文件头占用最开始的地址,程序节属性表在之后,若e_phoff为0表示不包含程序节属性表
|
|||
|
u64 e_shoff; //指定节属性表的文件内部地址,若为0表示不包含节属性表
|
|||
|
|
|||
|
u32 e_flags; //程序运行在特殊处理器中时使用此数据设置某些信息,运行在x86处理器中时此数据设置为0
|
|||
|
|
|||
|
u16 e_ehsize; //文件头的长度,以字节为单位
|
|||
|
|
|||
|
u16 e_phentsize; //程序节属性表的元素长度,以字节为单位
|
|||
|
u16 e_phnum; //程序节属性表的元素数量
|
|||
|
|
|||
|
u16 e_shentsize; //节属性表的元素长度,以字节为单位
|
|||
|
u16 e_shnum; //节属性表的元素数量
|
|||
|
|
|||
|
u16 e_shstrndx; //指定节属性表中哪个元素存储shstrtab节信息,值为元素下标
|
|||
|
} Elf64_Ehdr;
|
|||
|
```
|
|||
|
+ e_ident的第8-9个字节设置ABI信息,ABI全称为Application Binary Interface,中文名为应用程序二进制接口,它是一组规则的名称,是操作系统为程序制定的运行规则,比如:多字节数据排序方式、函数调用时参数如何传递、函数返回值如何存储、系统调用使用规则、异常功能使用规则,CPU在设计时会为了兼容ABI的某些规则进行优化,提升程序运行速度
|
|||
|
|
|||
|
#### 一个 x86-64 ELF 可执行文件的文件头
|
|||
|
```
|
|||
|
7F 45 4C 46 02 01 01 00 00 00 00 00 00 00 00 00 //e_ident
|
|||
|
02 00 3E 00 01 00 00 00 //e_type - e_version
|
|||
|
40 10 40 00 00 00 00 00 //e_entry
|
|||
|
40 00 00 00 00 00 00 00 //e_phoff
|
|||
|
18 39 00 00 00 00 00 00 //e_shoff
|
|||
|
00 00 00 00 40 00 38 00 //e_flags - e_phentsize
|
|||
|
0B 00 40 00 1D 00 1C 00 //e_phnum - e_shstrndx
|
|||
|
```
|
|||
|
|
|||
|
### 程序节属性表
|
|||
|
+ 程序节属性表紧邻文件头,是一个数组,元素为结构体,专用于存储程序节的属性
|
|||
|
```c
|
|||
|
typedef struct
|
|||
|
{
|
|||
|
u32 p_type; //指定节的类型
|
|||
|
u32 p_flags; //设置节的访问权限,最低位设置执行权限、第2位设置写权限、第3位设置读权限,对应位设置为1表示有此权限,指令节设置为可执行,全局变量节设置为可读可写,全局常量节设置为只读
|
|||
|
|
|||
|
u64 p_offset; //节的文件内部地址
|
|||
|
u64 p_vaddr; //节的虚拟地址
|
|||
|
u64 p_paddr; //节的内存物理地址,某些特殊操作系统需要使用,在linux中不使用此数据
|
|||
|
|
|||
|
u64 p_filesz; //ELF文件在辅存中存储时,此节的长度
|
|||
|
u64 p_memsz; //ELF文件读取到内存中时,此节的长度,有些节只在程序执行时才会存储数据,比如.bss节,这种节只在内存中分配存储空间
|
|||
|
u64 p_align; //节的内存地址对齐值,若值为0或1等同于无对齐要求
|
|||
|
}Elf64_Phdr;
|
|||
|
```
|
|||
|
```
|
|||
|
06 00 00 00 04 00 00 00 //p_type、p_flags
|
|||
|
40 00 00 00 00 00 00 00 //p_offset
|
|||
|
40 00 40 00 00 00 00 00 //p_vaddr
|
|||
|
40 00 40 00 00 00 00 00 //p_paddr
|
|||
|
68 02 00 00 00 00 00 00 //p_filesz
|
|||
|
68 02 00 00 00 00 00 00 //p_memsz
|
|||
|
08 00 00 00 00 00 00 00 //p_align
|
|||
|
```
|
|||
|
|
|||
|
### 节
|
|||
|
|
|||
|
__动态连接相关__
|
|||
|
```
|
|||
|
.interp,存储动态库加载器的路径,是一个字符串,ELF可执行文件才有此节。
|
|||
|
|
|||
|
.dynsym(Dynamic Symbol),存储动态库全局成员的属性,类似.strtab。
|
|||
|
|
|||
|
.dynstr(Dynamic String),存储动态库全局成员的名称,每个名称都是以空字符结尾的字符串,同时此节的第一个字节也定义为空字符。
|
|||
|
```
|
|||
|
__指令数据节__
|
|||
|
```
|
|||
|
.init,存储程序执行入口指令,用于程序执行前的基础初始化工作,代码由编译器自动生成,之后跳转到.text节执行,存储指令数据的节会被操作系统设置为只读。
|
|||
|
|
|||
|
.plt(procedure linkage table),调用动态库内数据相关,内部是由编译器生成的多个汇编代码模块。
|
|||
|
|
|||
|
.text,存储程序主体功能代码,程序执行前复杂的初始化工作代码在这里(编译器生成),用户自己编写的代码也存储在这里。
|
|||
|
|
|||
|
.fini,存储程序终止时执行的相关代码,代码由编译器生成
|
|||
|
```
|
|||
|
__数学数据节__
|
|||
|
```
|
|||
|
.rodata,存储全局常量,此节分配的内存页会被操作系统设置为只读。
|
|||
|
|
|||
|
.init_array,存储一组函数地址,这些函数会在main函数之前执行。
|
|||
|
|
|||
|
.fini_array,存储一组函数地址,这些函数会在main函数之后执行。
|
|||
|
|
|||
|
.dynamic,存储操作系统将程序加载到内存执行时需要使用的某些信息,比如程序需要使用哪些动态库、某些节的虚拟地址,具体数据的含义可使用 readelf -d 命令查看。
|
|||
|
|
|||
|
.got(global offset table),主要存储glibc两个函数的地址:__gmon_start__、__libc_start_main,此地址在ELF文件中存储0,程序执行时操作系统将gilbc读取到内存,之后将函数的具体地址写入.got。
|
|||
|
|
|||
|
.data,存储定义时已赋值的全局变量。
|
|||
|
|
|||
|
.bss,存储定义时未赋值的全局变量,编译后的文件此节不占用存储空间,程序读取到内存执行时才会为此节分配存储空间,此节占用的内存单元会全部设置为0,定义全局变量不赋值的话初始值为0
|
|||
|
```
|
|||
|
__编译器相关__
|
|||
|
```
|
|||
|
.comment,存储编译器版本信息,内部是一个字符串
|
|||
|
```
|
|||
|
__调试相关__
|
|||
|
```
|
|||
|
.debug,存储调试程序时需要的相关信息,供调试器使用,比如数据的类型、指令数据对应的高级语言源代码行号,使用gcc编译程序时添加 -g 参数会生成此节。
|
|||
|
```
|
|||
|
|
|||
|
__全局成员相关__
|
|||
|
```
|
|||
|
.symtab(symbol table),程序中的全局变量、全局常量、函数这三者统称为symbol,有人将其翻译为符号,鉴于这种翻译完全不合理,这里将其称为全局成员,.symtab节用于存储所有全局成员的属性信息,比如数据类型、数据长度、成员名称(实际存储的是数据名在.strtab节的位置)、成员是否可被外部程序调用,每个全局成员的属性使用一个Elf64_Sym结构体存储,所有的Elf64_Sym结构体组成一个数组,这个数组就是.symtab节。
|
|||
|
|
|||
|
.strtab(string table),存储全局成员(symbol)的名称,对程序进行调试和反汇编时看见的数据名由此节记录,使用gcc编译程序时可以添加-s参数不保留这些数据名。对于C语言程序,此节记录的是数据名原型,对于C++程序,此节记录的并非原始数据名,因为C++支持函数重载、符号重载、虚拟类型、命名空间,导致多个数据可以同名,编译器会在原始数据名的前后分别添加随机字符从而区分同名数据,另外类成员、命名空间成员还会包含类名、命名空间名
|
|||
|
```
|
|||
|
__节名相关__
|
|||
|
```
|
|||
|
.shstrtab,存储其它节的名称,为节设置名称是编译器的工作,使用gcc编译程序时添加-s参数将不会生成节名称
|
|||
|
```
|
|||
|
|
|||
|
### 节属性表
|
|||
|
`程序节之外的节使用Elf64_Shdr结构体存储其属性,每个节的属性称为 section headers,简称sh,所有的 section headers 组合为节属性表。`
|
|||
|
```c
|
|||
|
typedef struct
|
|||
|
{
|
|||
|
u32 sh_name; //存储本节的名称在shstrtab节中的位置,若为0则表示此节没有名称
|
|||
|
|
|||
|
u32 sh_type; //节的类型,常用值如下:
|
|||
|
//SHT_NULL,值为0,无效节
|
|||
|
//SHT_PROGBITS,值为1,存储程序运行所用数据的节,比如.text、.data、.rodata,不包含.bss
|
|||
|
//SHT_SYMTAB,值为2,.symtab节
|
|||
|
//SHT_STRTAB,值为3,字符串表,包括 .strtab .shstrtab .dynstr
|
|||
|
//SHT_RELA,值为4,.rela节
|
|||
|
//SHT_HASH,值为5,.hash节
|
|||
|
//SHT_DYNAMIC,值为6,.dynamic节
|
|||
|
//SHT_NOTE,值为7,此节包含一些注释信息
|
|||
|
//SHT_NOBITS,值为8,此节不在ELF文件中,而是在程序执行期间分配内存空间,比如.bss
|
|||
|
//SHT_REL,值为9,.rel节
|
|||
|
//SHT_SHLIB,值为10,保留节,具体含义未定义
|
|||
|
//SHT_DYNSYM,值为11,.dynsym节
|
|||
|
|
|||
|
u64 sh_flags; //节的访问权限
|
|||
|
|
|||
|
u64 sh_addr; //节的虚拟地址,若为0则程序执行时此节不会读取到内存中,因为这些节只起辅助作用
|
|||
|
u64 sh_offset; //节的文件内部地址
|
|||
|
u64 sh_size; //节的长度,单位为字节
|
|||
|
|
|||
|
u32 sh_link; //节属性表中另一个元素的下标,连接器有时需要知道两个节的联系,若不需要与另一个节有关联则设置为0
|
|||
|
u32 sh_info; //附加信息
|
|||
|
u64 sh_addralign; //节的内存地址对齐值,若值为0或1则表示无对齐要求
|
|||
|
u64 sh_entsize; //有些节的内容是一个数组,此成员存储数组元素的长度,若节的内容不是数组则设置为0
|
|||
|
} Elf64_Shdr;
|
|||
|
```
|
|||
|
```
|
|||
|
// 一个 x86-64 ELF 可执行文件的 .shstrtab 节属性信息:
|
|||
|
|
|||
|
11 00 00 00 03 00 00 00 //sh_name - sh_type
|
|||
|
00 00 00 00 00 00 00 00 //sh_flags
|
|||
|
00 00 00 00 00 00 00 00 //sh_addr
|
|||
|
2d 38 00 00 00 00 00 00 //sh_offset
|
|||
|
03 01 00 00 00 00 00 00 //sh_size
|
|||
|
00 00 00 00 00 00 00 00 //sh_link - sh_info
|
|||
|
01 00 00 00 00 00 00 00 //sh_addralign
|
|||
|
00 00 00 00 00 00 00 00 //sh_entsize
|
|||
|
```
|
|||
|
|
|||
|
## RIP相对寻址
|
|||
|
`在x86处理器中,指令使用32位内存地址读写内存,在x86-64处理器中,指令使用64位内存地址读写内存,64位处理器的mov指令使用直接内存寻址的方式读写内存数据时,若使用立即数存储要操作的内存地址则指令长度会超标(指令操作码 + 8字节内存地址 + 寄存器编号或4字节立即数),为此x86-64新增了一种内存寻址方式,称为RIP相对寻址,其让RIP寄存器增加一个值得出要操作的内存地址,就像跳转指令使用IP寄存器加一个立即数得出要跳转到的地址一样`
|
|||
|
|
|||
|
`RIP相对寻址用于程序的全局数据寻址,局部数据在栈中存储,栈空间数据不使用rip相对寻址,而是使用rsp、rbp确定要操作的地址`
|
|||
|
|
|||
|
```c
|
|||
|
#include <stdio.h>
|
|||
|
int a = 1;
|
|||
|
int main()
|
|||
|
{
|
|||
|
a = 9;
|
|||
|
printf("%d\n", a);
|
|||
|
|
|||
|
return 0;
|
|||
|
}
|
|||
|
```
|
|||
|
```asm
|
|||
|
0000000000401122 <main>:
|
|||
|
401122: 55 push rbp
|
|||
|
401123: 48 89 e5 mov rbp,rsp
|
|||
|
401126: c7 05 00 2f 00 00 09 00 00 00 mov DWORD PTR [rip+0x2f00],0x9 ;调用变量a(地址404030),rip现值 = 0x401130,0x401130 + 0x2f00 = 0x404030
|
|||
|
401130: 8b 05 fa 2e 00 00 mov eax,DWORD PTR [rip+0x2efa] ;变量a写入eax
|
|||
|
401136: 89 c6 mov esi,eax
|
|||
|
401138: bf 04 20 40 00 mov edi,0x402004
|
|||
|
40113d: b8 00 00 00 00 mov eax,0x0
|
|||
|
401142: e8 e9 fe ff ff call 401030 ;调用printf,rip加-0x117补码,向前跳转到0x401030
|
|||
|
401147: b8 00 00 00 00 mov eax,0x0
|
|||
|
40114c: 5d pop rbp
|
|||
|
40114d: c3 ret
|
|||
|
|
|||
|
Contents of section .data:
|
|||
|
404020 00000000 00000000 00000000 00000000
|
|||
|
404030 01000000
|
|||
|
```
|
|||
|
地址401126处的mov指令长度10字节,分别如下:
|
|||
|
|
|||
|
操作码,2字节,值为 05 c7。
|
|||
|
地址码1,4字节,存储rip要加的数据,定位到操作的内存单元,值为 00 00 2f 00。
|
|||
|
地址码2,4字节,存储要写入的立即数,值为 00 00 00 09。
|
|||
|
rip与一个4字节有符号数相加,总共可以寻址约4GB内存空间,前后各2GB。
|