ELF文件格式
1.ELF文件格式
ELF (Executable and Linkable Format)是一种为可执行文件,目标文件,共享链接库和内核转储(core dumps)准备的标准文件格式。ELF文件有两个平行视角:一个是程序链接视角,一个是程序装载视角。从链接视角来看,ELF文件是按Section划分的;而从装载的视角来看,ELF文件又可以按Segment来划分。如图所示:
从整体来看,完整的ELF文件由ELF头(ELF header)、程序头表(Program header table)、节头表(Section header table)组成。
linux/elf.h at master · torvalds/linux (github.com)
1.1 ELF Header
1 |
|
详细字段说明: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 |
|
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_link
和sh_info
的含义根据section类型的不同而不同,常用于提供链接信息,如下表所示:
1.3 Program header table
一个由 e_phnum个大小为 e_phentsize元素组成的数组,主要用来保存程序加载到内存中所使用的一些信息,使用段 (segment) 来表示。
1 |
|
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.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.2.2 常量全局变量的外部链接
常量全局变量默认是内部链接的,所以想要在文件间传递常量全局变量需要在定义时指明extern
1 |
|
2.2.3 extern “C”符号声明
在C++中,当与字符串连用时,extern指明当前声明使用了其他语言的链接规范,如extern “C”,就指明使用C语言的链接规范。C++语言在编译的时候为了解决函数的多态问题,会将函数名和参数联合起来生成一个唯一的函数签名(名字粉碎),而C语言不会,因此在C++中声明C编译的符号时会出现无法找到符号的问题,此时就需要用extern “C”进行链接指定。
1 |
|
2.3 弱符号与强符号
在C/C++中,编译器默认函数和初始化的全局变量为强符号,未初始化的全局变量为弱符号。
1 |
|
针对强弱符号的概念,链接器会按照如下规则处理与选择被多次定义的全局符号:
- 不允许强符号被多次定义(不同文件定义相同),发生重定义链接错误
- 如果一个符号在某个目标文件中是强符号,在其他文件中都是弱符号,那么最终强符号有效
- 如果一个符号在所有目标文件中都是弱符号,那么选择其中占用空间最大的一个
可以用来重载函数,测试实例如下:
Common Function Attributes (Using the GNU Compiler Collection (GCC))
2.4 弱引用与强引用
在C/C++中,链接阶段需要对目标文件引用的外部符号进行决议,如果没有找到该符号的定义,会报符号为定义链接错误,这种被称为强引用。弱引用与其相反,在没有找到该符号时,并不会产生错误,而是获得默认0值或特殊值,以便程序能够识别。
1 |
|
可以实现拓展,如果same_func函数在外部定义了,就执行外部定义的;如果未定义,就不执行这个功能,如下图所示:
3.重定位
链接器在处理目标文件时,需要对目标文件中的某些位置进行重定位,即将符号指向恰当的位置,确保程序正常执行。
1 |
|
一般来说,32 位程序只使用 Elf32_Rel,64 位程序只使用 Elf32_Rela。
3.1.1 r_offset
此成员给出了需要重定位的位置。对于一个可重定位文件而言,此值是从需要重定位的符号所在节区头部开始到将被重定位的位置之间的字节偏移。对于可执行文件或者共享目标文件而言,其取值是需要重定位的虚拟地址,一般而言,也就是说我们所说的 GOT 表的地址。
3.1.2 r_info
此成员给出需要重定位的符号的符号表索引,以及相应的重定位类型。根据符号来获取外部地址,重定位类型决定修复方式,比如对e8 call的修复和全局变量的修复方式存在差异。
位运算表示如下:
1 |
|
常用类型枚举如下:
1 |
|
3.1.2 r_addend
此成员给出一个常量补齐,用来计算将被填充到可重定位字段的数值。
Linux查看ELF命令
1 |
|