ELF 文件格式


一、概述

ELF(Executable and Linkable Format)是一种常用的可执行文件和可链接库格式。它是一种通用的二进制文件格式,用于在 Unix 和类 Unix 系统上进行可执行文件和库的存储和传输。

二、种类

ELF文件主要有四种类型

  1. 可执行文件(Executable File):包含可执行的机器代码,可以直接在操作系统中运行。
  2. 可重定位文件(Relocatable File):机器代码和数据,但其地址空间是相对的,需要进行重定位才能正确运行。可重定位文件通常用于静态库和动态库的编译过程。
  3. 共享对象文件(Shared Object File):这种类型的 ELF 文件是一种动态链接库(DLL),它包含了可共享的代码和数据,可以在运行时被多个进程共享。
  4. 核心转储文件(Core Dump File):这种类型的 ELF 文件是操作系统在程序崩溃或异常终止时生成的,它包含了程序在崩溃时的内存状态和寄存器信息,用于调试和故障排除。

在内核中有如下定义(/include/uapi/linux/elf.h):

#define  ET_REL   1
#define  ET_EXEC  2
#define  ET_DYN   3
#define  ET_CORE  4

三、布局

下面是ELF文件的组成格式的详细介绍:

  • ELF 头部ELF Header):ELF文件的开头是一个固定大小的ELF头部,它包含了文件的基本信息和属性,如ELF文件的类型、目标体系结构、入口点地址、程序头表和节头表的偏移和大小等。
  • 程序头表Program Header Table):程序头表是一个包含多个程序头表条目(Program Header Entry)的表。每个程序头表条目描述了如何将文件中的段(Segment)加载到内存中的某个位置。段是一个逻辑上相关的一组节,如代码段、数据段等。程序头表主要用于可执行文件和共享对象,对于可重定位文件,它可能为空。
  • 节头表Section Header Table):节头表是一个包含多个节头表条目(Section Header Entry)的表。每个节头表条目描述了一个节的属性和位置信息,如节的名称、类型、大小、在文件中的偏移等。节头表存储了 ELF 文件中所有的节,如代码节、数据节、符号表节等。
  • 节区Section):节是 ELF 文件中的基本组成单位,它包含了特定类型的数据。ELF 文件的各种信息和数据都存储在不同的节中,如代码节存储了可执行代码,数据节存储了全局变量和静态数据等。节区可以包含额外的属性和标记,如只读、可执行等。
  • 符号表Symbol Table):符号表是一个特殊的节,它包含了程序中定义和引用的符号(如变量、函数等)的信息,如符号的名称、类型、地址等。符号表在链接和调试过程中非常有用。
  • 字符串表String Table):字符串表是一个特殊的节,它包含了各种节的名称、符号表的字符串等。它通过索引来引用其他节或符号的名称,提供了更方便的字符串访问方法。

可以看到,其实 sections 和 segments 占的是一样的地方。这是从链接和加载的角度来讲的。左边是链接视图,右边是加载视图,sections 是程序员可见的,是给链接器使用的概念,而 segments 是程序员不可见的,是给加载器使用的概念。一般是一个 segment 包含多个 section。

链接视图是以节(section)为单位,执行视图是以段(segment)为单位。链接视图就是在链接时用到的视图,而执行视图则是在执行时用到的视图。目标文件 .o 里的代码段 .text 是 section(汇编中 .text 同理),当多个可重定向文件最终要整合成一个可执行的文件的时候(链接过程),链接器把目标文件中相同的 section 整合成一个segment,在程序运行的时候,方便加载器的加载。

四、组成结构

下面是定义中的各个类型数据结构的大小:

NameSizeAlignmentPurpose
Elf32_Addr44Unsigned program address
Elf32_Off44Unsigned file offset
Elf32_Half44Unsigned medium interger
Elf32_Word44Unsigned interger
Elf32_Sword44Signed interger
typedef uint32_t Elf32_Addr;
typedef uint32_t Elf32_Off;
typedef uint16_t Elf32_Half;
typedef uint32_t Elf32_Word;
typedef int32_t Elf32_Sword;

1、ELF header

#define EI_NIDENT 16
typedef struct{
    /*ELF的一些标识信息,固定值*/
    unsigned char e_ident[EI_NIDENT];
    Elf32_Half e_type;      /* 目标文件类型:1-可重定位文件,2-可执行文件,3-共享目标文件等 */
    Elf32_Half e_machine;   /* 文件的目标体系结构类型:3-intel 80386 */    
    Elf32_Word e_version;   /* 目标文件版本:1-当前版本 */    
    Elf32_Addr e_entry;  	/* 程序入口的虚拟地址,如果没有入口,可为0 */    
    Elf32_Off e_phoff; 		/* 程序头表(segment header table)的偏移量,如果没有,可为0 */    
    Elf32_Off e_shoff; 		/* 节区头表(section header table)的偏移量,没有可为0 */    
    Elf32_Word e_flags; 	/* 与文件相关的,特定于处理器的标志 */    
    Elf32_Half e_ehsize; 	/* ELF头部的大小,单位字节 */    
    Elf32_Half e_phentsize; /* 程序头表每个表项的大小,单位字节 */
    Elf32_Half e_phnum; 	/* 程序头表表项的个数 */
    Elf32_Half e_shentsize; /* 节区头表每个表项的大小,单位字节 */
    Elf32_Half e_shnum; 	/* 节区头表表项的数目 */
    Elf32_Half e_shstrndx;  /* 某些节区中包含固定大小的项目,如符号表。对于这类节区,此成员给出每个表项的长度字节数 */
}Elf32_Ehdr;

下面是 test 的 ELF header 结构各个数据成员对应的值:

从输出可以看到这个 ELF 文件的基本消息,比如 Section header table 有 31 个 section;从 14824 byte 处开始,Program header table 中有 13 个 segment,每个 56 byte。

  • e_ident[EI_NIDENT]

文件的标识以及标识描述了 ELF 如何编码等信息。

Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
名称取值目的
EI_MAG00文件标识(0x7f)
EI_MAG11文件标识(E)
EI_MAG22文件标识(L)
EI_MAG33文件标识(F)
EI_CLASS4文件类
EI_DATA5数据编码
EI_VERSION6文件版本
EI_PAD7补齐字节开始处
EI_NIDENT16e_ident[]大小
  • e_type

该数据类型是 uint16_t 数据类型的。通过字段查看,可以看到这个值为 00 02。表格定义如下:

名称取值含义
ET_NONE0x0000未知目标文件格式
ET_ERL0x0001可重定位文件
ET_EXEC0x0002可执行文件
ET_DYN0x0003共享目标文件
ET_CORE0x0004Core文件(转储格式)
ET_LOPROC0xff00特定处理器文件
ET_HIPROC0xffff特定处理器文件
  • e_machine

由字段可以看到为 00 03,关于这个字段的解析,基本上就是表示该elf文件是针对哪个处理器架构的。

下面只列出几个常见的架构的序号

名称取值含义
EM_NONE0No machine
EM_SPARC2SPARC
EM_3863Intel 80386
EM_MIPS8MIPS I Architecture
EM_PPC0x14PowerPC
EM_ARM0x28Advanced RISC Machines ARM

2、程序头表和程序头表条目

程序头表是从加载的角度来看 ELF 文件的,目标文件没有该表,每一个表项提供了各段在虚拟地址空间和物理地址空间的大小、位置、标志、访问权限和对其方面的信息。从上面知道,test 中有 13 个 segment,如下图:

下面对其中的一些进行简单的介绍。

  • PHDR 保存程序头表
  • INTERP 指定在程序已经从可执行文件映射到内存之后,必须调用的解释器。在这里,解释器并不意味着二进制文件的内容必须由另一个程序解释。它指的是这样一个程序:通过链接其他库,来满足未解决的引用。通常 /lib/ld-linux.so.2/lib/ld-linux-ia-64.so.2 等库,用于在虚拟地址空间中插入程序运行所需要的动态库。对几乎所有的程序来说,可能 C 标准库都是必须映射的。还需要添加的各种库包括,GTK、数学库、libjpeg 等等
  • LOAD 表示一个需要从二进制文件映射到虚拟地址空间的段。其中保存了常量数据(如字符串),程序的目标代码等。
  • DYNAMIC 段保存了由动态链接器(即,INTERP中指定的解释器)使用的信息。
  • NOTE 保存了专有信息

一个 entry 对应一个 segment,由如下的数据结构表示

typedef struct
{   
    Elf32_Word p_type;   /* segment的类型:PT_LOAD= 1 可加载的段 */    
    Elf32_Off p_offset;  /* 从文件头到该段第一个字节的偏移 */
    Elf32_Addr p_vaddr;  /* 该段第一个字节被放到内存中的虚拟地址 */  
    Elf32_Addr p_paddr;  /* 在linux中这个成员没有任何意义,值与p_vaddr相同 */
    Elf32_Word p_filesz; /* 该段在文件映像中所占的字节数 */
    Elf32_Word p_memsz;  /* 该段在内存映像中占用的字节数 */
    Elf32_Word p_flags;  /* 段标志 */
    Elf32_Word p_align;  /* p_vaddr是否对齐 */
} Elf32_phdr;

3、节头表和节头表条目

节表头包含了文件中的各个节,每个节都指定了一个类型,定义了节数据的语义。各节都指定了大小和在二进制文件内部的偏移。从上面知道,test 中有 31 个 section,如下图:


下面对其中的一些进行简单的介绍:

  • .interp 保存了解释器的文件名,这是一个ASCII字符串
  • .data 保存初始化的数据,这是普通程序数据一部分,可以再程序运行时修改
  • .rodata 保存了只读数据,可以读取但不能修改。例如,编译器将出现在printf语句中的所有静态字符串封装到该节
  • .init.fini 保存了进程初始化和结束所用的代码,这两个节通常都是由编译器自动添加
  • .gnu.hash 是一个散列表,允许在不对全表元素进行线性搜索的情况下,快速访问所有的符号表项

section 的结构定义如下:

typedef struct{
	Elf32_Word sh_name;		  /* 节区名称 */
    Elf32_Word sh_type;		  /* 节区类型:PROGBITS-程序定义的信息,NOBITS-不占用文件空间(bss),REL-重定位表项 */
    Elf32_Word sh_flags;	  /* 每一bit位代表一种信息,表示节区内的内容是否可以修改,是否可执行等信息 */  
    Elf32_Addr sh_addr;	      /* 如果节区将出现在进程的内存影响中,此成员给出节区的第一个字节应处的位置 */
    Elf32_Off sh_offset;	  /* 节区的第一个字节与文件头之间的偏移 */ 
    Elf32_Word sh_size;		  /* 节区的长度,单位字节,NOBITS虽然这个值非0但不占文件中的空间 */
    Elf32_Word sh_link;    	  /* 节区头部表索引链接 */
    Elf32_Word sh_info;       /* 节区附加信息 */
    Elf32_Word sh_addralign;  /* 节区带有地址对齐的约束 */
    Elf32_Word sh_entsize;    /* 某些节区中包含固定大小的项目,如符号表,那么这个成员给出其固定大小 */
}Elf32_Shdr;

4、系统固定的 section

名称类型属性含义
.bssSHT_NOBITSSHF_ALLOC + SHF_WRITE包含将出现在程序的内存映像中的为初始化数据。根据定义,当程序开始执行,系统将把这些数据初始化为 0。此节区不占用文件空间。
.commentSHT_PROGBITS(无)包含版本控制信息。
.dataSHT_PROGBITSSHF_ALLOC + SHF_WRITE这些节区包含初始化了的数据,将出现在程序的内存映像中。
.data1SHT_PROGBITSSHF_ALLOC + SHF_WRITE这些节区包含初始化了的数据,将出现在程序的内存映像中。
.debugSHT_PROGBITS(无)此节区包含用于符号调试的信息。
.dynamicSHT_DYNAMIC此节区包含动态链接信息。节区的属性将包含 SHF_ALLOC 位。是否 SHF_WRITE 位被设置取决于处理器。
.dynstrSHT_STRTABSHF_ALLOC此节区包含用于动态链接的字符串,大多数情况下这些字符串代表了与符号表项相关的名称。
.dynsymSHT_DYNSYMSHF_ALLOC此节区包含了动态链接符号表。
.finiSHT_PROGBITSSHF_ALLOC + SHF_EXECINSTR此节区包含了可执行的指令,是进程终止代码的一部分。程序正常退出时,系统将安排执行这里的代码。
.gotSHT_PROGBITS此节区包含全局偏移表。
.hashSHT_HASHSHF_ALLOC此节区包含了一个符号哈希表。
.initSHT_PROGBITSSHF_ALLOC + SHF_EXECINSTR此节区包含了可执行指令,是进程初始化代码的一部分。当程序开始执行时,系统要在开始调用主程序入口之前(通常指 C 语言的 main 函数)执行这些代码。
.interpSHT_PROGBITS此节区包含程序解释器的路径名。如果程序包含一个可加载的段,段中包含此节区,那么节区的属性将包含 SHF_ALLOC 位,否则该位为 0。
.lineSHT_PROGBITS(无)此节区包含符号调试的行号信息,其中描述了源程序与机器指令之间的对应关系。其内容是未定义的。
.noteSHT_NOTE(无)此节区中包含注释信息,有独立的格式。
.pltSHT_PROGBITS此节区包含过程链接表(procedure linkage table)。
.relname .relanameSHT_REL SHT_RELA这些节区中包含了重定位信息。如果文件中包含可加载的段,段中有重定位内容,节区的属性将包含 SHF_ALLOC 位,否则该位置 0。传统上 name 根据重定位所适用的节区给定。例如 .text 节区的重定位节区名字将是:.rel.text 或者 .rela.text。
.rodata .rodata1SHT_PROGBITSSHF_ALLOC这些节区包含只读数据,这些数据通常参与进程映像的不可写段。
.shstrtabSHT_STRTAB此节区包含节区名称。
.strtabSHT_STRTAB此节区包含字符串,通常是代表与符号表项相关的名称。如果文件拥有一个可加载的段,段中包含符号串表,节区的属性将包含SHF_ALLOC 位,否则该位为 0。
.symtabSHT_SYMTAB此节区包含一个符号表。如果文件中包含一个可加载的段,并且该段中包含符号表,那么节区的属性中包含SHF_ALLOC 位,否则该位置为 0。
.textSHT_PROGBITSSHF_ALLOC + SHF_EXECINSTR此节区包含程序的可执行指令。
  • 34
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 11
    评论
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值