ELF文件格式

1.ELF文件格式

ELF (Executable and Linkable Format)是一种为可执行文件,目标文件,共享链接库和内核转储(core dumps)准备的标准文件格式。ELF文件有两个平行视角:一个是程序链接视角,一个是程序装载视角。从链接视角来看,ELF文件是按Section划分的;而从装载的视角来看,ELF文件又可以按Segment来划分。如图所示:

ELF双视图

从整体来看,完整的ELF文件由ELF头(ELF header)、程序头表(Program header table)、节头表(Section header table)组成。

linux/elf.h at master · torvalds/linux (github.com)

ELF双视图

1.1 ELF Header

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#define EI_NIDENT	16

typedef struct elf32_hdr{
unsigned char e_ident[EI_NIDENT];
Elf32_Half e_type;
Elf32_Half e_machine;
Elf32_Word e_version;
Elf32_Addr e_entry; /* Entry point */
Elf32_Off e_phoff;
Elf32_Off e_shoff;
Elf32_Word e_flags;
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;

typedef struct elf64_hdr {
unsigned char e_ident[EI_NIDENT]; /* ELF Magic number and other info */
Elf64_Half e_type; /* Object file type */
Elf64_Half e_machine; /* CPU Architecture */
Elf64_Word e_version; /* Object file version */
Elf64_Addr e_entry; /* OEP */
Elf64_Off e_phoff; /* Program header table foa */
Elf64_Off e_shoff; /* Section header table foa */
Elf64_Word e_flags; /* Processor-specific flags */
Elf64_Half e_ehsize; /* ELF header size in bytes */
Elf64_Half e_phentsize; /* Program header table entry size */
Elf64_Half e_phnum; /* Program header table entry count */
Elf64_Half e_shentsize; /* Section header table entry size */
Elf64_Half e_shnum; /* Section header table entry count */
Elf64_Half e_shstrndx; /* Section header string table index */
} Elf64_Ehdr;

详细字段说明:ELF Header (linuxfoundation.org)

1.1.1 e_type

表示ELF文件类型,有以下几种:

  • ET_NONE: 位置类型
  • ET_REL: 可重定向类型(relocatable),通常是编译后的*.o文件
  • ET_EXEC: 可执行类型(executable),静态链接后的可执行文件 *.out
  • ET_DYN: 共享对象(shared object),动态链接的可执行文件或动态库 *.so
  • ET_CORE: 程序崩溃生成的核心转储文件(coredump)

1.1.2 e_machine

表示当前程序所需的CPU架构,常见有以下几种:

  • EM_ARM: arm指令集
  • EM_X86_64: amd x86-64指令集

1.1.3 e_entry

指定程序的入口虚拟地址,不是main函数地址,而是.text段的首地址_start。当然这也要求程序本身非PIE(-no-pie)编译的且ASLR关闭的情况下,对于非ET_EXEC类型通常并不是实际的虚拟地址值。

1.1.4 e_shstrndx

表示section table名称字符串表在section table中的索引。

1.2 Section header table

一个由 e_shentsize 个大小为 e_shentsize 元素组成的数组,主要用来指定静态链接所使用的一些信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
typedef struct elf32_shdr {
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_Shdr;

typedef struct elf64_shdr {
Elf64_Word sh_name; /* Section name, index in string tbl */
Elf64_Word sh_type; /* Type of section */
Elf64_Xword sh_flags; /* Miscellaneous section attributes */
Elf64_Addr sh_addr; /* Section virtual addr at execution */
Elf64_Off sh_offset; /* Section file offset */
Elf64_Xword sh_size; /* Size of section in bytes */
Elf64_Word sh_link; /* Index of another section */
Elf64_Word sh_info; /* Additional section information */
Elf64_Xword sh_addralign; /* Section alignment */
Elf64_Xword sh_entsize; /* Entry size if section holds table */
} Elf64_Shdr;

1.2.1 sh_name

表示节名在字符串表中的偏移

1.2.2 sh_type

表示节的类型,常见的类型枚举如下:

  • SHT_NULL: 表示无效节,通常第0号节区为该类型
  • SHT_PROGBITS: 表示该section包含由程序决定的内容,如.text.data.plt.got
  • SHT_SYMTAB/SHT_DYNSYM: 表示该section中包含符号表,如.symtab.dynsym
  • SHT_DYNAMIC: 表示该section中包含动态链接阶段所需要的信息
  • SHT_STRTAB: 表示该section中包含字符串信息,如.strtab.shstrtab
  • SHT_REL/SHT_RELA: 表示该section中包含重定向项信息

1.2.3 文件时信息

  • sh_offset: 内容起始地址相对于文件开头的偏移
  • sh_size: 内容的大小
  • sh_entsize: 有的内容是也是一个数组,这个字段就表示数组的元素大小

1.2.4 装载时信息

  • sh_addr: 如果该section需要在运行时加载到虚拟内存中,该字段就是对应section内容(第一个字节)的虚拟地址
  • sh_addralign: 内容地址的对齐,如果有的话需要满足sh_addr % sh_addralign = 0
  • sh_flags: 表示所映射内容的权限,可根据SHF_WRITE/ALLOC/EXECINSTR进行组合

1.2.5 拓展字段

sh_linksh_info的含义根据section类型的不同而不同,常用于提供链接信息,如下表所示:

Section header拓展字段

1.3 Program header table

一个由 e_phnum个大小为 e_phentsize元素组成的数组,主要用来保存程序加载到内存中所使用的一些信息,使用段 (segment) 来表示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct elf32_phdr{
Elf32_Word p_type;
Elf32_Off p_offset;
Elf32_Addr p_vaddr;
Elf32_Addr p_paddr;
Elf32_Word p_filesz;
Elf32_Word p_memsz;
Elf32_Word p_flags;
Elf32_Word p_align;
} Elf32_Phdr;

typedef struct elf64_phdr {
Elf64_Word p_type;
Elf64_Word p_flags;
Elf64_Off p_offset; /* Segment file offset */
Elf64_Addr p_vaddr; /* Segment virtual address */
Elf64_Addr p_paddr; /* Segment physical address */
Elf64_Xword p_filesz; /* Segment size in file */
Elf64_Xword p_memsz; /* Segment size in memory */
Elf64_Xword p_align; /* Segment alignment, file & memory */
} Elf64_Phdr;

1.3.1 p_type

表示段的类型,常见的类型枚举如下:

  • PT_NULL: 表示该段未使用
  • PT_LOAD: Loadable Segment,将文件中的segment内容映射到进程内存中对应的地址上。值得一提的是SPEC中说在program header中的多个PT_LOAD地址是按照虚拟地址递增排序的
  • PT_DYNAMIC: 动态链接中用到的段,通常是RW映射,因为需要由interpreter(ld.so)修复对应的的入口
  • PT_INTERP: 包含interpreter的路径,动态连接时,类似ELF用户态加载器
  • PT_HDR: 表示program header table本身。如果有这个segment的话,必须要在所有可加载的segment之前,并且在文件中不能出现超过一次

1.3.2 装载信息

  • p_offset: 该segment的数据在文件中的偏移地址(相对文件头)
  • p_vaddr: segment数据应该加载到进程的虚拟地址
  • p_paddr: segment数据应该加载到进程的物理地址(如果对应系统使用的是物理地址)
  • p_filesz: 该segment数据在文件中的大小
  • p_memsz: 该segment数据在进程内存中的大小。注意需要满足p_memsz>=p_filesz,多出的部分初始化为0,通常作为.bss段内容
  • p_flags: 进程中该segment的权限(R/W/X)
  • p_align: 该segment数据的对齐,2的整数次幂。即要求p_offset % p_align = p_vaddr

2.符号

链接的本质就是寻找目标文件之间对函数和变量地址的引用并修复正确,其中函数和变量叫做符号(Symbol)。整个链接过程是基于符号的,每一个目标文件都会有一个相应的符号表(Symbol Table)。

2.1 符号表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct elf32_sym{
Elf32_Word st_name;
Elf32_Addr st_value;
Elf32_Word st_size;
unsigned char st_info;
unsigned char st_other;
Elf32_Half st_shndx;
} Elf32_Sym;

typedef struct elf64_sym {
Elf64_Word st_name; /* Symbol name, index in string tbl */
unsigned char st_info; /* Type and binding attributes */
unsigned char st_other; /* No defined meaning, 0 */
Elf64_Half st_shndx; /* Associated section index */
Elf64_Addr st_value; /* Value of the symbol */
Elf64_Xword st_size; /* Associated symbol size */
} Elf64_Sym;

2.1.1 st_name

表示符号名在字符串表中的索引

2.1.2 st_info

低4位标识符号类型(Symbol Type),高4位表示符号绑定信息(Symbol Binding)。

符号绑定表:

名称 说明
STB_LOCAL 0 局部符号,目标文件外部不可见
STB_GLOBA 1 全局符号,目标文件外部可见
STB_WEAK 2 弱引用
STB_LOOS 10
STB_HIOS 12
STB_LOPROC 13
STB_HIPROC 15

符号类型表:

名称 说明
STT_NOTYPE 0 未知类型
STT_OBJECT 1 表示数据对象,如变量、数组等
STT_FUNC 2 表示函数或其他可执行代码
STT_SECTION 3 表示一个Section,必选是局部符号
STT_FILE 4 表示文件名,常表示源文件名,必选是局部符号,st_shndx为SHN_ABS
STT_COMMON 5
STT_TLS 6
STT_LOOS 10
STT_HIOS 12
STT_LOPROC 13
STT_SPARC_REGISTER 13
STT_HIPROC 15

2.1.3 st_value

如果该符号通常是函数或变量,这个值即为地址。具体有如下几种情况:

  • 目标文件 && st_shndx != SHN_COMMON,st_value表示该符号在st_shndx指定的Section中的偏移,全局变量最常见情况
  • 目标文件 && st_shndx == SHN_COMMON,st_value表示该符号的对齐属性
  • 可执行文件,st_value表示符号的虚拟地址

2.1.4 st_size

符号大小(Byte),跟符号类型有关系,如double占8个字节

2.1.5 st_shndx

如果符号定义在本目标文件中,该值表示这个符号所在的段在段表中的索引。如果符号不再本目标文件中,或者对于一些特殊符号,shndx可能取下表中的值:

名称 说明
SHN_ABS 0xfff1 表示该符号包含了一个绝对的值
SHN_COMMON 0xfff2 表示该符号是一个”COMMON”块类型的符号,如未初始化的全局弱符号
SHN_UNDEF 0 表示该符号未定义,这个符号在本目标文件被引用但是定义在其他目标文件中

2.2 extern关键字

2.2.1 非常量全局变量的外部链接

当链接器在一个全局变量声明前看到extern关键字,它会尝试在其他文件中寻找这个变量的定义

1
2
3
4
5
6
7
8
// a.cpp
int i = 1; // 声明并定义i

// b.cpp
extern int i; // 正确,声明i链接外部全局变量

// c.pp
extern int i = 2; // 错误,全局变量i重定义

2.2.2 常量全局变量的外部链接

常量全局变量默认是内部链接的,所以想要在文件间传递常量全局变量需要在定义时指明extern

1
2
3
4
5
// a.cpp
extern const int i = 1; // 定义

// b.cpp
extern const int i; // 声明

2.2.3 extern “C”符号声明

在C++中,当与字符串连用时,extern指明当前声明使用了其他语言的链接规范,如extern “C”,就指明使用C语言的链接规范。C++语言在编译的时候为了解决函数的多态问题,会将函数名和参数联合起来生成一个唯一的函数签名(名字粉碎),而C语言不会,因此在C++中声明C编译的符号时会出现无法找到符号的问题,此时就需要用extern “C”进行链接指定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 声明printf函数使用C链接
extern "C" int printf(const char *fmt, ...);

//声明指定的头文件内所有的东西都使用 C 链接
extern "C" {
#include <stdio.h>
}

// 声明全局变量 errno 为C链接
extern "C" int errno;

// 定义函数 ShowChar 使用 C 链接
extern "C" char ShowChar(char ch) {
putchar(ch);
return ch;
}

// C++常见的预编译写法
#ifdef __cplusplus
extern "C" {
#endif

/**** some declaration or so *****/

#ifdef __cplusplus
}
#endif

2.3 弱符号与强符号

在C/C++中,编译器默认函数和初始化的全局变量为强符号,未初始化的全局变量为弱符号。

1
2
3
int waek;								// 弱符号
int strong = 1; // 强符号
__attribute__((weak)) waek2 = 2; // 指定弱符号

针对强弱符号的概念,链接器会按照如下规则处理与选择被多次定义的全局符号:

  • 不允许强符号被多次定义(不同文件定义相同),发生重定义链接错误
  • 如果一个符号在某个目标文件中是强符号,在其他文件中都是弱符号,那么最终强符号有效
  • 如果一个符号在所有目标文件中都是弱符号,那么选择其中占用空间最大的一个

可以用来重载函数,测试实例如下:

强弱符号测试

Common Function Attributes (Using the GNU Compiler Collection (GCC))

2.4 弱引用与强引用

在C/C++中,链接阶段需要对目标文件引用的外部符号进行决议,如果没有找到该符号的定义,会报符号为定义链接错误,这种被称为强引用。弱引用与其相反,在没有找到该符号时,并不会产生错误,而是获得默认0值或特殊值,以便程序能够识别。

1
2
3
4
extern void func();												// 强引用

static __attribute__ ((weakref("same_func"))) void func(); // 弱引用
// 其中same_func为func的别名,函数必须指定static,作用域为当前文件

可以实现拓展,如果same_func函数在外部定义了,就执行外部定义的;如果未定义,就不执行这个功能,如下图所示:

弱引用与强引用测试

3.重定位

链接器在处理目标文件时,需要对目标文件中的某些位置进行重定位,即将符号指向恰当的位置,确保程序正常执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct elf32_rel {
Elf32_Addr r_offset;
Elf32_Word r_info;
} Elf32_Rel;

typedef struct elf64_rel {
Elf64_Addr r_offset; /* Location at which to apply the action */
Elf64_Xword r_info; /* index and type of relocation */
} Elf64_Rel;

typedef struct elf32_rela{
Elf32_Addr r_offset;
Elf32_Word r_info;
Elf32_Sword r_addend;
} Elf32_Rela;

typedef struct elf64_rela {
Elf64_Addr r_offset; /* Location at which to apply the action */
Elf64_Xword r_info; /* index and type of relocation */
Elf64_Sxword r_addend; /* Constant addend used to compute value */
} Elf64_Rela;

一般来说,32 位程序只使用 Elf32_Rel,64 位程序只使用 Elf32_Rela。

3.1.1 r_offset

此成员给出了需要重定位的位置。对于一个可重定位文件而言,此值是从需要重定位的符号所在节区头部开始到将被重定位的位置之间的字节偏移。对于可执行文件或者共享目标文件而言,其取值是需要重定位的虚拟地址,一般而言,也就是说我们所说的 GOT 表的地址。

3.1.2 r_info

此成员给出需要重定位的符号的符号表索引,以及相应的重定位类型。根据符号来获取外部地址,重定位类型决定修复方式,比如对e8 call的修复和全局变量的修复方式存在差异。

位运算表示如下:

1
2
3
4
5
6
7
#define ELF32_R_SYM(info)             ((info)>>8)
#define ELF32_R_TYPE(info) ((unsigned char)(info))
#define ELF32_R_INFO(sym, type) (((sym)<<8)+(unsigned char)(type))

#define ELF64_R_SYM(info) ((info)>>32)
#define ELF64_R_TYPE(info) ((Elf64_Word)(info))
#define ELF64_R_INFO(sym, type) (((Elf64_Xword)(sym)<<32) + (Elf64_Xword)(type))

常用类型枚举如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* x86-64 relocation types */
#define R_X86_64_NONE 0 /* No reloc */
#define R_X86_64_64 1 /* Direct 64 bit */
#define R_X86_64_PC32 2 /* PC relative 32 bit signed */
#define R_X86_64_GOT32 3 /* 32 bit GOT entry */
#define R_X86_64_PLT32 4 /* 32 bit PLT address */
#define R_X86_64_COPY 5 /* Copy symbol at runtime */
#define R_X86_64_GLOB_DAT 6 /* Create GOT entry */
#define R_X86_64_JUMP_SLOT 7 /* Create PLT entry */
#define R_X86_64_RELATIVE 8 /* Adjust by program base */
#define R_X86_64_GOTPCREL 9 /* 32 bit signed pc relative offset to GOT */
#define R_X86_64_32 10 /* Direct 32 bit zero extended */
#define R_X86_64_32S 11 /* Direct 32 bit sign extended */
#define R_X86_64_16 12 /* Direct 16 bit zero extended */
#define R_X86_64_PC16 13 /* 16 bit sign extended pc relative */
#define R_X86_64_8 14 /* Direct 8 bit sign extended */
#define R_X86_64_PC8 15 /* 8 bit sign extended pc relative */
#define R_X86_64_NUM 16

3.1.2 r_addend

此成员给出一个常量补齐,用来计算将被填充到可重定位字段的数值。

Linux查看ELF命令

1
2
3
4
5
6
7
8
# 查看Section header table
readelf -S xxx

# 查看符号表
readelf -s xxx

# 查看重定位表
readelf -r xxx

ELF文件格式
http://helloymf.github.io/2022/11/15/elf-wen-jian-ge-shi/
作者
JNZ
许可协议