ELF 文件格式解析
Linux 系统下的可执行文件、可重定位表和.so文件都是采用ELF格式存储。ELF格式起源于Unix的COFF文件格式。定义了各种段表结构体和存储方式。本文就这几个内容做一些介绍.
ELF 基本结构
ELF的基本结构是由一个ELF文件头部和多个段所组成。文件头部包括了指明该ELF文件的相关信息。比如,头部信息可以表明该文件是可执行文件,还是可重定位表 or .so文件。头部之后就是各个段排列开来,例如.text
段主要是代码段,编译后的代码一般存储在此段。.data
段为数据段,数据段主要是存放初始化的全局静态变量、局部静态变量。实际上还有只读数据段.rodata
,这个段一般存放一些常量,如字符串常量。.bss
段则是那些未初始化的全局变量和局部静态变量存储的区域。当然,实际中,段的类型和数量远不止这几种这么多,甚至用户可以自己定义属于自己的段。后面会详述这些段的组织方式和结构。
ELF整体结构
一个通用的ELF整体结构图如下所示,注意这里的节区头部表指的就是段头部表( 这个表相当于一个数组,每一个元素用来描述相应段的属性例如段名、段长、段偏移等)
ELF 头部信息结构
ELF 最前面是ELF文件头(ELF-Header),它包括了描述整个文件的基本属性,如版本号、目标机器型号、程序入口地址等。ELF头中更重要的一部分是包括了对后面所有段描述的数组,该数组中的每一个元素包括了段的名字、长度、在文件中的偏移,读写权限及段的属性。
ELF Header:
Magic: xx xx xx xx xx xx xx xx xx xx //魔数
Class: //ELF字长 ELF32,ELF64
Data: //数据存储方式
Version://版本号
OS/ABI://运行平台
ABI Version://ABI(application binary Interface)版本号
Type://ELF重定位类型
Machine://硬件平台 Intel x80386 x86_64 (是指芯片的型号与类型)
Version://硬件平台版本号
Entry point address://程序入口地址
Start of program headers://程序头入口 ??作用
Start of section headers://段头入口
Flags://
Size of this header:
Size of program headers:
Number of program headers:
Size of Section headers://段头长度
Number of section headers://段的数量
Section header string table index:
注意 这里magic 也就是魔数,ELF文件最前面的16个字节,它是用来代表ELF文件的平台属性、字长、字序、版本等相关信息。最开始的4个字节是所有ELF度必须相同的标识码即:7F 45 4C 46。魔数的作用是用来确认文件的类型,操作系统在加载可执行文件的时候会确认魔数是否正确,如果不正确会拒绝加载。
使用binutils 工具readelf 可以读到ELF文件的头部信息,例如下面的一个例子。这是一个在ARM平台上运行ELF文件
zhr-macox@laptop:/Project/Projects/iotsecurity$ readelf -a iot_app
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2s complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: ARM
Version: 0x1
Entry point address: 0x8a84
Start of program headers: 52 (bytes into file)
Start of section headers: 12780 (bytes into file)
Flags: 0x5000002, has entry point, Version5 EABI
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 10
Size of section headers: 40 (bytes)//段头长度
Number of section headers: 30 //一共30个段
Section header string table index: 27
以上的头部信息对应的ELF头部结构体如下:
typedef struct {
unsigned char e_ident[16]; /* ELF魔数,ELF字长,字节序,ELF文件版本等 */
Elf32_Half e_type; /*ELF文件类型,REL, 可执行文件,共享目标文件等 */
Elf32_Half e_machine; /* ELF的CPU平台属性 */
Elf32_Word e_version; /* ELF版本号 */
Elf32_Addr e_entry; /* ELF程序的入口虚拟地址,REL一般没有入口地址为0 */
Elf32_Off e_phoff;
Elf32_Off e_shoff; /* 段表在文件中的偏移 */
Elf32_Word e_flags; /* 用于标识ELF文件平台相关的属性 */
Elf32_Half e_ehsize; /* 本文件头的长度 */
Elf32_Half e_phentsize;
Elf32_Half e_phnum;
Elf32_Half e_shentsize; /* 段表描述符的大小 */
Elf32_Half e_shnum; /* 段表描述符的数量 */
Elf32_Half e_shstrndx; /* 段表字符串表所在的段在段表中的下标 */
} Elf32_Ehdr;
其中的数据类型定义如下
ELF头部表中对段的描述
在ELF的头部中,有一部分是关于对段的描述,其中e_shoff
代表了段头描述符表(Section Data Table,注意就是段头数组)的偏移位置,e_shentsize
代表一个段头描述符的长度,e_shnum
代表该段头数组中元素的个数,也就是段的个数也就是说,通过这三个参数,可以获知该ELF文件中所有的段信息。在段头描述符表中,每个元素代表一个段的信息,也是一个结构体,这个结构体的描述如下;
typedef struct
{
Elf32_Word sh_name; /* 段的名字 */
Elf32_Word sh_type; /* 段的类型,代码段,数据段,符号表等 */
Elf32_Word sh_flags; /* 段在进程虚拟地址空间中的属性 */
Elf32_Addr sh_addr; /* 段的虚拟地址 */
Elf32_Off sh_offset; /* 段在文件中的偏移 */
Elf32_Word sh_size; /* 段的长度 */
Elf32_Word sh_link;
Elf32_Word sh_info;
Elf32_Word sh_addralign; /* 段地址对齐 */
Elf32_Word sh_entsize;
} Elf32;
由此可以知道,当一个ELF文件被载入的时候,加载器可以先读取ELF的头部信息,然后根据该头部信息可以获取段头部表的偏移,获取到段头部表后,可以接儿获知该ELF文件中的所有段信息。