2025-05-18 09:11:15 +08:00

14 KiB
Raw Blame History

[ELF文件格式]

ELF是程序文件的一种类型它并非由Linux研发但Linux采用这种类型管理程序文件具体又分为以下四类

  1. 可执行文件,可以自己在操作系统中执行的程序。

  2. 可重定向文件,不能自己在操作系统中执行的程序,其只有程序本身的数据,没有操作系统识别其各种属性以及辅助执行的数据,需要使用连接器添加属性信息、以及辅助执行代码制作为可执行文件才能在操作系统中执行,或用于被其它程序调用,与其它程序一起组合为可执行文件。

  3. 动态连接库文件,类似可重定向文件,不能自己执行,需要被其它程序调用执行,但是在执行期间与其组合为一体

  4. 核心转储文件,进程执行出错后操作系统将其终止执行,同时将进程在内存中的数据保存一份到辅存,用户可以读取核心转储文件查询进程终止前的执行状态与终止原因。 静态连接库不归类于ELF文件

ELF文件内的数据可以分为四类

  1. 文件头,文件内容起始数据,用于说明文件类型、文件属性。

  2. 程序节属性表,同节属性表,但只用于存储指令数据节、指令执行相关节的属性

  3. 英文名为section也有人称为段程序的指令数据、数学数据、运行相关属性数据会按作用分为多组每一组使用一个节存储。

  4. 节属性表,是一个元素为结构体的数组,每个元素存储一个节的属性信息。

文件头

文件头使用一个结构体定义以64位ELF文件为例文件头原型如下
	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-41表示可重定向文件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	

程序节属性表

  • 程序节属性表紧邻文件头,是一个数组,元素为结构体,专用于存储程序节的属性
	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可执行文件才有此节。

.dynsymDynamic Symbol存储动态库全局成员的属性类似.strtab。

.dynstrDynamic String存储动态库全局成员的名称每个名称都是以空字符结尾的字符串同时此节的第一个字节也定义为空字符。

指令数据节

.init存储程序执行入口指令用于程序执行前的基础初始化工作代码由编译器自动生成之后跳转到.text节执行存储指令数据的节会被操作系统设置为只读。

.pltprocedure linkage table调用动态库内数据相关内部是由编译器生成的多个汇编代码模块。

.text存储程序主体功能代码程序执行前复杂的初始化工作代码在这里编译器生成用户自己编写的代码也存储在这里。

.fini存储程序终止时执行的相关代码代码由编译器生成

数学数据节

.rodata存储全局常量此节分配的内存页会被操作系统设置为只读。

.init_array存储一组函数地址这些函数会在main函数之前执行。

.fini_array存储一组函数地址这些函数会在main函数之后执行。

.dynamic存储操作系统将程序加载到内存执行时需要使用的某些信息比如程序需要使用哪些动态库、某些节的虚拟地址具体数据的含义可使用 readelf -d 命令查看。

.gotglobal offset table主要存储glibc两个函数的地址__gmon_start__、__libc_start_main此地址在ELF文件中存储0程序执行时操作系统将gilbc读取到内存之后将函数的具体地址写入.got。

.data存储定义时已赋值的全局变量。

.bss存储定义时未赋值的全局变量编译后的文件此节不占用存储空间程序读取到内存执行时才会为此节分配存储空间此节占用的内存单元会全部设置为0定义全局变量不赋值的话初始值为0

编译器相关

.comment存储编译器版本信息内部是一个字符串

调试相关

.debug存储调试程序时需要的相关信息供调试器使用比如数据的类型、指令数据对应的高级语言源代码行号使用gcc编译程序时添加 -g 参数会生成此节。

全局成员相关

.symtabsymbol table程序中的全局变量、全局常量、函数这三者统称为symbol有人将其翻译为符号鉴于这种翻译完全不合理这里将其称为全局成员.symtab节用于存储所有全局成员的属性信息比如数据类型、数据长度、成员名称实际存储的是数据名在.strtab节的位置、成员是否可被外部程序调用每个全局成员的属性使用一个Elf64_Sym结构体存储所有的Elf64_Sym结构体组成一个数组这个数组就是.symtab节。

.strtabstring table存储全局成员symbol的名称对程序进行调试和反汇编时看见的数据名由此节记录使用gcc编译程序时可以添加-s参数不保留这些数据名。对于C语言程序此节记录的是数据名原型对于C++程序此节记录的并非原始数据名因为C++支持函数重载、符号重载、虚拟类型、命名空间,导致多个数据可以同名,编译器会在原始数据名的前后分别添加随机字符从而区分同名数据,另外类成员、命名空间成员还会包含类名、命名空间名

节名相关

.shstrtab存储其它节的名称为节设置名称是编译器的工作使用gcc编译程序时添加-s参数将不会生成节名称

节属性表

程序节之外的节使用Elf64_Shdr结构体存储其属性每个节的属性称为 section headers简称sh所有的 section headers 组合为节属性表。

	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确定要操作的地址

	#include <stdio.h>
	int a = 1;
	int main()
	{
		a = 9;
		printf("%d\n", a);
	
		return 0;
	}
	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地址404030rip现值 = 0x4011300x401130 + 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                        ;调用printfrip加-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。 地址码14字节存储rip要加的数据定位到操作的内存单元值为 00 00 2f 00。 地址码24字节存储要写入的立即数值为 00 00 00 09。 rip与一个4字节有符号数相加总共可以寻址约4GB内存空间前后各2GB。