ELF 可执行链接格式(Executable and Linking Format)最初是由 UNIX 系统实验室(UNI XSystem Laboratories,USL)开发并发布的,作为应用程序二进制接口(Application Binary Interface,ABI)的一部分。ELF 是一种用于二进制文件、可执行文件、目标代码、共享库和核心转储格式文件的格式。
1 ELF 文件类型
ELF 主要分为 3 种文件类型:
-
可重定位文件(Relocatable File),目标文件编译完成,尚未链接。一般多个目标文件链接成一个可执行文件或共享目标文件。包含适合于与其他目标文件链接来创建可执行文件或者共享目标文件的代码和数据。 -
可执行文件(Executable File),Linux 中的执行程序。包含适合于执行的一个程序,此文件规定了 exec() 如何创建一个程序的进程映像。 -
共享目标文件(Shared Object File),包含可在两种上下文中链接的代码和数据。首先链接编辑器可以将它和其它可重定位文件和共享目标文件一起处理,生成另外一个目标文件。其次,动态链接器(Dynamic Linker)可能将它与某个可执行文件以及其它共享目标一起组合,创建进程映像。
还有一种 ELF 文件 —— 核心转储文件(Core Dump file),程序异常终止后对地址空间的转储。可以使用 gdb 读取文件查找异常原因。
2 ELF 文件格式
目标文件既要参与程序链接又要参与程序执行。出于方便性和效率考虑,目标文件格式提供了两种并行视图,分别反映了这些活动的不同需求。
文件开始处是一个 ELF 头部(ELF Header),用来描述整个文件的组织。节区部分包含链接视图的大量信息:指令、数据、符号表和重定位信息等等。
程序头部表(Program Header Table),如果存在的话,告诉系统如何创建进程映像。用来构造进程映像的目标文件必须具有程序头部表,可重定位文件不需要这个表。 节区头部表(Section Heade Table)包含了描述文件节区的信息,每个节区在表中都有一项,每一项给出诸如节区名称、节区大小这类信息。用于链接的目标文件必须包含节区头部表,其他目标文件可以有,也可以没有这个表。
在链接视角程序头部表是可选的,通过节区(section)来划分。在运行视角节区头部表是可选的,通过段(segment)来划分(段大都来自链接阶段的节区)。节区一般包含代码节(.text)、数据节(.data)和 BSS 节(.bss)等。代码节用于保存机器指令,数据节保存了初始化的全局变量等数据。BSS 节保存了未进行初始化的全局数据,程序加载时数据被初始化为 0,在程序执行期间可以进行赋值。将数据和指令分开存放有利于安全,设置代码节对进程只读、数据和 BSS 节可读写。
注意:尽管图中显示的各个组成部分是有顺序的,实际上除了 ELF 头部表以外,其他节区和段都没有规定的顺序。
3 ELF 文件中的数据表示
目标文件格式支持 8 位字节/32 位体系结构,当然也支持 8 位字节/64 位体系结构。不过这种格式是可以扩展的,目标文件因此以某些机器独立的格式表达某些控制数据,使得能够以一种公共的方式来识别和解释其内容。目标文件中的其它数据使用目标处理器的编码结构,而不管文件在何种机器上创建。
下面的分析基于 Linux 内核 v4.19.111 版本。
include/uapi/linux/elf.h
typedef __u32 Elf32_Addr;
typedef __u16 Elf32_Half;
typedef __u32 Elf32_Off;
typedef __s32 Elf32_Sword;
typedef __u32 Elf32_Word;
typedef __u64 Elf64_Addr;
typedef __u16 Elf64_Half;
typedef __s16 Elf64_SHalf;
typedef __u64 Elf64_Off;
typedef __s32 Elf64_Sword;
typedef __u32 Elf64_Word;
typedef __u64 Elf64_Xword;
typedef __s64 Elf64_Sxword;
名称 | 大小 | 对齐 | 目的 |
---|
Elf32_Addr | 4 | 4 | 无符号32位程序地址 | Elf32_Half | 2 | 2 | 无符号短整数 | Elf32_Off | 4 | 4 | 无符号32位文件偏移 | Elf32_Sword | 4 | 4 | 有符号整数 | Elf32_Word | 4 | 4 | 无符号整数 | unsigned char | 1 | 1 | 无符号字节 |
目标文件中的所有数据结构都遵从“自然”大小和对齐规则。如果必要,数据结构可以包含显式的补齐,例如为了确保 4 字节对象按 4 字节边界对齐。数据对齐同样适用于文件内部。
名称 | 大小 | 对齐 | 目的 |
---|
Elf64_Addr | 8 | 8 | 无符号64位程序地址 | Elf64_Half | 2 | 2 | 无符号短整数 | Elf64_SHalf | 2 | 2 | 有符号短整数 | Elf64_Off | 8 | 8 | 无符号64位文件偏移 | Elf64_Sword | 4 | 4 | 有符号整数 | Elf64_Word | 4 | 4 | 无符号整数 | Elf64_Xword | 8 | 8 | 无符号长整数 | Elf64_Sxword | 8 | 8 | 有符号长整数 | unsigned char | 1 | 1 | 无符号字节 |
4 ELF 文件头
文件的最开始几个字节给出如何解释文件的提示信息。这些信息独立于处理器,也独立于文件中的其余内容。
include/uapi/linux/elf.h
#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;
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];
Elf64_Half e_type;
Elf64_Half e_machine;
Elf64_Word e_version;
Elf64_Addr e_entry;
Elf64_Off e_phoff;
Elf64_Off e_shoff;
Elf64_Word e_flags;
Elf64_Half e_ehsize;
Elf64_Half e_phentsize;
Elf64_Half e_phnum;
Elf64_Half e_shentsize;
Elf64_Half e_shnum;
Elf64_Half e_shstrndx;
} Elf64_Ehdr;
其中,e_ident 数组给出了 ELF 的一些标识信息,这个数组中不同下标的含义如下表所示:
名称 | 取值 | 目的 |
---|
EI_MAG0 | 0 | 文件标识 | EI_MAG1 | 1 | 文件标识 | EI_MAG2 | 2 | 文件标识 | EI_MAG3 | 3 | 文件标识 | EI_CLASS | 4 | 文件类 | EI_DATA | 5 | 数据编码 | EI_VERSION | 6 | 文件版本 | EI_PAD | 7 | 补齐字节开始处 | EI_NIDENT | 16 | e_ident[]大小 |
这些索引访问包含以下数值的字节:
索引 | 说明 |
---|
EI_MAG0 到 EI_MAG3 | 魔数(Magic Number),标志此文件是一个 ELF 目标文件。 名称 取值 位置 EI_MAG0 0x7f e_ident[EI_MAG0] EI_MAG1 ‘E’ e_ident[EI_MAG1] EI_MAG2 ‘L’ e_ident[EI_MAG2] EI_MAG3 ‘F’ e_ident[EI_MAG3]
| EI_CLASS | 标识文件的类别,或者说,容量。 名称 取值 位置 ELFCLASSNONE 0 非法类别 ELFCLASS32 1 32 位目标 ELFCLASS64 2 64 位目标 ELFCLASS32 支持虚存范围 4 GB。ELFCLASS64 是为 64 位使用的。 | EI_DATA | 字节 e_ident[EI_DATA] 给出处理器特定数据的数据编码方式。 名称 取值 位置 ELFDATANONE 0 非法数据编码 ELFDATA2LSB 1 高位在前 ELFDATA2MSB 2 低位在前 | EI_VERSION | ELF 头部的版本号码,当前此字节必须为 EV_CURRENT。 | EI_PAD | 标记 e_ident 中未使用字节的开始。初始化为 0。 |
ELF 文件头中各个字段的说明如下表:
成员 | 说明 |
---|
e_ident | 目标文件标识。 | e_type | 目标文件类型。 | e_machine | 给出文件的目标体系结构类型。 | e_version | 目标文件版本。 | e_entry | 程序入口的虚拟地址。如果目标文件没有程序入口,可以为 0。 | e_phoff | 程序头部表格(Program Header Table)的偏移量(按字节计算)。如果文件没有程序头部表格,可以为 0。 | e_shoff | 节区头部表格(Section Header Table)的偏移量(按字节计算)。如果文件没有节区头部表格,可以为 0。 | e_flags | 保存与文件相关的,特定于处理器的标志。标志名称采用 EF_machine_flag 的格式。 | e_ehsize | ELF 头部的大小(以字节计算)。 | e_phentsize | 程序头部表格的表项大小(按字节计算)。 | e_phnum | 程序头部表格的表项数目。可以为 0。 | e_shentsize | 节区头部表格的表项大小(按字节计算)。 | e_shnum | 节区头部表格的表项数目。可以为 0。 | e_shstrndx | 节区头部表格中与节区名称字符串表相关的表项的索引。如果文件没有节区名称字符串表,此参数可以为 SHN_UNDEF。 |
目标文件类型(e_type):
名称 | 取值 | 含义 |
---|
ET_NONE | 0 | 未知目标文件格式 | ET_REL | 1 | 可重定位文件 | ET_EXEC | 2 | 可执行文件 | ET_DYN | 3 | 共享目标文件 | ET_CORE | 4 | Core 文件(转储格式) | ET_LOPROC | 0xff00 | 特定处理器文件 | ET_HIPROC | 0xffff | 特定处理器文件 |
ET_LOPROC 和 ET_HIPROC 之间的取值用来标识与处理器相关的文件格式。
给出文件的目标体系结构类型(e_machine),此处我们可以参考一下源码,比如 ARM 32 被定义为 EM_ARM,ARM 64 定义为 EM_AARCH64。
include/uapi/linux/elf-em.h
#define EM_NONE 0
#define EM_M32 1
#define EM_SPARC 2
#define EM_386 3
#define EM_68K 4
#define EM_88K 5
#define EM_486 6
#define EM_860 7
#define EM_MIPS 8
#define EM_MIPS_RS3_LE 10
#define EM_MIPS_RS4_BE 10
#define EM_PARISC 15
#define EM_SPARC32PLUS 18
#define EM_PPC 20
#define EM_PPC64 21
#define EM_SPU 23
#define EM_ARM 40
#define EM_SH 42
#define EM_SPARCV9 43
#define EM_H8_300 46
#define EM_IA_64 50
#define EM_X86_64 62
#define EM_S390 22
#define EM_CRIS 76
#define EM_M32R 88
#define EM_MN10300 89
#define EM_OPENRISC 92
#define EM_BLACKFIN 106
#define EM_ALTERA_NIOS2 113
#define EM_TI_C6000 140
#define EM_AARCH64 183
#define EM_TILEPRO 188
#define EM_MICROBLAZE 189
#define EM_TILEGX 191
#define EM_BPF 247
#define EM_FRV 0x5441
#define EM_ALPHA 0x9026
#define EM_CYGNUS_M32R 0x9041
#define EM_S390_OLD 0xA390
#define EM_CYGNUS_MN10300 0xbeef
目标文件版本(e_version):
名称 | 取值 | 含义 |
---|
EV_NONE | 0 | 非法版本 | EV_CURRENT | 1 | 当前版本 |
下面我们通过一个实际例子加深一下印象,代码非常简单打印“Hello world!”。
hello.cpp
#include <stdio.h>
int main(int argc, char* argv[]){
printf("Hello world!\n");
return 0;
}
先编译为 .o 文件,然后再去生成可执行文件。
gcc -c hello.cpp & gcc -o hello hello.o
现在目录下已经存在两个 ELF 文件了,其一为 hello.o,其二为 hello。
readelf -h hello.o 查看 hello.o 的文件头详情。
readelf -h hello 查看 hello 的文件头详情,可以对比一下和 hello.o 的差异。
此二者最明显的区别在于 Type 不一样了,另外入口地址其一是 0,其二却不是…
5 ELF 节区(Sections)
节区中包含目标文件中的所有信息,除了 ELF 头部、程序头部表格、节区头部表格。节区满足以下条件:
- 目标文件中的每个节区都有对应的节区头部描述它,反过来,有节区头部不意味着有节区。
- 每个节区占用文件中一个连续字节区域(这个区域可能长度为 0)。
- 文件中的节区不能重叠,不允许一个字节存在于两个节区中的情况发生。
- 目标文件中可能包含非活动空间(INACTIVE SPACE)。这些区域不属于任何头部和节区,其内容未指定。
5.1 ELF 节区头部表格
ELF 头部中,e_shoff 成员给出从文件头到节区头部表格的偏移字节数;e_shnum 给出表格中条目数目;e_shentsize 给出每个项(条)目的字节数。从这些信息中可以确切地定位节区的具体位置和长度。
节区头部表格中比较特殊的几个下标如下:
名称 | 取值 | 说明 |
---|
SHN_UNDEF | 0 | 标记未定义的、缺失的、不相关的,或者没有含义的节区引用。 | SHN_LORESERVE | 0XFF00 | 保留索引的下界。 | SHN_LOPROC | 0XFF00 | 保留索引的下界。 | SHN_HIPROC | 0XFF1F | 保留给处理器特殊的语义。 | SHN_ABS | 0XFFF1 | 包含对应引用量的绝对取值。这些值不会被重定位所影响。 | SHN_COMMON | 0XFFF2 | 相对于此节区定义的符号是公共符号。如 FORTRAN 中 COMMON 或者未分配的 C 外部变量。 | SHN_HIRESERVE | 0XFFFF | 保留索引的上界。 |
介于 SHN_LORESERVE 和 SHN_HIRESERVE 之间的表项不会出现在节区头部表中。
5.1.1 ELF 节区头部表表项
节区头部表实际上是一个 Elf_Shdr[m] 数组,其中的每一个元素(表项)记录系统中一个节区的信息,包括如节区名、类型、flag、内存/文件起始地址、大小、对齐等信息。
include/uapi/linux/elf.h
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;
Elf64_Word sh_type;
Elf64_Xword sh_flags;
Elf64_Addr sh_addr;
Elf64_Off sh_offset;
Elf64_Xword sh_size;
Elf64_Word sh_link;
Elf64_Word sh_info;
Elf64_Xword sh_addralign;
Elf64_Xword sh_entsize;
} Elf64_Shdr;
对其中各个字段的解释如下:
成员 | 说明 |
---|
sh_name | 给出节区名称。是节区头部字符串表节区(Section Header String Table Section)的索引。名字是一个 NULL 结尾的字符串。 | sh_type | 为节区的内容和语义进行分类。参见节区类型。 | sh_flags | 节区支持 1 位形式的标志,这些标志描述了多种属性。 | sh_addr | 如果节区将出现在进程的内存映像中,此成员给出节区的第一个字节应处的位置。否则,此字段为 0。 | sh_offset | 此成员的取值给出节区的第一个字节与文件头之间的偏移。不过,SHT_NOBITS 类型的节区不占用文件的空间,因此其 sh_offset 成员给出的是其概念性的偏移。 | sh_size | 此 成 员 给 出 节 区 的 长 度 ( 字 节 数 )。 除 非 节 区 的 类 型 是 SHT_NOBITS , 否 则 节区 占 用 文 件中 的 sh_size 字 节 。类型为 SHT_NOBITS 的节区长度可能非零,不过却不占用文件中的空间。 | sh_link | 此成员给出节区头部表索引链接。其具体的解释依赖于节区类型。 | sh_info | 此成员给出附加信息,其解释依赖于节区类型。 | sh_addralign | 某 些 节 区 带 有 地 址 对 齐 约 束 。 例 如 , 如 果 一 个 节 区 保 存 一 个 doubleword,那么系统必须保证整个节区能够按双字对齐。sh_addr 对 sh_addralign 取模,结果必须为 0。目前仅允许取值为 0 和 2 的幂次数。数值 0 和 1 表示节区没有对齐约束。 | sh_entsize | 某些节区中包含固定大小的项目,如符号表。对于这类节区,此成员给出每个表项的长度字节数。如果节区中并不包含固定长度表项的表格,此成员取值为 0。 |
索引为零(SHN_UNDEF)的节区头部也是存在的,尽管此索引标记的是未定义的节区引用。这个节区的内容固定如下:
字段名称 | 取值 | 说明 |
---|
sh_name | 0 | 无名称 | sh_type | SHT_NULL | 非活动 | sh_flags | 0 | 无标志 | sh_addr | 0 | 无地址 | sh_offset | 0 | 无文件偏移 | sh_size | 0 | 无尺寸大小 | sh_link | SHN_UNDEF | 无链接信息 | sh_info | 0 | 无辅助信息 | sh_addralign | 0 | 无对齐要求 | sh_entsize | 0 | 无表项 |
通过 readelf -S 可读取节区头部表内容,例如 readelf -S hello.o ,这个 .o 文件一共含有 13 个表项,起始位置偏移量为 0x2d8。
再来看 hello 可执行文件的节区头部表(readelf -S hello )。
snake@snake:~/Test$ readelf -S hello
There are 29 section headers, starting at offset 0x1930:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000000238 00000238
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.ABI-tag NOTE 0000000000000254 00000254
0000000000000020 0000000000000000 A 0 0 4
[ 3] .note.gnu.build-i NOTE 0000000000000274 00000274
0000000000000024 0000000000000000 A 0 0 4
[ 4] .gnu.hash GNU_HASH 0000000000000298 00000298
000000000000001c 0000000000000000 A 5 0 8
[ 5] .dynsym DYNSYM 00000000000002b8 000002b8
00000000000000a8 0000000000000018 A 6 1 8
[ 6] .dynstr STRTAB 0000000000000360 00000360
0000000000000082 0000000000000000 A 0 0 1
[ 7] .gnu.version VERSYM 00000000000003e2 000003e2
000000000000000e 0000000000000002 A 5 0 2
[ 8] .gnu.version_r VERNEED 00000000000003f0 000003f0
0000000000000020 0000000000000000 A 6 1 8
[ 9] .rela.dyn RELA 0000000000000410 00000410
00000000000000c0 0000000000000018 A 5 0 8
[10] .rela.plt RELA 00000000000004d0 000004d0
0000000000000018 0000000000000018 AI 5 22 8
[11] .init PROGBITS 00000000000004e8 000004e8
0000000000000017 0000000000000000 AX 0 0 4
[12] .plt PROGBITS 0000000000000500 00000500
0000000000000020 0000000000000010 AX 0 0 16
[13] .plt.got PROGBITS 0000000000000520 00000520
0000000000000008 0000000000000008 AX 0 0 8
[14] .text PROGBITS 0000000000000530 00000530
00000000000001a2 0000000000000000 AX 0 0 16
[15] .fini PROGBITS 00000000000006d4 000006d4
0000000000000009 0000000000000000 AX 0 0 4
[16] .rodata PROGBITS 00000000000006e0 000006e0
0000000000000011 0000000000000000 A 0 0 4
[17] .eh_frame_hdr PROGBITS 00000000000006f4 000006f4
000000000000003c 0000000000000000 A 0 0 4
[18] .eh_frame PROGBITS 0000000000000730 00000730
0000000000000108 0000000000000000 A 0 0 8
[19] .init_array INIT_ARRAY 0000000000200db8 00000db8
0000000000000008 0000000000000008 WA 0 0 8
[20] .fini_array FINI_ARRAY 0000000000200dc0 00000dc0
0000000000000008 0000000000000008 WA 0 0 8
[21] .dynamic DYNAMIC 0000000000200dc8 00000dc8
00000000000001f0 0000000000000010 WA 6 0 8
[22] .got PROGBITS 0000000000200fb8 00000fb8
0000000000000048 0000000000000008 WA 0 0 8
[23] .data PROGBITS 0000000000201000 00001000
0000000000000010 0000000000000000 WA 0 0 8
[24] .bss NOBITS 0000000000201010 00001010
0000000000000008 0000000000000000 WA 0 0 1
[25] .comment PROGBITS 0000000000000000 00001010
0000000000000029 0000000000000001 MS 0 0 1
[26] .symtab SYMTAB 0000000000000000 00001040
00000000000005e8 0000000000000018 27 43 8
[27] .strtab STRTAB 0000000000000000 00001628
0000000000000205 0000000000000000 0 0 1
[28] .shstrtab STRTAB 0000000000000000 0000182d
00000000000000fe 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
5.2 节区类型
sh_type 字段指示了节区类型,节区类型定义如下表。
名称 | 取值 | 说明 |
---|
SHT_NULL | 0 | 此值标志节区头部是非活动的,没有对应的节区。此节区头部中的其他成员取值无意义。 | SHT_PROGBITS | 1 | 此节区包含程序定义的信息,其格式和含义都由程序来解释。 | SHT_SYMTAB | 2 | 此节区包含一个符号表。一般,SHT_SYMTAB 节区提供用于链接编辑(指 ld 而言)的符号,尽管也可用来实现动态链接。 | SHT_STRTAB | 3 | 此节区包含字符串表。目标文件可能包含多个字符串表节区。 | SHT_RELA | 4 | 此节区包含重定位表项,其中可能会有补齐内容(addend),例如 32 位目标文件中的 Elf32_Rela 类型。目标文件可能拥有多个重定位节区。 | SHT_HASH | 5 | 此节区包含符号哈希表。所有参与动态链接的目标都必须包含一个符号哈希表。 | SHT_DYNAMIC | 6 | 此节区包含动态链接的信息。 | SHT_NOTE | 7 | 此节区包含以某种方式来标记文件的信息。 | SHT_NOBITS | 8 | 这 种 类 型 的 节 区 不 占 用 文 件 中 的 空 间 , 其 他 方 面 和 SHT_PROGBITS 相似。尽管此节区不包含任何字节,成员 sh_offset 中还是会包含概念性的文件偏移。 | SHT_REL | 9 | 此节区包含重定位表项,其中没有补齐(addends),例如 32 位目标文件中的 Elf32_rel 类型。目标文件中可以拥有多个重定位节区。 | SHT_SHLIB | 10 | 此节区被保留,不过其语义是未规定的。包含此类型节区的程序与 ABI 不兼容。 | SHT_DYNSYM | 11 | 作为一个完整的符号表,它可能包含很多对动态链接而言不必要的符号。因此,目标文件也可以包含一个 SHT_DYNSYM 节区,其中保存动态链接符号的一个最小集合,以节省空间。 | SHT_LOPROC | 0X70000000 | 保留给处理器专用语义的索引下界。 | SHT_HIPROC | OX7FFFFFFF | 保留给处理器专用语义的索引上界。 | SHT_LOUSER | 0X80000000 | 此值给出保留给应用程序的索引下界。 | SHT_HIUSER | 0X8FFFFFFF | 此值给出保留给应用程序的索引上界。 |
5.3 节区 flag
sh_flags 字段定义了一个节区中包含的内容是否可以修改、是否可以执行等信息。如果一个标志位被设置,则该位取值为 1。未定义的各位都设置为 0。
名称 | 取值 |
---|
SHF_WRITE | 0x1 | SHF_ALLOC | 0x2 | SHF_EXECINSTR | 0x4 | SHF_MASKPROC | 0xF0000000 |
- SHF_WRITE: 节区包含进程执行过程中将可写的数据。
- SHF_ALLOC: 此节区在进程执行过程中占用内存。某些控制节区并不出现于目标文件的内存映像中,对于那些节区,此位应设置为 0。
- SHF_EXECINSTR: 节区包含可执行的机器指令。
- SHF_MASKPROC: 所有包含于此掩码中的四位都用于处理器专用的语义。
5.4 节区 sh_link 和 sh_info 字段
根据节区类型的不同,sh_link 和 sh_info 的具体含义也有所不同:
sh_type | sh_link | sh_info |
---|
SHT_DYNAMIC | 此节区中条目所用到的字符串表格的节区头部索引 0 | | SHT_HASH | 此哈希表所适用的符号表的节区头部索引 0 | | SHT_REL SHT_RELA | 相关符号表的节区头部索引 | 重定位所适用的节区的节区头部索引 | SHT_SYMTAB SHT_DYNSYM | 相关联的字符串表的节区头部索引 | 最后一个局部符号(绑定 STB_LOCAL)的符号表索引值加一 | 其它 | SHN_UNDEF | 0 |
5.5 特殊节区
很多节区中包含了程序和控制信息。下面的表中给出了系统使用的节区,以及它们的类型和属性。
名称 | 类型 | 属性 | 含义 |
---|
.bss | SHT_NOBITS | SHF_ALLOC + SHF_WRITE | 包含将出现在程序的内存映像中的未初始化数据。根据定义,当程序开始执行,系统将把这些数据初始化为 0。此节区不占用文件空间。 | .comment | SHT_PROGBITS | (无) | 包含版本控制信息。 | .data | SHT_PROGBITS | SHF_ALLOC + SHF_WRITE | 此节区包含初始化了的数据,将出现在程序的内存映像中。 | .data1 | SHT_PROGBITS | SHF_ALLOC + SHF_WRITE | 同上 | .debug | SHT_PROGBITS | (无) | 此节区包含用于符号调试的信息。 | .dynamic | SHT_DYNAMIC | | 此节区包含动态链接信息。节区的属性将包含 SHF_ALLOC 位。是否 SHF_WRITE 位被设置取决于处理器。 | .dynstr | SHT_STRTAB | SHF_ALLOC | 此节区包含用于动态链接的字符串,大多数情况下这些字符串代表了与符号表项相关的名称。 | .dynsym | SHT_DYNSYM | SHF_ALLOC | 此节区包含了动态链接符号表。 | .fini | SHT_PROGBITS | SHF_ALLOC + SHF_EXECINSTR | 此节区包含了可执行的指令,是进程终止代码的一部分。程序正常退出时,系统将安排执行这里的代码。 | .got | SHT_PROGBITS | | 此节区包含全局偏移表。 | .hash | SHT_HASH | SHF_ALLOC | 此节区包含了一个符号哈希表。 | .init | SHT_PROGBITS | SHF_ALLOC + SHF_EXECINSTR | 此节区包含了可执行指令,是进程初始化代码的一部分。当程序开始执行时,系统要在开始调用主程序入口之前(通常指 C 语言的 main 函数)执行这些代码。 | .interp | SHT_PROGBITS | | 此节区包含程序解释器的路径名。如果程序包含一个可加载的段,段中包含此节区,那么节区的属性将包含 SHF_ALLOC 位,否则该位为 0。 | .line | SHT_PROGBITS | (无) | 此节区包含符号调试的行号信息,其中描述了源程序与机器指令之间的对应关系。其内容是未定义的。 | .note | SHT_NOTE | (无) | 此节区中包含注释信息,有独立的格式。 | .plt | SHT_PROGBITS | | 此节区包含过程链接表(procedure linkage table)。 | .relname | SHT_REL | | 此节区包含了重定位信息。如果文件中包含可加载的段,段中有重定位内容,节区的属性将包含 SHF_ALLOC 位,否则该位置 0。传统上 name 根据重定位所适用的节区给定。例如 .text 节区的重定位节区名字将是:.rel.text 或者 .rela.text。 | .relaname | SHT_RELA | | 同上 | .rodata | SHT_PROGBITS | SHF_ALLOC | 此节区包含只读数据,这些数据通常参与进程映像的不可写段。 | .rodata1 | SHT_PROGBITS | SHF_ALLOC | 同上 | .shstrtab | SHT_STRTAB | | 此节区包含节区名称。 | .strtab | SHT_STRTAB | | 此节区包含字符串,通常是代表与符号表项相关的名称。如果文件拥有一个可加载的段,段中包含符号串表,节区的属性将包含 SHF_ALLOC 位,否则该位为 0。 | .symtab | SHT_SYMTAB | | 此节区包含一个符号表。如果文件中包含一个可加载的段,并且该段中包含符号表,那么节区的属性中包含SHF_ALLOC 位,否则该位置为 0。 | .text | SHT_PROGBITS | SHF_ALLOC + SHF_EXECINSTR | 此节区包含程序的可执行指令。 |
在分析这些节区的时候,需要注意如下事项:
- 以“.”开头的节区名称是系统保留的。应用程序可以使用没有前缀的节区名称,以避免与系统节区冲突。
- 目标文件格式允许人们定义不在上述列表中的节区。
- 目标文件中也可以包含多个名字相同的节区。
- 保留给处理器体系结构的节区名称一般构成为:处理器体系结构名称简写 + 节区名称。处理器名称应该与 e_machine 中使用的名称相同。例如 .FOO.psect 节区是由 FOO 体系结构定义的 psect 节区。
另外,有些编译器对如上节区进行了扩展,这些已存在的扩展都使用约定俗成的名称,如:
- .sdata
- .tdesc
- .sbss
- .lit4
- .lit8
- .reginfo
- .gptab
- .liblist
- .conflict
6 字符串表(String Table)
字符串表节区包含以 NULL(ASCII 码 0)结尾的字符序列,通常称为字符串。ELF 目标文件通常使用字符串来表示符号和节区名称。对字符串的引用通常以字符串在字符串表中的下标给出。
一般,第一个字节(索引为 0)定义为一个空字符串。类似的,字符串表的最后一个字节也定义为 NULL,以确保所有的字符串都以 NULL 结尾。索引为 0 的字符串在不同的上下文中可以表示无名或者名字为 NULL 的字符串。
允许存在空的字符串表节区,其节区头部的 sh_size 成员应该为 0。对空的字符串表而言,非 0 的索引值是非法的。
例如:对于各个节区而言,节区头部的 sh_name 成员包含其对应的节区头部字符串表节区的索引,此节区由 ELF 头的 e_shstrndx 成员给出。下图给出了包含 25 个字节的一个字符串表,以及与不同索引相关的字符串。
上图包含的字符串如下:
索引 | 字符串 |
---|
0 | (无) | 1 | name. | 7 | Variable | 11 | able | 16 | able | 24 | (空字符串) |
在使用、分析字符串表时,要注意以下几点:
- 字符串表索引可以引用节区中任意字节。
- 字符串可以出现多次。
- 可以存在对子字符串的引用。
- 同一个字符串可以被引用多次。
- 字符串表中也可以存在未引用的字符串。
下面来观察一下 hello.o 和 hello 文件的节区名字符串表(.shstrtab)readelf -p .shstrtab hello.o & readelf -p .shstrtab hello 。
7 符号表(Symbol Table)
目标文件的符号表中包含用来定位、重定位程序中符号定义和引用的信息。符号表索引是对此数组的索引。索引 0 表示表中的第一表项,同时也作为未定义符号的索引。例如,printf() 函数会在动态符号表 .dynsym 中存有一个指向该函数的符号条目。在大多数共享库和动态链接可执行文件中,存在两个符号表(.dynsym 和 .symtab)。
.dynsym 保存了引用来自外部文件符号的全局符号,如 printf 这样的库函数,.dynsym 保存的符号是 .symtab 所保存符号的子集,.symtab 中还保存了可执行文件的本地符号,如全局变量,或者代码中定义的本地函数等。因此,.symtab 保存了所有的符号,而.dynsym 只保存动态/全局符号。
因此,就存在这样一个问题:既然 .symtab 中保存了.dynsym 中所有的符号,那么为什么还需要两个符号表呢?使用 readelf –S 命令查看可执行文件的输出,可以看到一部分节被标记为了A(ALLOC)、WA(WRITE/ALLOC)或者 AX(ALLOC/EXEC)。.dynsym 是被标记了 ALLOC 的,而 .symtab 则没有标记。
ALLOC 表示有该标记的节会在运行时分配并装载进入内存,而 .symtab 不是在运行时必需的,因此不会被装载到内存中。.dynsym 保存的符号只能在运行时被解析,因此是运行时动态链接器所需要的唯一符号。 .dynsym 符号表对于动态链接可执行文件的执行来说是必需的,而 .symtab 符号表只是用来进行调试和链接的,有时候为了节省空间,会将 .symtab 符号表从生产二进制文件中删掉。
include/uapi/linux/elf.h
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;
unsigned char st_info;
unsigned char st_other;
Elf64_Half st_shndx;
Elf64_Addr st_value;
Elf64_Xword st_size;
} Elf64_Sym;
其中各个字段的含义如下表。
字段 | 说明 |
---|
st_name | 包含目标文件符号字符串表的索引,其中包含符号名的字符串表示。如果该值非 0,则它表示了给出符号名的字符串表索引,否则符号表项没有名称。注:外部 C 符号在 C 语言和目标文件的符号表中具有相同的名称。 | st_value | 此成员给出相关联的符号的取值。依赖于具体的上下文,它可能是一个绝对值、一个地址等等。 | st_size | 很多符号具有相关的尺寸大小。例如一个数据对象的大小是对象中包含的字节数。如果符号没有大小或者大小未知,则此成员为 0。 | st_info | 此成员给出符号的类型和绑定属性。 | st_other | 该成员当前指定符号的可见性。下面的代码展示了如何操作32位和64位对象的值。其他位包含0,没有定义。
#define ELF32_ST_VISIBILITY(o) ((o)&0x3)
#define ELF64_ST_VISIBILITY(o) ((o)&0x3) | st_shndx | 每个符号表项都以和其他节区间的关系的方式给出定义。此成员给出相关的节区头部表索引。某些索引具有特殊含义。 |
7.1 符号表符号类型和绑定信息
st_info 中包含符号类型和绑定信息,操纵方式如:
include/uapi/linux/elf.h
#define ELF_ST_BIND(x) ((x) >> 4)
#define ELF_ST_TYPE(x) (((unsigned int) x) & 0xf)
#define ELF32_ST_BIND(x) ELF_ST_BIND(x)
#define ELF32_ST_TYPE(x) ELF_ST_TYPE(x)
#define ELF64_ST_BIND(x) ELF_ST_BIND(x)
#define ELF64_ST_TYPE(x) ELF_ST_TYPE(x)
从中可以看出,st_info 的高四位表示符号绑定,用于确定链接可见性和行为。具体的绑定类型如:
名称 | 取值 | 说明 |
---|
STB_LOCAL | 0 | 局部符号在包含该符号定义的目标文件以外不可见。相同名称的局部符号可以存在于多个文件中,互不影响。 | STB_GLOBAL | 1 | 全局符号对所有将组合的目标文件都是可见的。一个文件中对某个全局符号的定义将满足另一个文件对相同全局符号的未定义引用。 | STB_WEAK | 2 | 弱符号与全局符号类似,不过它们的定义优先级比较低。 | STB_LOOS | 10 | 为特定于操作系统的语义保留的值下界。 | STB_HIOS | 12 | 为特定于操作系统的语义保留的值上界。 | STB_LOPROC | 13 | 为特定于处理器的语义保留的值下界。如果指定了含义,则处理器补充解释它们。 | STB_HIPROC | 15 | 为特定于处理器的语义保留的值上界。如果指定了含义,则处理器补充解释它们。 |
全局符号与弱符号之间的区别主要有两点:
- 当 链 接 编 辑 器 组 合 若 干 可 重 定 位 的 目 标 文 件 时 , 不 允 许 对 同 名 的 STB_GLOBAL 符号给出多个定义。另一方面如果一个已定义的全局符号已经存在,出现一个同名的弱符号并不会产生错误。链接编辑器尽关心全局符号,忽略弱符号。类似地,如果一个公共符号(符号的 st_shndx 中包含 SHN_COMMON),那么具有相同名称的弱符号出现也不会导致错误。链接编辑器会采纳公共定义,而忽略弱定义。
- 当链接编辑器搜索归档库(archive libraries)时,会提取那些包含未定义全局符号的档案成员。成员的定义可以是全局符号,也可以是弱符号。连接编辑器不会提取档案成员来满足未定义的弱符号。未能解析的弱符号取值为 0。
在每个符号表中,所有具有 STB_LOCAL 绑定的符号都优先于弱符号和全局符号。符号表节区中的 sh_info 头部成员包含第一个非局部符号的符号表索引。
符号类型定义如下:
名称 | 取值 | 说明 |
---|
STT_NOTYPE | 0 | 符号的类型没有指定。 | STT_OBJECT | 1 | 符号与某个数据对象相关,比如一个变量、数组等等。 | STT_FUNC | 2 | 符号与某个函数或者其他可执行代码相关。 | STT_SECTION | 3 | 符号与某个节区相关。这种类型的符号表项主要用于重定位,通常具有 STB_LOCAL 绑定。 | STT_FILE | 4 | 传统上,符号的名称给出了与目标文件相关的源文件的名称。文件符号具有 STB_LOCAL 绑定,其节区索引是 SHN_ABS,并且它优先于文件的其他 STB_LOCAL 符号(如果有的话)。 | STT_COMMON | 5 | 该符号标记了一个未初始化的公共块。 | STT_TLS | 6 | 该符号指定线程本地存储实体。当被定义时,它会给出符号赋值的偏移量,而不是实际的地址。类型为 STT_TLS 的符号只能被特殊的线程本地存储重定位引用,而线程本地存储重定位只能引用类型为 STT_TLS 的符号。实现不需要支持线程本地存储。 | STT_LOOS | 10 | 为特定于操作系统的语义保留的值下界。 | STT_HIOS | 12 | 为特定于操作系统的语义保留的值上界。 | STT_LOPROC | 13 | 为特定于处理器的语义保留的值下界。如果指定了含义,则处理器补充解释它们。 | STT_HIPROC | 15 | 为特定于处理器的语义保留的值上界。如果指定了含义,则处理器补充解释它们。 |
共享目标文件中的函数符号(类型为 STT_FUNC的)具有特殊意义。当另一个对象文件引用共享对象中的函数时,链接编辑器自动为所引用的符号创建一个过程链接表项。类型不是 STT_FUNC 的共享对象符号不会通过过程链接表自动引用。
带有 STT_COMMON 类型标签的符号未初始化公共块。在可重定位对象中,这些符号不被分配,必须具有特殊的节索引 SHN_COMMON。在共享对象和可执行文件中,这些符号必须分配给定义对象中的某些部分。
在可重定位对象中,类型为 STT_COMMON 的符号与索引为 SHN_COMMON 的其他符号一样。如果链接编辑器在它生成的对象的输出部分为 SHN_COMMON 符号分配空间,它必须将输出符号的类型保留为 STT_COMMON。
当动态链接器遇到一个解析为 STT_COMMON 类型定义的符号引用时,它可以(但不是必须)改变它的符号解析规则如下:动态链接器不将引用绑定到具有给定名称的第一个符号,而是搜索具有该名称的类型非 STT_COMMON 的第一个符号。如果没有找到这样的符号,它就查找该名称的 STT_COMMON 定义中大小最大的那个。
如果一个符号的取值引用了某个节区中的特定位置,那么它的节区索引成员(st_shndx)包含了其在节区头部表中的索引。当节区在重定位过程中被移动时,符号的取值也会随之变化,对符号的引用始终会“指向”程序中的相同位置。
7.2 符号表特殊的节区索引
某些特殊的节区索引具有不同的语义:
SHN_ABS: 符号具有绝对取值,不会因为重定位而发生变化。
SHN_COMMON: 符号标注了一个尚未分配的公共块。符号的取值给出了对齐约束,与节区的 sh_addralign 成员类似。就是说,链接编辑器将为符号分配存储空间,地址位于 st_value 的倍数处。符号的大小给出了所需要的字节数。
SHN_UNDEF: 此节区表索引值意味着符号没有定义。当链接编辑器将此目标文件与其它定义了该符号的目标文件进行组合时,此文件中对该符号的引用将被链接到实际定义的位置。
7.3 STN_UNDEF 符号
如上所述,符号表中下标为 0(STN_UNDEF)的表项被保留。其中包含如下数值:
名称 | 取值 | 说明 |
---|
st_name | 0 | 无名称 | st_value | 0 | 0 值 | st_size | 0 | 无大小 | st_info | 0 | 无类型,局部绑定 | st_other | 0 | 无附加信息 | st_shndx | 0 | 无节区 |
7.4 符号取值
不同的目标文件类型中符号表项对 st_value 成员具有不同的解释:
- 在可重定位文件中,st_value 中遵从了节区索引为 SHN_COMMON 的符号的对齐约束。
- 在可重定位的文件中,st_value 中包含已定义符号的节区偏移。就是说,st_value 是从 st_shndx 所标识的节区头部开始计算,到符号位置的偏移。
- 在可执行和共享目标文件中,st_value 包含一个虚地址。为了使得这些文件的符号对动态链接器更有用,节区偏移(针对文件的解释)让位于虚拟地址(针对内存的解释),因为这时与节区号无关。
尽管符号表取值在不同的目标文件中具有相似的含义,适当的程序可以采取高效的数据访问方式。
下面来看前面提及的 hello.o 和 hello 文件的符号表。
hello 可执行文件包含了 .dynsym 和 .symtab 两个符号表。而 hello.o 则不包含 .dynsym。
snake@snake:~/Test$ readelf -s hello
Symbol table '.dynsym' contains 7 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5 (2)
3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
4: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
5: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
6: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5 (2)
Symbol table '.symtab' contains 63 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000238 0 SECTION LOCAL DEFAULT 1
2: 0000000000000254 0 SECTION LOCAL DEFAULT 2
3: 0000000000000274 0 SECTION LOCAL DEFAULT 3
4: 0000000000000298 0 SECTION LOCAL DEFAULT 4
5: 00000000000002b8 0 SECTION LOCAL DEFAULT 5
6: 0000000000000360 0 SECTION LOCAL DEFAULT 6
7: 00000000000003e2 0 SECTION LOCAL DEFAULT 7
8: 00000000000003f0 0 SECTION LOCAL DEFAULT 8
9: 0000000000000410 0 SECTION LOCAL DEFAULT 9
10: 00000000000004d0 0 SECTION LOCAL DEFAULT 10
11: 00000000000004e8 0 SECTION LOCAL DEFAULT 11
12: 0000000000000500 0 SECTION LOCAL DEFAULT 12
13: 0000000000000520 0 SECTION LOCAL DEFAULT 13
14: 0000000000000530 0 SECTION LOCAL DEFAULT 14
15: 00000000000006d4 0 SECTION LOCAL DEFAULT 15
16: 00000000000006e0 0 SECTION LOCAL DEFAULT 16
17: 00000000000006f4 0 SECTION LOCAL DEFAULT 17
18: 0000000000000730 0 SECTION LOCAL DEFAULT 18
19: 0000000000200db8 0 SECTION LOCAL DEFAULT 19
20: 0000000000200dc0 0 SECTION LOCAL DEFAULT 20
21: 0000000000200dc8 0 SECTION LOCAL DEFAULT 21
22: 0000000000200fb8 0 SECTION LOCAL DEFAULT 22
23: 0000000000201000 0 SECTION LOCAL DEFAULT 23
24: 0000000000201010 0 SECTION LOCAL DEFAULT 24
25: 0000000000000000 0 SECTION LOCAL DEFAULT 25
26: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c
27: 0000000000000560 0 FUNC LOCAL DEFAULT 14 deregister_tm_clones
28: 00000000000005a0 0 FUNC LOCAL DEFAULT 14 register_tm_clones
29: 00000000000005f0 0 FUNC LOCAL DEFAULT 14 __do_global_dtors_aux
30: 0000000000201010 1 OBJECT LOCAL DEFAULT 24 completed.7698
31: 0000000000200dc0 0 OBJECT LOCAL DEFAULT 20 __do_global_dtors_aux_fin
32: 0000000000000630 0 FUNC LOCAL DEFAULT 14 frame_dummy
33: 0000000000200db8 0 OBJECT LOCAL DEFAULT 19 __frame_dummy_init_array_
34: 0000000000000000 0 FILE LOCAL DEFAULT ABS hello.cpp
35: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c
36: 0000000000000834 0 OBJECT LOCAL DEFAULT 18 __FRAME_END__
37: 0000000000000000 0 FILE LOCAL DEFAULT ABS
38: 0000000000200dc0 0 NOTYPE LOCAL DEFAULT 19 __init_array_end
39: 0000000000200dc8 0 OBJECT LOCAL DEFAULT 21 _DYNAMIC
40: 0000000000200db8 0 NOTYPE LOCAL DEFAULT 19 __init_array_start
41: 00000000000006f4 0 NOTYPE LOCAL DEFAULT 17 __GNU_EH_FRAME_HDR
42: 0000000000200fb8 0 OBJECT LOCAL DEFAULT 22 _GLOBAL_OFFSET_TABLE_
43: 00000000000006d0 2 FUNC GLOBAL DEFAULT 14 __libc_csu_fini
44: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab
45: 0000000000201000 0 NOTYPE WEAK DEFAULT 23 data_start
46: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@@GLIBC_2.2.5
47: 0000000000201010 0 NOTYPE GLOBAL DEFAULT 23 _edata
48: 00000000000006d4 0 FUNC GLOBAL DEFAULT 15 _fini
49: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@@GLIBC_
50: 0000000000201000 0 NOTYPE GLOBAL DEFAULT 23 __data_start
51: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
52: 0000000000201008 0 OBJECT GLOBAL HIDDEN 23 __dso_handle
53: 00000000000006e0 4 OBJECT GLOBAL DEFAULT 16 _IO_stdin_used
54: 0000000000000660 101 FUNC GLOBAL DEFAULT 14 __libc_csu_init
55: 0000000000201018 0 NOTYPE GLOBAL DEFAULT 24 _end
56: 0000000000000530 43 FUNC GLOBAL DEFAULT 14 _start
57: 0000000000201010 0 NOTYPE GLOBAL DEFAULT 24 __bss_start
58: 000000000000063a 34 FUNC GLOBAL DEFAULT 14 main
59: 0000000000201010 0 OBJECT GLOBAL HIDDEN 23 __TMC_END__
60: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
61: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@@GLIBC_2.2
62: 00000000000004e8 0 FUNC GLOBAL DEFAULT 11 _init
8 重定位信息
重定位是将符号引用与符号定义进行连接的过程。例如,当程序调用了一个函数时,相关的调用指令必须把控制传输到适当的目标执行地址。
8.1 重定位表项
可重定位文件必须包含如何修改其节区内容的信息,从而允许可执行文件和共享目标文件保存进程的程序映像的正确信息。重定位表项就是这样一些数据。
include/uapi/linux/elf.h
typedef struct elf32_rel {
Elf32_Addr r_offset;
Elf32_Word r_info;
} Elf32_Rel;
typedef struct elf64_rel {
Elf64_Addr r_offset;
Elf64_Xword r_info;
} 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;
Elf64_Xword r_info;
Elf64_Sxword r_addend;
} Elf64_Rela;
成员 | 说明 |
---|
r_offset | 此成员给出了重定位动作所适用的位置。对于一个可重定位文件而言,此值是从节区头部开始到将被重定位影响的存储单位之间的字节偏移。对于可执行文件或者共享目标文件而言,其取值是被重定位影响到的存储单元的虚拟地址。 | r_info | 此成员给出要进行重定位的符号表索引,以及将实施的重定位类型。例如一个调用指令的重定位项将包含被调用函数的符号表索引。如果索引是 STN_UNDEF,那么重定位使用 0 作为“符号值”。重定位类型是和处理器相关的。当程序代码引用一个重定位项的重定位类型或者符号表索引,则表示对表项的 r_info 成员应用 ELFx_R_TYPE 或者 ELFx_R_SYM 的结果(x 等于 32 或 64)。
#define ELF32_R_SYM(x) ((x) >> 8) #define ELF32_R_TYPE(x) ((x) & 0xff)
#define ELF64_R_SYM(i) ((i) >> 32) #define ELF64_R_TYPE(i) ((i) & 0xffffffff) | r_addend | 此成员给出一个常量补齐,用来计算将被填充到可重定位字段的数值。 |
如上所述,只有 Elf32_Rela (Elf64_Rela)项目可以明确包含补齐信息。类型为 Elf32_Rel(Elf64_Rel) 的表项在将被修改的位置保存隐式的补齐信息。 依赖于处理器体系结构,各种形式都可能存在,甚至是必需的。因此,对特定机器的实现可以仅使用一种形式,也可以根据上下文使用不同的形式。
重定位节区会引用两个其它节区:符号表、要修改的节区。节区头部的 sh_info 和 sh_link 成员给出这些关系。不同目标文件的重定位表项对 r_offset 成员具有略微不同的解释。
- 在可重定位文件中,r_offset 中包含节区偏移。就是说重定位节区自身描述了如何修改文件中的其他节区;重定位偏移指定了被修改节区中的一个存储单元。
- 在可执行文件和共享的目标文件中,r_offset 中包含一个虚拟地址。为了使得这些文件的重定位表项对动态链接器更为有用,节区偏移(针对文件的解释)让位于虚地址(针对内存的解释)。
尽管对 r_offset 的解释会有少许不同,重定位类型的含义始终不变。
8.2 重定位类型
重定位表项描述如何修改后面的指令和数据字段。一般,共享目标文件在创建时,其基 ? 虚拟地址是 0,不过执行地址将随着动态加载而发生变化。
重定位的过程,按照如下标记:
-
A 用来计算可重定位字段的取值的补齐。 -
B 共享目标在执行过程中被加载到内存中的位置(基地址)。 -
G 在执行过程中,重定位项的符号的地址所处的位置 —— 全局偏移表的索引。 -
GOT 全局偏移表(GOT)的地址。 -
L 某个符号的过程链接表项的位置(节区偏移/地址)。过程链接表项把函数调用重定位到正确的目标位置。链接编辑器构造初始的过程链接表,动态链接器在执行过程中修改这些项目。 -
P 存储单位被重定位(用 r_offset 计算)到的位置(节区偏移或者地址)。 -
S 其索引位于重定位项中的符号的取值。
重定位项的 r_offset 取值给定受影响的存储单位的第一个字节的偏移或者虚拟地址。重定位类型给出那些位需要修改以及如何计算它们的取值。
下面是 x86 (32 位)体系结构下常见的重定位类型表。
下面研究一个例子,探究一下 R_386_PLT32 这种类型如何计算?先准备两个 c 文件。
obj1.c
void _start(){
work();
}
obj2.c
void work(){
}
接下来编译 obj1.c 和 obj2.c 生成 .o 文件,gcc 编译选项使用 -nostdlib(链接的时候不使用标准的系统启动文件和系统库)、-m32(生成 32 位机器的汇编代码) 和 -c(表示只编译源文件但不链接)。
gcc -nostdlib obj1.c -m32 -c & gcc -nostdlib obj2.c -m32 -c
编译 obj1.c 和 obj2.c 生成 .o 文件后,接着将obj1.o 和 obj2.o 合并为 obj 文件。
gcc -nostdlib obj1.o obj2.o -m32 -o obj
接着 dump obj1.o 、 obj2.o 和 obj 的汇编代码。
objdump -d obj1.o & objdump -d obj2.o & objdump -d obj
最后再来查看 obj1.o 的重定位节区信息(包含重定位表)。
readelf -r obj1.o
下图演示了整个流程。
不难从上图看出 L = 0x1d8,P = 0x1b5 + 0x14 = 0x1c9,A = 0xfffffffc = -4,
R_386_PLT32 类型时,公式为 L + A - P = 0x1d8 - 0x4 - 0x1c9 = 0xb(e8 0b 00 00 00 也就是 call 1d8 )。
9 程序加载和动态链接
实现程序加载和动态链接的主要技术有:
- 程序头部(Program Header):描述与程序执行直接相关的目标文件结构信息。用来在文件中定位各个段的映像。同时包含其他一些用来为程序创建进程映像所必需的信息。
- 程序加载:给定一个目标文件,系统加载该文件到内存中,启动程序执行。
- 动态链接:系统加载了程序以后,必须通过解析构成进程的目标文件之间的符号引用,以便完整地构造进程映像。
9.1 程序头部(Program Header)
可执行文件或者共享目标文件的程序头部是一个结构数组,每个结构描述了一个段或者系统准备程序执行所必需的其它信息。目标文件的“段”包含一个或者多个“节区”,也就是“段内容(Segment Contents)”。程序头部仅对于可执行文件和共享目标文件有意义。
可执行目标文件在 ELF 头部的 e_phentsize 和 e_phnum 成员中给出其自身程序头部的大小。
include/uapi/linux/elf.h
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;
Elf64_Addr p_vaddr;
Elf64_Addr p_paddr;
Elf64_Xword p_filesz;
Elf64_Xword p_memsz;
Elf64_Xword p_align;
} Elf64_Phdr;
其中各个字段说明如下:
字段 | 说明 |
---|
p_type | 此数组元素描述的段的类型,或者如何解释此数组元素的信息。 | p_offset | 此成员给出从文件头到该段第一个字节的偏移。 | p_vaddr | 此成员给出段的第一个字节将被放到内存中的虚拟地址。 | p_paddr | 此成员仅用于与物理地址相关的系统中。因为 System V 忽略所有应用程序的物理地址信息,此字段对与可执行文件和共享目标文件而言具体内容是未指定的。 | p_filesz | 此成员给出段在文件映像中所占的字节数。可以为 0。 | p_memsz | 此成员给出段在内存映像中占用的字节数。可以为 0。 | p_flags | 此成员给出与段相关的标志。 | p_align | 可加载的进程段的 p_vaddr 和 p_offset 取值必须合适,相对于对页面大小的取模而言。此成员给出段在文件中和内存中如何对齐。数值 0 和 1 表示不需要对齐。否则 p_align 应该是个正整数,并且是 2 的幂次数,p_vaddr 和 p_offset 对 p_align 取模后应该相等。 |
9.1.1 段类型
可执行 ELF 目标文件中的段类型如下表所示:
9.1.2 基地址(Base Address)
基地址用来对程序的内存映像进行重定位。可执行文件或者共享目标文件的基地址是在执行过程中从三个数值计算的:
- 内存加载地址
- 最大页面大小
- 程序的可加载段的最低虚地址
程序头部中的虚拟地址可能不能代表程序内存映像的实际虚地址。要计算基地址,首先要确定与 PT_LOAD 段的最低 p_vaddr 值相关的内存地址。通过对内存地址向最接近的最大页面大小截断,就可以得到基地址。根据要加载到内存中的文件的类型,内存地址可能与 p_vaddr 相同也可能不同。
如前所述,“.bss”节区的类型为 SHT_NOBITS。尽管它在文件中不占据空间,却会占据段的内存映像的空间。通常,这些未初始化的数据位于段的末尾,所以 p_memsz 会比 p_filesz 大。
9.1.3 注释节区(Note Section)
类型为 SHT_NOTE 的节区和类型为 PT_NOTE 的节区可以用作特殊信息的存放。节区和程序头部中的注释信息可以有任意多个条目,每个条目都是一个按目标处理器格式给出的 4 字节的数组。
注释节区示例:
其中:
- namesz 和 name 注 释 信 息 的 name 部 分 前 namesz 字 节 包 含 一 个 NULL 结尾的字符串,表示该项的属主或者发起者。没有正式的机制来避免名字冲突。如果没有名字,namesz 中包含 0。如果需要的话,可以用补零来确保描述符以 4 字节对齐。这类补齐并不包含在 namesz 中。
- descsz 和 desc desc 中的前 descsz 字节包含注释信息的描述。ABI 对此没有作出约束。如果需要,可以用补零来确保 4 字节边界对齐。补零的字节数不计算在 descsz 中。
- type 此 word 给出描述符的解释。每个发起者都要负责对自己的类型进行控制;同一类型值可以包含多种不同解释。因此程序必须能够识别名称和类型,才能理解描述符。类型取值必须非负。ABI 并不定义描述符的含义。
使用命令 readelf -l hello.o & readelf -l hello 查看对应的程序头。hello.o 是重定位类型的文件不存在程序头。hello 下面显示其中不少节区和段的映射关系。
9.2 程序加载
进程除非在执行过程中引用到相应的逻辑页面,否则不会请求真正的物理页面。进程通常会包含很多未引用的页面,因此,延迟物理读操作通常会避免这类费力不讨好的事情发生,从而提高系统性能。要想实际获得这种效率,可执行文件和共享目标文件必须具有这样的段:其文件偏移和虚拟地址对页面大小取模后余数相同。
例如,可执行文件布局如下图(图中左侧为文件偏移值,右侧为虚拟地址):
该可执行文件中程序头部段如下所示:
成员 | 正文段 | 数据段 |
---|
p_type | PT_LOAD | PT_LOAD | p_offset | 0X100 | 0x2bf00 | p_vaddr | 0x8048100 | 0x8074f00 | p_paddr | 未指定 | 未指定 | p_filesz | 0x2BE00 | 0x4e00 | p_memsz | 0x2BE00 | 0x5e24 | p_flags | PF_R + PF_X | PF_R + PF_W + PF_X | p_align | 0x1000 | 0x1000 |
在这个例子中,至多四个文件页面包含非纯粹的正文或者数据。
- 第一个页面中包含 ELF 头部、程序头部表、以及其它信息
- 最后一个页面包含数据开始部分的一个副本
- 第一数据页面包含正文段的末尾部分
- 最后一个数据页面可能包含与运行进程无关的文件信息
不过系统对这些页面一般会做两次映射,以保证每个段的内存访问许可是相同的。数据段的末尾需要对未初始化数据进行特殊处理,系统应该将这些初始化为 0。
可执行文件与共享目标文件之间的段加载之间有一点不同。可执行文件的段通常包含绝对代码,为了能够让进程正确执行,所使用的段必须是构造可执行文件时所使用的虚拟地址。因此系统会使用 p_vaddr 作为虚拟地址。
另外,共享目标文件的段通常包含与位置无关的代码。这使得段的虚拟地址在不同的进程中不同,但不影响执行行为。尽管系统为每个进程选择独立的虚拟地址,仍能维持段的相对位置。因为位置独立的代码在段与段之间使用相对寻址,内存虚地址之间的差异必须与文件中虚拟地址之间的差异相匹配。
下表给出共享目标文件的针对不同进程的一种虚拟地址指定方案,说明了这种重定位问题。
源 | 正文 | 数据 | 基地址 |
---|
文件 | 0x200 | 0x2a400 | 0x0 | 进程 1 | 0x80000200 | 0x8002a400 | 0x80000000 | 进程 2 | 0x80081200 | 0x800ab400 | 0x80081000 | 进程 3 | 0x900c0200 | 0x900ea400 | 0x900c0000 | 进程 4 | 0x900c6200 | 0x900f0400 | 0x900c6000 |
9.3 动态链接
9.3.1 程序解释器
可 执 行 文 件 可 以 包 含 PT_INTERP 程 序 头 部 元 素 。 在 exec() 期 间 , 系 统 从 PT_INTERP 段中检索路径名,并从解释器文件的段创建初始的进程映像。也就是说,系统并不使用原来可执行文件的段映像,而是为解释器构造一个内存映像。接下来是解释器从系统接收控制,为应用程序提供执行环境。解释器可以有两种方式接受控制。
- 接受一个文件描述符,读取可执行文件并将其映射到内存中。
- 根据可执行文件的格式,系统可能把可执行文件加载到内存中,而不是为解释器提供一个已经打开的文件描述符。
解释器可以是一个可执行文件,也可以是一个共享目标文件。共享目标文件被加载到内存中时,其地址可能在各个进程中呈现不同的取值。系统在 mmap 以及相关服务所使用的动态段区域创建共享目标文件的段。因此,共享目标解释器通常不会与原来的可执行文件的原始段地址发生冲突。
可执行文件被加载到内存中固定地址,系统使用来自其程序头部表的虚拟地址创建各个段。因此,可执行文件解释器的虚拟地址可能会与原来的可执行文件的虚拟地址发生冲突。解释器要负责解决这种冲突。
9.3.2 动态加载程序
在构造使用动态链接技术的可执行文件时,连接编辑器向可执行文件中添加一个类型为 PT_INTERP 的程序头部元素,告诉系统要把动态链接器激活,作为程序解释器。系统所提供的动态链接器的位置是和处理器相关。
Exec() 和动态链接器合作,为程序创建进程映像,其中包括以下动作:
- 将可执行文件的内存段添加到进程映像中;
- 把共享目标内存段添加到进程映像中;
- 为可执行文件和它的共享目标执行重定位操作;
- 关闭用来读入可执行文件的文件描述符,如果动态链接程序收到过这样的文件描述符的话;
- 将控制转交给程序,使得程序好像从 exec 直接得到控制。
链接编辑器也会构造很多数据来协助动态链接器处理可执行文件和共享目标文件。这些数据包含在可加载段中,在执行过程中可用。如:
- 类型为 SHT_DYNAMIC 的 .dynamic 节区包含很多数据。位于节区头部的结构保存了其他动态链接信息的地址。
- 类型为 SHT_HASH 的 .hash 节区包含符号哈希表。
- 类型为 SHT_PROGBITS 的 .got 和 .plt 节区包含两个不同的表:全局偏移表和过程链接表。
因为任何符合 ABI 规范的程序都要从共享目标库中导入基本的系统服务,动态链接器会参与每个符合 ABI 规范的程序的执行。
9.3.3 动态节区
如果一个目标文件参与动态链接,它的程序头部表将包含类型为 PT_DYNAMIC 的元素。此“段”包含.dynamic 节区。该节区采用一个特殊符号_DYNAMIC 来标记,其中包含如下结构的数组。
include/uapi/linux/elf.h
typedef struct dynamic{
Elf32_Sword d_tag;
union{
Elf32_Sword d_val;
Elf32_Addr d_ptr;
} d_un;
} Elf32_Dyn;
typedef struct {
Elf64_Sxword d_tag;
union {
Elf64_Xword d_val;
Elf64_Addr d_ptr;
} d_un;
} Elf64_Dyn;
include/linux/elf.h
#if ELF_CLASS == ELFCLASS32
extern Elf32_Dyn _DYNAMIC [];
......
#else
extern Elf64_Dyn _DYNAMIC [];
......
#endif
对每个这种类型的对象,d_tag 控制 d_un 的解释含义:
- d_val 表示一个整数值,可以有多种解释。
- d_ptr 代表程序的虚拟地址。如前所述,文件的虚拟地址可能与执行过程中的内存虚地址不匹配。在解释包含于动态结构中的地址时,动态链接程序基于原来文件值和内存基地址计算实际地址。为了保持一致性,文件中不包含用来“纠正”动态结构中重定位项地址的重定位项目。
下面的表格总结了可执行文件和共享目标文件对标志的要求。如果标志被标记为“必需”,那么符合 ABI 规范的文件的动态链接数组必须包含一个该类型表项。“可选”意味着该标志可以出现,但不是必需的。
注:
- 没有出现在此表中的标记值是保留的。
- 除了数组末尾的 DT_NULL 元素以及 DT_NEEDED 元素的相对顺序约束以外,其他项目可以以任意顺序出现。
9.3.4 共享目标的依赖关系
在链接编辑器处理某个归档库时,它会从库中提取成员并将它们复制到输出目标文件中。这些静态链接的服务都是在执行中可用的,不需要动态链接器的参与。共享目标所提供的服务则必须由动态链接器附加到进程映像中。因此可执行文件和共享目标文件描述了它们的特定的依赖关系。
在动态链接器为某个目标文件创建内存段时,依赖关系(记录于动态结构的 DT_NEEDED 表项中)能够提供需要哪些目标来提供程序服务的信息。通过不断地将被引用的共享目标与他们的依赖之间建立连接,动态链接器构造出完整的进程映像。在解析符号引用时,动态链接程序使用宽度优先算法检查符号表。
就是说首先检查可执行程序自身的符号表,然后检查 DT_NEEDED 条目(按顺序)的符号表,接着在第二级 DT_NEEDED 条目上搜索。共享目标文件必须对进程而言可读,不需要其他权限。
即使某个共享目标在依赖表中出现多次,动态链接器也仅会对其连接一次。
存在于依赖表中的名称或者是 DT_SONAME 字符串的副本,或者是用来构造目标文件的共享目标的路径名称。例如,如果连接编辑器在构造某个可执行文件时使用的一 个共享目标中包含 lib1 的 DT_SONAME 项,并且使用了路径名为 /usr/llib/lib2 的共享目标库,那么可执行文件将在其依赖表中包含 lib1 和 /usr/lib/lib2。如果共享目标的名称中包含一个或者多个斜线(/),那么动态链接器将使用该字符串作为路径名称。如果名称中没有斜线,则对共享目标的路径搜索按如下顺序进行:
- 动态数组标记 DT_RPATH 中可能包含若干字符串,这些字符串用“:”分隔,用来通知动态链接器从哪里开始搜索。默认情况下最后搜索当前目录。
- 进程环境中可能包含一个名为 LD_LIBRARY_PATH 的变量,其中也包含若干用“:”分隔的路径名。变量可以以“;”结尾。所有 LD_LIBRARY_PATH 都在 DT_RPATH 之后被搜索。尽管某些程序对分号前后的列表的处理有所不同,动态链接程序并不这样,它能够接受分号,语义如上。
- 最后,如果上面两组目录搜索都失败,未能找到所需要的库,则对/usr/lib 进行搜索。
注意:出于安全性考虑,动态链接器会针对 SUID 或者 SGID 的程序忽略环境搜索规范(如 LD_LIBRARY_PATH)。不过仍然会搜索 DT_RPATH 和/usr/lib 路径。
9.4 全局偏移表(GOT)
位置独立的代码一般不能包含绝对的虚拟地址。全局偏移表在私有数据中包含绝对地址,从而使得地址可用,并且不会影响位置独立性和程序代码的可共享性。程序使用位置独立的寻址引用其全局偏移表,并取得绝对值,从而把位置独立的引用重定向到绝对位置。
全局偏移表中最初包含其重定位项中要求的信息。在系统为可加载目标创建内存段以后,动态链接器要处理重定位项,其中有一些重定位项的类型是 R_386_GLOB_DAT,是对全局偏移表的引用。动态链接器确定相关的符号取值,计算其绝对地址,并将相应的内存表格项目设置为正确的数值。尽管在链接编辑器构造一个目标文件时还无法知道绝对地址,动态链接器清楚所有内存段的地址,因而能够计算其中所包含的符号的绝对地址。
如果程序需要直接访问某个符号的绝对地址,那么该符号就会具有一个全局偏移表项。由于可执行文件和共享目标具有独立的全局偏移表,一个符号的地址可能出现在多个表中。动态链接器在将控制交给进程映像中任何代码之前,要处理所有的全局偏移表重定位,因而确保了执行过程中绝对地址信息可用。
表项 0 是保留的,用来存放动态结构的地址,可以用符号 _DYNAMIC 引用之。这样,类似动态链接器这种程序能够在尚未处理其重定位项的时候先找到自己的动态结构。对于动态链接器而言这点很重要,因为它必须能够在不依赖其他程序来对其内存映像进行重定位的前提下,初始化自己。在 32 位 Intel 体系结构下,全局偏移表中的表项 1 和 2 也是保留的。
系统可能在不同的程序中为相同的共享目标选择不同的内存段地址,甚至为统一程序的两次执行选择不同的库地址。尽管如此,一旦进程映像被建立起来,内存段不会改变其地址。只有进程存在,其内存段都位于固定的虚地址。
全局偏移表的格式和解释都是和处理器相关的。对于 32 位 Intel 体系结构而言,符号 _GLOBAL_OFFSET_TABLE_ 可以用来访问该表。
extern Elf32_Addr _GLOBAL_OFFSET_TABLE[];
符号 _GLOBAL_OFFSET_TABLE_ 可能存在于 .got 节区的中间,允许使用负的/非负的下标来访问地址数组。
9.5 过程链接表(PLT)
全局偏移表(GOT)用来将位置独立的地址计算重定向到绝对位置,与此相似,过程链接表(PLT)能够把位置独立的函数调用重定向到绝对位置。链接编辑器能解析从一个可执行文件/共享目标到另一个可执行文件/共享目标控制转移(例如函数调用)。因此,链接编辑器让程序把控制转移给过程链接表中的表项。
动态链接器能够确定目标处的绝对地址,并据此修改全局偏移表的内存映像。动态链接器因此能够对表项进行重定位,并且不会影响程序代码的位置独立性和可共享性。可执行文件和共享目标文件拥有各自独立的过程链接表。
例如,绝对过程链接表如下:
位置独立的过程链接表如下:
如图所示,过程链接表命令针对绝对代码和位置独立的代码使用不同的操作数寻址模式。尽管如此,它们对动态链接器的接口还是相同的。动态链接器和程序“合作”,通过过程链接表和全局偏移表解析符号引用:
- 在第一次创建程序的内存映像时,动态链接器为全局偏移表的第二和第三项设置特殊值。
- 如果过程链接表是位置独立的,全局偏移表必须位于 %bx 中,进程映像中的每个共享目标文件都有自己的过程链接表,控制向过程链接表项的传递仅发生在同一个目标文件中。因此,调用函数用负责在调用过程链接表项之前设置全局偏移表的基址寄存器。
- 出于说明的目的,假定程序调用了 name1,name1 将控制传输给标号 .PLT1。
- 第一条指令跳转到 name1 的全局偏移表项的地址。最初,全局偏移表中包含后面的 pushl 指令的地址,而不是 name1 的真实地址。
- 接下来,程序将重定位偏移(offset)压栈。重定位偏移是一个 32 位非负数,是在重定位表中的字节偏移量。指定的重定位表项的类型为 R_386_JMP_SLOT,其偏移将给出在前面的 jmp 指令中使用的 GOT 表项。重定位项也包含一个符号表索引,借以告诉动态链接器被引用的符号是什么,在这里是 name1。
- 在将重定位偏移压栈后,程序会跳转到 .PLT0,也就是过程链接表的第一项。pushl 指令把第二个全局偏移表项(got_plus_4 或者 4(%ebx))压入堆栈,因而为动态链接器提供了识别信息的机会。程序然后跳转到第三个 GOT 表项内保存的地址(got_plus_8 或者 8(%ebx)),后者将控制传递给动态链接器。
- 当动态链接器得到控制后,它恢复堆栈,查看指定的重定位项,寻找符号的值,将 name1 的“真实”地址存储于全局偏移表项中,并将控制传递给期望的目的地。
- 过程链接表项的后续执行将把控制直接传递给 name1,不会再次调用动态链接器。就是说 .PLT1 处的 jmp 将控制传递给 name1,而不会执行后面的 pushl 指令。
环境变量 LD_BIND_NOW 可以更改动态链接行为。如果其取值非空,动态链接器会在控制传递给程序之前,对过程链接表项进行计算。就是说动态链接器会在进程初始化的过程中处理类型为 R_386_JMP_SLOT 的重定位项。否则,动态链接器会对过程链接表实行懒惰计算,延迟符号解析和重定位,直到某个表项的第一次执行。
懒惰绑定通常会提高整体的应用性能,因为未使用的符号不会引入额外的动态链接开销。尽管如此,有些应用情形会使得懒惰绑定不太合适。首先,对共享目标函数的第一次引用花的时间会超出后续调用,因为动态链接器要截获调用以便解析符号。一些应用不能容忍这种不可预测性。第二,如果发生了错误,动态链接器无法解析某个符号,动态链接器会终止程序。在懒惰绑定下,这类事情可能会发生任意多次。某些应用也可能无法容忍这种不可预测性。通过关闭懒惰绑定,动态链接器会迫使所有错误都发生在进程初始化期间,而不是应用程序接收控制以后。
9.6 哈希表(Hash Table)
下面的例子有助于解释哈希表组织,不过不是规范的一部分。
bucket 数组包含 nbucket 个项目,chain 数组包含 nchain 个项目,下标都是从 0 开始。bucket 和 chain 中都保存符号表索引。Chain 表项和符号表存在对应。符号表项的数目应该和 nchain 相等,所以符号表的索引也可用来选取 chain 表项。哈希函数能够接受符号名并且返回一个可以用来计算 bucket 的索引。
因此,如果哈希函数针对某个名字返回了数值 X,则 bucket[X%nbucket] 给出了一个索引 y,该索引可用于符号表,也可用于 chain 表。如果符号表项不是所需要的,那么 chain[y] 则给出了具有相同哈希值的下一个符号表项。我们可以沿着 chain 链一直搜索,直到所选中的符号表项包含了所需要的符号,或者 chain 项中包含值 STN_UNDEF。
哈希函数
ELF 实现中常用的哈希函数如下,有时候会作一些优化(比如 Linux)。
unsigned long
elf_hash (const unsigned char *name)
{
unsigned long h = 0, g;
while (*name)
{
h = (h << 4) + *name++;
if (g = h & 0xf0000000)
h ^= g >> 24;
h &= -g;
}
return h;
}
9.7 初始化和终止函数
在动态链接器构造了进程映像,并执行了重定位以后,每个共享的目标都获得执行某些初始化代码的机会。这些初始化函数的被调用顺序是不一定的,不过所有共享目标初始化都会在可执行文件得到控制之前发生。
类似地,共享目标也包含终止函数,这些函数在进程完成终止动作序列时,通过 atexit() 机制执行。动态链接器对终止函数的调用顺序是不确定的。
共享目标通过动态结构中的 DT_INIT 和 DT_FINI 条目指定初始化/终止函数。通常这些代码放在.init 和.fini 节区中。
注意:尽管 atexit() 终止处理通常会被执行,在进程消亡时并不能保证被执行。特别地,如果进程调用了 _exit 或者进程因为收到某个它既未捕捉又未忽略的信号而终止时,不会执行终止处理。
10 参考资料
- 滕启明 《ELF 文件格式分析》
- 〔美〕Ryan O’Neill 《Linux 二进制分析》
|