0%

ELF文件结构

参考文章:详解ELF文件ELF文件结构

其实PE文件和ELF文件结构比较类似,学过PE文件后,ELF文件就很好懂。

通俗点说由汇编器和链接器生成的文件都属于ELF文件。通常我们接触的ELF文件主要有以下三类:

  • 可重定位文件relocatable file) ,一般为.o文件。可以被链接成可执行文件或共享目标文件。静态链接库属于可重定位文件。
  • 可执行文件excutable file)它保存了适合直接加载到内存中执行的二进制程序。
  • 共享库文件shared object file)一般为.so文件。一种特殊的可重定位目标文件,可以在加载或者运行时被动态的加载进内存并链接。

image-20220322201522334

image-20220322203524439

Note:段(Segment)与节(Section)的区别。很多地方对两者有所混淆。段是程序执行的必要组成,当多个目标文件链接成一个可执行文件时,会将相同权限的节合并到一个段中。相比而言,节的粒度更小。

ELF文件主要由四部分组成:

  • ELF Header
  • ELF Program Header Table(程序头)
  • ELF Section Header Table(节头表)
  • ELF sections

ELF Header

ELF Header其实对应的是一个结构体,该结构体定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#define EI_NIDENT 16

typedef struct {
unsigned char e_ident[EI_NIDENT];
uint16_t e_type;
uint16_t e_machine;
uint32_t e_version;
ElfN_Addr e_entry;
ElfN_Off e_phoff;
ElfN_Off e_shoff;
uint32_t e_flags;
uint16_t e_ehsize;
uint16_t e_phentsize;
uint16_t e_phnum;
uint16_t e_shentsize;
uint16_t e_shnum;
uint16_t e_shstrndx;
} ElfN_Ehdr;
1
2
ElfN_Addr       Unsigned program address, uintN_t
ElfN_Off Unsigned file offset, uintN_t
  • e_ident:包含一个magic number、ABI信息,该文件使用的平台、大小端规则
  • e_type:文件类型, 表示该文件属于可执行文件、可重定位文件、core dump文件或者共享库
  • e_entry: 表示程序执行的入口地址,规定ELF程序的入口虚拟地址,操作系统在加载完该程序后从这个地址开始执行进程的指令。可重定位指令一般没有入口地址,则该值为0
  • e_phoff: 表示Program Header的入口偏移量(以字节为单位)
  • e_shoff: 表示Section Header的入口偏移量(以字节为单位)
  • e_flags: 保存了这个ELF文件相关的特定处理器的flag
  • e_ehsize: 表示ELF Header大小(以字节为单位)
  • e_phentsize: 表示Program Header大小(以字节为单位)
  • e_phnum: 表示Program Header的数量 (十进制数字)
  • e_shentsize: 表示单个Section Header大小(以字节为单位)
  • e_shnum: 表示Section Header的数量 (十进制数字)
  • e_shstrndx: 表示字符串表的索引,字符串表用来保存ELF文件中的字符串,比如段名、变量名。 然后通过字符串在表中的偏移访问字符串。

magic:通过判断该字段可以确定文件格式和类型。如:ELF的可执行文件格式的头4个字节为0x7Felf;Java的可执行文件格式的头4个字节为cafe;如果被执行的是Shell脚本或perl、python等解释型语言的脚本,那么它的第一行往往是#!/bin/sh#!/usr/bin/perl#!/usr/bin/python,此时前两个字节#!就构成了魔数,系统一旦判断到这两个字节,就对后面的字符串进行解析,以确定具体的解释程序路径。

ELF Section Header Table

ELF 节头表是一个节头数组。每一个节头都描述了其所对应的节的信息,如节名、节大小、在文件中的偏移、读写权限等。编译器、链接器、装载器都是通过节头表来定位和访问各个节的属性的。

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct {
uint32_t sh_name;
uint32_t sh_type;
uint64_t sh_flags;
Elf64_Addr sh_addr;
Elf64_Off sh_offset;
uint64_t sh_size;
uint32_t sh_link;
uint32_t sh_info;
uint64_t sh_addralign;
uint64_t sh_entsize;
} Elf64_Shdr;
  • sh_name:表示该section的名字,节名是一个字符串,保存在一个名为.shstrtab的字符串表(可通过Section Header索引到)。sh_name的值实际上是其节名字符串在.shstrtab中的偏移值。

  • sh_type:表示该section中存放的内容类型,比如符号表,可重定位段等。

  • sh_flags: 表示该section的一些属性,比如是否可写,可执行等。

  • sh_addr: 表示该section在程序运行时的内存地址

  • sh_offset: 如果该节存在于文件中,则表示该节在文件中的偏移;否则无意义,如sh_offset对于BSS 节来说是没有意义的

  • sh_size: 表示该section的大小

  • sh_link: 配合sh_info保存section的额外信息

  • sh_info:保存该section相关的一些额外信息

  • sh_addralign:表示该section需要的地址对齐信息

  • sh_entsize:有些section里保存的是一些固定长度的条目,比如符号表。对于这些section来讲,sh_entsize里保存的就是条目的长度。

    sh_type(节类型)

image-20220322205037066

sh_link、sh_info(节链接信息)

如果节的类型是与链接相关的(无论是动态链接还是静态链接),如重定位表、符号表、等,则sh_linksh_info两个成员所包含的意义如下所示。其他类型的节,这两个成员没有意义。

image-20220322205250620

ELF Sections

在ELF文件中,数据和代码分开存放的,这样可以按照其功能属性分成一些区域,比如程序、数据、符号表等。这些分离存放的区域在ELF文件中反映成section。ELF文件中典型的section如下:

  • .text: 已编译程序的二进制代码
  • .rodata: 只读数据段,比如常量
  • .data: 已初始化的全局变量和静态变量
  • .bss: 未初始化的全局变量和静态变量,所有被初始化成0的全局变量和静态变量(记得与PE文件区分开)
  • .sysmtab: 符号表,它存放了程序中定义和引用的函数和全局变量的信息
  • .debug: 调试符号表,它需要以'-g'选项编译才能得到,里面保存了程序中定义的局部变量和类型定义,程序中定义和引用的全局变量,以及原始的C文件
  • .line: 原始的C文件行号和.text节中机器指令之间的映射
  • .strtab: 字符串表,内容包括.symtab.debug节中的符号表

特殊的,
1)对于可重定位的文件,由于在编译时,并不能确定它引用的外部函数和变量的地址信息,因此,编译器在生成目标文件时,增加了两个section:

  • .rel.text 保存了程序中引用的外部函数的重定位信息,这些信息用于在链接时重定位其对应的符号。
  • .rel.data 保存了被模块引用或定义的所有全局变量的重定位信息,这些信息用于在链接时重定位其对应的全局变量。

2)对于可执行文件,由于它已经全部完成了重定位工作,可以直接加载到内存中执行,所以它不存在.rel.text.rel.data这两个section。但是,它增加了一个section:

  • .init: 这个section里面保存了程序运行前的初始化代码

重定位表是一个Elf_Rel类型的数组结构,每一项对应一个需要进行重定位的项。
其成员含义如下表所示:

image-20220322205729738

ELF Program Header Table

在可执行文件中,ELF header下面紧接着就是Program Header Table。它描述了各个segment在ELF文件中的位置以及在程序执行过程中系统需要准备的其他信息。它也是用一个结构体数组来表示的。具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef uint64_t  Elf64_Addr;
typedef uint64_t Elf64_Off;
typedef uint32_t Elf64_Word;
typedef uint64_t Elf64_Xword;

typedef struct {
Elf64_Word p_type; // 4
Elf64_Word p_flags; // 4
Elf64_Off p_offset; // 8
Elf64_Addr p_vaddr; // 8
Elf64_Addr p_paddr; // 8
Elf64_Xword p_filesz; // 8
Elf64_Xword p_memsz; // 8
Elf64_Xword p_align; // 8
} Elf64_Phdr;
  • p_type:描述了当前segment是何种类型的或者如何解释当前segment,比如是动态链接相关的或者可加载类型的等
  • p_flags:保存了该segment的flag
  • p_offset:表示从ELF文件到该segment第一个字节的偏移量
  • p_vaddr:表示该segment的第一个字节在内存中的虚拟地址
  • p_paddr:对于使用物理地址的系统来讲,这个成员表示该segment的物理地址
  • p_filesz:表示该segment的大小,以字节表示
  • p_memsz:表示该segment在内存中的大小,以字节表示
  • p_align:表示该segment在文件中或者内存中需要以多少字节对齐