1 概述

这是一个从0开始手写的 ELF 加载器.

项目链接: https://github.com/Jvlegod/uELF

alt text

今天我们来看 ELF Header 的结构.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct {
unsigned char e_ident[16]; // 魔数 + 文件类型
uint16_t e_type; // 文件类型 (ET_EXEC, ET_DYN, ET_REL)
uint16_t e_machine; // 架构类型 (EM_X86_64, EM_RISCV, etc.)
uint32_t e_version; // ELF 版本 (一般是 1)
uint64_t e_entry; // 程序入口地址
uint64_t e_phoff; // Program Header Table 偏移
uint64_t e_shoff; // Section Header Table 偏移
uint32_t e_flags; // 架构相关标志
uint16_t e_ehsize; // ELF Header 大小
uint16_t e_phentsize; // 每个 Program Header 的大小
uint16_t e_phnum; // Program Header 的数量
uint16_t e_shentsize; // 每个 Section Header 的大小
uint16_t e_shnum; // Section Header 的数量
uint16_t e_shstrndx; // 字符串表段索引(节名字符串表)
} uElf64_Ehdr;

里面有 Section 和 Segment 的区别是.

“节” 是静态编译时概念,“段” 是运行时加载时概念.

名称 对应表 用途
Section(节) Section Header Table (.shdr) 链接器 用的(比如 .text.data.bss.symtab
Segment(段) Program Header Table (.phdr) 加载器(kernel loader / ld-linux) 用的(比如 PT_LOAD, PT_DYNAMIC

2 解析 ELF Header

第一步我们需要解析 Elf 的魔数为 0x7F 45 4C 46 表示这是一个正确的 ELF 文件.

1
2
3
4
5
6
7
...
if (memcpy(elf_file->elf_header.e_ident, "\x7f""ELF", 4) != 0) {
uELF_ERROR("Not a valid ELF file");
close(fd);
return -1;
}
...

之后我们可以来看看对于 Linker 和 Exec 文件,他们的 ELF Header 有什么不一样.

主要是 Entry pointProgram Header 不同(从无到有的过程).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
jvle@jvle-ThinkPad-X1-Carbon-Gen-8:~/Desktop/works/temp/ELF$ ./uelf test/main.o
2025-10-15 00:16:00[] INFO (parse.c:46) ELF Header:
2025-10-15 00:16:00[] INFO (parse.c:47) Entry point: 0x0
2025-10-15 00:16:00[] INFO (parse.c:48) Program Header Offset: 0
2025-10-15 00:16:00[] INFO (parse.c:49) Section Header Offset: 536
2025-10-15 00:16:00[] INFO (parse.c:50) Number of Program Headers: 0
2025-10-15 00:16:00[] INFO (parse.c:51) Number of Section Headers: 13
jvle@jvle-ThinkPad-X1-Carbon-Gen-8:~/Desktop/works/temp/ELF$ ./uelf test/main
2025-10-15 00:17:53[] INFO (parse.c:46) ELF Header:
2025-10-15 00:17:53[] INFO (parse.c:47) Entry point: 0x1060
2025-10-15 00:17:53[] INFO (parse.c:48) Program Header Offset: 64
2025-10-15 00:17:53[] INFO (parse.c:49) Section Header Offset: 14032
2025-10-15 00:17:53[] INFO (parse.c:50) Number of Program Headers: 13
2025-10-15 00:17:53[] INFO (parse.c:51) Number of Section Headers: 31

这里的 Entry point 决定了——当 ELF 被加载并开始执行时,CPU 从哪一条指令开始运行.

简单来说.

当你运行一个 ELF 可执行文件(例如 /bin/ls),内核加载 ELF 后,会:

  1. 创建进程;

  2. 根据 ELF 的 Program Header 映射 .text, .data, .bss, 栈等;

  3. 然后让 CPU 跳转到 e_entry 指定的地址执行。

该程序的第一条指令会被放在虚拟地址为 e_entry 处.

3 解析 Section Header Table

我们可以看一下 Section Header Table 的结构.

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; // 节标志
uint64_t sh_addr; // 虚拟地址(仅加载时使用)
uint64_t sh_offset; // 文件中偏移
uint64_t sh_size; // 节大小
uint32_t sh_link; // 关联节索引
uint32_t sh_info; // 额外信息
uint64_t sh_addralign; // 对齐
uint64_t sh_entsize; // 每个表项大小(符号表类)注:中文版中显示全体大小,很奇怪的翻译.
} uElf64_Shdr;
字段名 对应 ELF 成员 含义
Idx (索引号) Section Header 在表中的编号(从 0 开始)
Name sh_name + .shstrtab 解析结果 节名称,例如 .text, .data
Type sh_type 节的类型,如 PROGBITS, SYMTAB, STRTAB
Addr sh_addr 该节加载到内存时的虚拟地址(仅对可执行文件有效,在 .o 里一般为 0)
Off sh_offset 该节在文件中的偏移(以字节为单位)
Size sh_size 该节的总字节大小
EntSz sh_entsize 每个 entry 的大小(表类型节专用,如符号表/重定位表)
Flags sh_flags 节的标志位(属性,如可写、可执行、分配到内存等)
Link sh_link 链接相关索引,含义随类型不同
Info sh_info 附加信息字段,含义随类型不同
Align sh_addralign 对齐要求(内存/文件中对齐到多少字节边界)

关于 sh_addr 这里需要进行解释.

文件类型 文件格式类型 (e_type) 是否已分配虚拟地址 .textsh_addr 含义
目标文件 .o ET_REL 还没分配 0x00000000 链接器后续决定
可执行文件 .out ET_EXEC 已分配,固定虚拟地址 0x401000 程序入口/代码段
共享库 .so ET_DYN 已分配,但可重定位 0x0000000000001000 加载器按需重定位

另外关于两个 size 字段要注意一下区分,有两个大小.

字段 含义
sh_size 整个节(section)的总字节数
sh_entsize 如果节中包含一张表(table),则表中每个“表项”的大小
1
2
3
4
5
6
7
8
# 这里举个例子说明.
# sh_size = entry_count × sh_entsize
# 有时候发现 sh_entsize 为 0 表示这个是一段完整的段,比如 .text 段.
.section (总大小 = sh_size)
├── entry0 (大小 = sh_entsize)
├── entry1 (大小 = sh_entsize)
├── entry2 (大小 = sh_entsize)
└── ...
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
$ readelf -S test/main.o
There are 13 section headers, starting at offset 0x218:

节头:
[号] 名称 类型 地址 偏移量
大小 全体大小 旗标 链接 信息 对齐
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
0000000000000024 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 00000178
0000000000000018 0000000000000018 I 10 1 8
[ 3] .data PROGBITS 0000000000000000 00000064
0000000000000000 0000000000000000 WA 0 0 1
[ 4] .bss NOBITS 0000000000000000 00000064
0000000000000000 0000000000000000 WA 0 0 1
[ 5] .comment PROGBITS 0000000000000000 00000064
000000000000002c 0000000000000001 MS 0 0 1
[ 6] .note.GNU-stack PROGBITS 0000000000000000 00000090
0000000000000000 0000000000000000 0 0 1
[ 7] .note.gnu.pr[...] NOTE 0000000000000000 00000090
0000000000000020 0000000000000000 A 0 0 8
[ 8] .eh_frame PROGBITS 0000000000000000 000000b0
0000000000000038 0000000000000000 A 0 0 8
[ 9] .rela.eh_frame RELA 0000000000000000 00000190
0000000000000018 0000000000000018 I 10 8 8
[10] .symtab SYMTAB 0000000000000000 000000e8
0000000000000078 0000000000000018 11 3 8
[11] .strtab STRTAB 0000000000000000 00000160
0000000000000013 0000000000000000 0 0 1
[12] .shstrtab STRTAB 0000000000000000 000001a8
000000000000006c 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),
D (mbind), l (large), p (processor specific)

$ readelf -S test/main
There are 31 section headers, starting at offset 0x36d0:

节头:
[号] 名称 类型 地址 偏移量
大小 全体大小 旗标 链接 信息 对齐
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000000318 00000318
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.gnu.pr[...] NOTE 0000000000000338 00000338
0000000000000030 0000000000000000 A 0 0 8
[ 3] .note.gnu.bu[...] NOTE 0000000000000368 00000368
0000000000000024 0000000000000000 A 0 0 4
[ 4] .note.ABI-tag NOTE 000000000000038c 0000038c
0000000000000020 0000000000000000 A 0 0 4
[ 5] .gnu.hash GNU_HASH 00000000000003b0 000003b0
0000000000000024 0000000000000000 A 6 0 8
[ 6] .dynsym DYNSYM 00000000000003d8 000003d8
00000000000000a8 0000000000000018 A 7 1 8
[ 7] .dynstr STRTAB 0000000000000480 00000480
000000000000008d 0000000000000000 A 0 0 1
[ 8] .gnu.version VERSYM 000000000000050e 0000050e
000000000000000e 0000000000000002 A 6 0 2
[ 9] .gnu.version_r VERNEED 0000000000000520 00000520
0000000000000030 0000000000000000 A 7 1 8
[10] .rela.dyn RELA 0000000000000550 00000550
00000000000000c0 0000000000000018 A 6 0 8
[11] .rela.plt RELA 0000000000000610 00000610
0000000000000018 0000000000000018 AI 6 24 8
[12] .init PROGBITS 0000000000001000 00001000
000000000000001b 0000000000000000 AX 0 0 4
[13] .plt PROGBITS 0000000000001020 00001020
0000000000000020 0000000000000010 AX 0 0 16
[14] .plt.got PROGBITS 0000000000001040 00001040
0000000000000010 0000000000000010 AX 0 0 16
[15] .plt.sec PROGBITS 0000000000001050 00001050
0000000000000010 0000000000000010 AX 0 0 16
[16] .text PROGBITS 0000000000001060 00001060
0000000000000127 0000000000000000 AX 0 0 16
[17] .fini PROGBITS 0000000000001188 00001188
000000000000000d 0000000000000000 AX 0 0 4
[18] .rodata PROGBITS 0000000000002000 00002000
0000000000000011 0000000000000000 A 0 0 4
[19] .eh_frame_hdr PROGBITS 0000000000002014 00002014
000000000000003c 0000000000000000 A 0 0 4
[20] .eh_frame PROGBITS 0000000000002050 00002050
00000000000000cc 0000000000000000 A 0 0 8
[21] .init_array INIT_ARRAY 0000000000003db8 00002db8
0000000000000008 0000000000000008 WA 0 0 8
[22] .fini_array FINI_ARRAY 0000000000003dc0 00002dc0
0000000000000008 0000000000000008 WA 0 0 8
[23] .dynamic DYNAMIC 0000000000003dc8 00002dc8
00000000000001f0 0000000000000010 WA 7 0 8
[24] .got PROGBITS 0000000000003fb8 00002fb8
0000000000000048 0000000000000008 WA 0 0 8
[25] .data PROGBITS 0000000000004000 00003000
0000000000000010 0000000000000000 WA 0 0 8
[26] .bss NOBITS 0000000000004010 00003010
0000000000000008 0000000000000000 WA 0 0 1
[27] .comment PROGBITS 0000000000000000 00003010
000000000000002b 0000000000000001 MS 0 0 1
[28] .symtab SYMTAB 0000000000000000 00003040
0000000000000390 0000000000000018 29 19 8
[29] .strtab STRTAB 0000000000000000 000033d0
00000000000001e6 0000000000000000 0 0 1
[30] .shstrtab STRTAB 0000000000000000 000035b6
000000000000011a 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),
D (mbind), l (large), p (processor specific)

下面我们来尝试解析 Section Header Table.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
// e_shnum 这里表示 section 的数目,在上面的指令中可以看到为 13.
elf_file->section_headers = malloc(ehdr->e_shnum * sizeof(uElf64_Shdr));
if (!elf_file->section_headers) {
uELF_ERROR("Failed to allocate memory for section headers");
return -1;
}
...
// 这里将 Section Header Table 的字段全部解析.
lseek(elf_file->fd, ehdr->e_shoff, SEEK_SET);
if (read(elf_file->fd, elf_file->section_headers, ehdr->e_shnum * sizeof(uElf64_Shdr)) !=
ehdr->e_shnum * sizeof(uElf64_Shdr)) {
uELF_ERROR("Failed to read section headers");
free(elf_file->section_headers);
return -1;
}
...

这里当然还有一个特殊的段叫 节名字符串表.

于是我们在结构体中新加入字段.

1
2
3
4
5
typedef struct {
...
uElf64_Shdr *shstrtab_section; // 节名字符串表节
char *shstrtab; // 节名字符串表内容
} uElf64_File;

这里单独对该字段进行抽取.

1
2
3
4
5
6
7
elf_file->shstrtab_section = &elf_file->section_headers[ehdr->e_shstrndx];
elf_file->shstrtab = malloc(elf_file->shstrtab_section->sh_size);
if (!elf_file->shstrtab) {
uELF_ERROR("Failed to allocate memory for section header string table");
free(elf_file->section_headers);
return -1;
}

结果进行查看.

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
jvle@jvle-ThinkPad-X1-Carbon-Gen-8:~/Desktop/works/temp/ELF$ ./uelf test/main.o
2025-10-15 17:17:49[] INFO (uELF.c:47) ELF Header:
2025-10-15 17:17:49[] INFO (uELF.c:48) Entry point: 0x0
2025-10-15 17:17:49[] INFO (uELF.c:49) Program Header Offset: 0
2025-10-15 17:17:49[] INFO (uELF.c:50) Section Header Offset: 536
2025-10-15 17:17:49[] INFO (uELF.c:51) Number of Program Headers: 0
2025-10-15 17:17:49[] INFO (uELF.c:52) Number of Section Headers: 13
2025-10-15 17:17:49[] INFO (uELF.c:60) Section Headers (all 13):
2025-10-15 17:17:49[] INFO (uELF.c:61) [Idx] Name Type Addr Off Size EntSz Flags Link Info Align
2025-10-15 17:17:49[] INFO (uELF.c:83) [ 0] NULL 00000000 000000 000000 000000 0 0 0 0
2025-10-15 17:17:49[] INFO (uELF.c:83) [ 1] .text PROGBITS 00000000 000040 000024 000000 6 0 0 1
2025-10-15 17:17:49[] INFO (uELF.c:83) [ 2] .rela.text RELA 00000000 000178 000018 000018 40 10 1 8
2025-10-15 17:17:49[] INFO (uELF.c:83) [ 3] .data PROGBITS 00000000 000064 000000 000000 3 0 0 1
2025-10-15 17:17:49[] INFO (uELF.c:83) [ 4] .bss NOBITS 00000000 000064 000000 000000 3 0 0 1
2025-10-15 17:17:49[] INFO (uELF.c:83) [ 5] .comment PROGBITS 00000000 000064 00002c 000001 30 0 0 1
2025-10-15 17:17:49[] INFO (uELF.c:83) [ 6] .note.GNU-stack PROGBITS 00000000 000090 000000 000000 0 0 0 1
2025-10-15 17:17:49[] INFO (uELF.c:83) [ 7] .note.gnu.property NOTE 00000000 000090 000020 000000 2 0 0 8
2025-10-15 17:17:49[] INFO (uELF.c:83) [ 8] .eh_frame PROGBITS 00000000 0000b0 000038 000000 2 0 0 8
2025-10-15 17:17:49[] INFO (uELF.c:83) [ 9] .rela.eh_frame RELA 00000000 000190 000018 000018 40 10 8 8
2025-10-15 17:17:49[] INFO (uELF.c:83) [10] .symtab SYMTAB 00000000 0000e8 000078 000018 0 11 3 8
2025-10-15 17:17:49[] INFO (uELF.c:83) [11] .strtab STRTAB 00000000 000160 000013 000000 0 0 0 1
2025-10-15 17:17:49[] INFO (uELF.c:83) [12] .shstrtab STRTAB 00000000 0001a8 00006c 000000 0 0 0 1
jvle@jvle-ThinkPad-X1-Carbon-Gen-8:~/Desktop/works/temp/ELF$ ./uelf test/main
2025-10-15 17:18:49[] INFO (uELF.c:47) ELF Header:
2025-10-15 17:18:49[] INFO (uELF.c:48) Entry point: 0x1060
2025-10-15 17:18:49[] INFO (uELF.c:49) Program Header Offset: 64
2025-10-15 17:18:49[] INFO (uELF.c:50) Section Header Offset: 14032
2025-10-15 17:18:49[] INFO (uELF.c:51) Number of Program Headers: 13
2025-10-15 17:18:49[] INFO (uELF.c:52) Number of Section Headers: 31
2025-10-15 17:18:49[] INFO (uELF.c:60) Section Headers (all 31):
2025-10-15 17:18:49[] INFO (uELF.c:61) [Idx] Name Type Addr Off Size EntSz Flags Link Info Align
2025-10-15 17:18:49[] INFO (uELF.c:83) [ 0] NULL 00000000 000000 000000 000000 0 0 0 0
2025-10-15 17:18:49[] INFO (uELF.c:83) [ 1] .interp PROGBITS 00000318 000318 00001c 000000 2 0 0 1
2025-10-15 17:18:49[] INFO (uELF.c:83) [ 2] .note.gnu.property NOTE 00000338 000338 000030 000000 2 0 0 8
2025-10-15 17:18:49[] INFO (uELF.c:83) [ 3] .note.gnu.build-id NOTE 00000368 000368 000024 000000 2 0 0 4
2025-10-15 17:18:49[] INFO (uELF.c:83) [ 4] .note.ABI-tag NOTE 0000038c 00038c 000020 000000 2 0 0 4
2025-10-15 17:18:49[] INFO (uELF.c:83) [ 5] .gnu.hash OTHER 000003b0 0003b0 000024 000000 2 6 0 8
2025-10-15 17:18:49[] INFO (uELF.c:83) [ 6] .dynsym DYNSYM 000003d8 0003d8 0000a8 000018 2 7 1 8
2025-10-15 17:18:49[] INFO (uELF.c:83) [ 7] .dynstr STRTAB 00000480 000480 00008d 000000 2 0 0 1
2025-10-15 17:18:49[] INFO (uELF.c:83) [ 8] .gnu.version OTHER 0000050e 00050e 00000e 000002 2 6 0 2
2025-10-15 17:18:49[] INFO (uELF.c:83) [ 9] .gnu.version_r OTHER 00000520 000520 000030 000000 2 7 1 8
2025-10-15 17:18:49[] INFO (uELF.c:83) [10] .rela.dyn RELA 00000550 000550 0000c0 000018 2 6 0 8
2025-10-15 17:18:49[] INFO (uELF.c:83) [11] .rela.plt RELA 00000610 000610 000018 000018 42 6 24 8
2025-10-15 17:18:49[] INFO (uELF.c:83) [12] .init PROGBITS 00001000 001000 00001b 000000 6 0 0 4
2025-10-15 17:18:49[] INFO (uELF.c:83) [13] .plt PROGBITS 00001020 001020 000020 000010 6 0 0 16
2025-10-15 17:18:49[] INFO (uELF.c:83) [14] .plt.got PROGBITS 00001040 001040 000010 000010 6 0 0 16
2025-10-15 17:18:49[] INFO (uELF.c:83) [15] .plt.sec PROGBITS 00001050 001050 000010 000010 6 0 0 16
2025-10-15 17:18:49[] INFO (uELF.c:83) [16] .text PROGBITS 00001060 001060 000127 000000 6 0 0 16
2025-10-15 17:18:49[] INFO (uELF.c:83) [17] .fini PROGBITS 00001188 001188 00000d 000000 6 0 0 4
2025-10-15 17:18:49[] INFO (uELF.c:83) [18] .rodata PROGBITS 00002000 002000 000011 000000 2 0 0 4
2025-10-15 17:18:49[] INFO (uELF.c:83) [19] .eh_frame_hdr PROGBITS 00002014 002014 00003c 000000 2 0 0 4
2025-10-15 17:18:49[] INFO (uELF.c:83) [20] .eh_frame PROGBITS 00002050 002050 0000cc 000000 2 0 0 8
2025-10-15 17:18:49[] INFO (uELF.c:83) [21] .init_array OTHER 00003db8 002db8 000008 000008 3 0 0 8
2025-10-15 17:18:49[] INFO (uELF.c:83) [22] .fini_array OTHER 00003dc0 002dc0 000008 000008 3 0 0 8
2025-10-15 17:18:49[] INFO (uELF.c:83) [23] .dynamic DYNAMIC 00003dc8 002dc8 0001f0 000010 3 7 0 8
2025-10-15 17:18:49[] INFO (uELF.c:83) [24] .got PROGBITS 00003fb8 002fb8 000048 000008 3 0 0 8
2025-10-15 17:18:49[] INFO (uELF.c:83) [25] .data PROGBITS 00004000 003000 000010 000000 3 0 0 8
2025-10-15 17:18:49[] INFO (uELF.c:83) [26] .bss NOBITS 00004010 003010 000008 000000 3 0 0 1
2025-10-15 17:18:49[] INFO (uELF.c:83) [27] .comment PROGBITS 00000000 003010 00002b 000001 30 0 0 1
2025-10-15 17:18:49[] INFO (uELF.c:83) [28] .symtab SYMTAB 00000000 003040 000390 000018 0 29 19 8
2025-10-15 17:18:49[] INFO (uELF.c:83) [29] .strtab STRTAB 00000000 0033d0 0001e6 000000 0 0 0 1
2025-10-15 17:18:49[] INFO (uELF.c:83) [30] .shstrtab STRTAB 00000000 0035b6 00011a 000000 0 0 0 1

4 解析 Symbol Table

首先我们要知道符号表的结构.

1
2
3
4
5
6
7
8
typedef struct {
uint32_t st_name; // 符号名在字符串表中的偏移
uint8_t st_info; // 类型 + 绑定信息
uint8_t st_other; // 可见性
uint16_t st_shndx; // 该符号所在节索引它出现在 Elf{32,64}_Sym 里,用来说明“这个符号是定义在目标文件/可执行文件的哪一个节里”. 链接器和加载器据此判断符号是否本地已定义、绝对、公共(common)还是未定义,并据此决定重定位与符号解析策略
uint64_t st_value; // 符号值(地址/偏移)
uint64_t st_size; // 符号大小(例如函数长度)
} uElf64_Sym;

当我们在解析 Section的时候,发现他的 sh_type 是 .symtab 静态链接符号 或 .dynsym 动态链接符号 标志,则说明这可能是一个符号表.

更多的 sh_type 信息如下.

sh_type 常量名 意义
SHT_NULL 无效节
SHT_PROGBITS 程序数据(机器码、常量等)
SHT_NOBITS 不占空间(如 .bss
SHT_SYMTAB 符号表(静态链接符号)
SHT_STRTAB 字符串表
SHT_DYNSYM 动态符号表(动态链接符号)
SHT_RELA / SHT_REL 重定位表
SHT_NOTE 注释信息(比如 GNU Stack)
1
2
3
4
5
6
7
8
9
10
11
12
typedef struct {
...
uElf64_Shdr *strtab_section; // 字符串表节
char *strtab; // 字符串表内容
uElf64_Shdr *dynstr_section; // 动态字符串表节
char *dynstr; // 动态字符串表内容

uElf64_Shdr *symtab_section; // 符号表节
char *symtab; // 符号表内容
uElf64_Shdr *dynsym_section; // 动态符号表节
char *dynsym; // 动态符号表内容
} uElf64_File;

我们可能需要获取各个符号的名称,那么我们可以通过 symtab_sectionsh_link 字段来作为 .strtab 的索引. 另外该节的 sh_info 字段表示本地符号的数量(或者说是第一个全局符号的索引).

同样 dynstr 对应着动态链接的各个符号和信息.

主要的解析部分如下.

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
...
//parse symbol table if exists
for (int i = 0; i < elf_file->elf_header.e_shnum; i++) {
if (elf_file->section_headers[i].sh_type == UELF_SHT_SYMTAB) {
elf_file->symtab_section = &elf_file->section_headers[i];
elf_file->symtab = malloc(elf_file->section_headers[i].sh_size);
if (!elf_file->symtab) {
uELF_ERROR("Failed to allocate memory for symbol table");
return -1;
}

lseek(fd, elf_file->symtab_section->sh_offset, SEEK_SET);
if (read(fd, elf_file->symtab, elf_file->section_headers[i].sh_size) !=
(ssize_t)elf_file->section_headers[i].sh_size) {
uELF_ERROR("Failed to read symbol table");
free(elf_file->symtab);
return -1;
}
} else if (elf_file->section_headers[i].sh_type == UELF_SHT_DYNSYM) {
elf_file->dynsym_section = &elf_file->section_headers[i];
elf_file->dynsym = malloc(elf_file->section_headers[i].sh_size);
if (!elf_file->dynsym) {
uELF_ERROR("Failed to allocate memory for dynamic symbol table");
return -1;
}

lseek(fd, elf_file->dynsym_section->sh_offset, SEEK_SET);
if (read(fd, elf_file->dynsym, elf_file->section_headers[i].sh_size) !=
(ssize_t)elf_file->section_headers[i].sh_size) {
uELF_ERROR("Failed to read dynamic symbol table");
free(elf_file->dynsym);
return -1;
}
}
}

// parse static symbol table if exists
if (elf_file->symtab_section) {
elf_file->strtab_section = &elf_file->section_headers[elf_file->symtab_section->sh_link];
elf_file->strtab = malloc(elf_file->strtab_section->sh_size);
if (!elf_file->strtab) {
uELF_ERROR("Failed to allocate memory for string table");
return -1;
}

if (lseek(fd, elf_file->strtab_section->sh_offset, SEEK_SET) < 0) {
uELF_ERROR("lseek failed when reading .strtab");
return -1;
}

if (read(fd, elf_file->strtab, elf_file->strtab_section->sh_size) !=
(ssize_t)elf_file->strtab_section->sh_size) {
uELF_ERROR("read failed when reading .strtab");
return -1;
}
}

// parse dynamic symbol table if exists
if (elf_file->dynsym_section) {
elf_file->dynstr_section = &elf_file->section_headers[elf_file->dynsym_section->sh_link];
elf_file->dynstr = malloc(elf_file->dynstr_section->sh_size);
if (!elf_file->dynstr) {
uELF_ERROR("Failed to allocate memory for dynamic string table");
return -1;
}

if (lseek(fd, elf_file->dynstr_section->sh_offset, SEEK_SET) < 0) {
uELF_ERROR("lseek failed when reading .dynstr");
return -1;
}

if (read(fd, elf_file->dynstr, elf_file->dynstr_section->sh_size) !=
(ssize_t)elf_file->dynstr_section->sh_size) {
uELF_ERROR("read failed when reading .dynstr");
return -1;
}
}
...

5 解析 Program headers

Program Headers(程序头表) 出现在哪些 ELF 文件类型中,是理解 ELF 加载机制的关键.

文件类型 ELF Header 中的 e_type 是否包含 Program Headers 说明
ET_REL 1 可重定位目标文件.o、内核模块 .ko),仅有 Section Headers;供链接器使用。
ET_EXEC 2 可执行文件a.out、Linux ELF 可执行程序),包含程序头表,供内核加载。
ET_DYN 3 动态共享对象(.so 文件),可被动态加载(dlopen)或作为主程序执行(PIE)。
ET_CORE 4 Core Dump 文件(进程崩溃转储),Program 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
36
typedef enum {
UELF_PT_NULL = 0, // 无效段,占位用
UELF_PT_LOAD = 1, // 可加载段 (.text, .data 等)
UELF_PT_DYNAMIC = 2, // 动态链接信息段 (.dynamic)
UELF_PT_INTERP = 3, // 解释器路径段 (.interp)
UELF_PT_NOTE = 4, // 注释段 (.note)
UELF_PT_SHLIB = 5, // 保留(很少使用)
UELF_PT_PHDR = 6, // 程序头表本身
UELF_PT_TLS = 7, // 线程局部存储段 (.tdata, .tbss)

// 系统/平台特定范围
UELF_PT_LOOS = 0x60000000, // OS 特定范围下限
UELF_PT_GNU_EH_FRAME= 0x6474e550, // GNU 异常处理段 (.eh_frame_hdr)
UELF_PT_GNU_STACK = 0x6474e551, // GNU 栈属性段(标记栈是否可执行)
UELF_PT_GNU_RELRO = 0x6474e552, // GNU 只读段(relro 保护区)
UELF_PT_GNU_PROPERTY= 0x6474e553, // GNU 属性段
UELF_PT_LOSUNW = 0x6ffffffa, // Sun/UNIX 特定段
UELF_PT_SUNWBSS = 0x6ffffffa, // Sun 特定 BSS 段
UELF_PT_SUNWSTACK = 0x6ffffffb, // Sun 栈段

// 处理器特定范围
UELF_PT_LOPROC = 0x70000000, // 处理器特定范围下限
UELF_PT_HIPROC = 0x7fffffff // 处理器特定范围上限
} uELF_ProgramType;


typedef struct {
uint32_t p_type; // 段类型 (Segment type)
uint32_t p_flags; // 段标志 (Segment flags)
uint64_t p_offset; // 文件中的偏移量
uint64_t p_vaddr; // 虚拟地址 (加载到内存中的地址)
uint64_t p_paddr; // 物理地址 (通常在系统中不用)
uint64_t p_filesz; // 段在文件中的大小
uint64_t p_memsz; // 段在内存中的大小
uint64_t p_align; // 段的对齐约束
} uElf64_Phdr;

这里可以看看我们解析出来的和 readelf -l 解析的区别.

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
2025-10-15 20:04:11[] INFO      (uELF.c:328) Program Headers (all 13):
2025-10-15 20:04:11[] INFO (uELF.c:329) [Idx] Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
2025-10-15 20:04:11[] INFO (uELF.c:346) [ 0] PHDR 000040 00000040 00000040 0002d8 0002d8 4 8
2025-10-15 20:04:11[] INFO (uELF.c:346) [ 1] INTERP 000318 00000318 00000318 00001c 00001c 4 1
2025-10-15 20:04:11[] INFO (uELF.c:346) [ 2] LOAD 000000 00000000 00000000 000628 000628 4 4096
2025-10-15 20:04:11[] INFO (uELF.c:346) [ 3] LOAD 001000 00001000 00001000 000195 000195 5 4096
2025-10-15 20:04:11[] INFO (uELF.c:346) [ 4] LOAD 002000 00002000 00002000 00011c 00011c 4 4096
2025-10-15 20:04:11[] INFO (uELF.c:346) [ 5] LOAD 002db8 00003db8 00003db8 000258 000260 6 4096
2025-10-15 20:04:11[] INFO (uELF.c:346) [ 6] DYNAMIC 002dc8 00003dc8 00003dc8 0001f0 0001f0 6 8
2025-10-15 20:04:11[] INFO (uELF.c:346) [ 7] NOTE 000338 00000338 00000338 000030 000030 4 8
2025-10-15 20:04:11[] INFO (uELF.c:346) [ 8] NOTE 000368 00000368 00000368 000044 000044 4 4
2025-10-15 20:04:11[] INFO (uELF.c:346) [ 9] OTHER 000338 00000338 00000338 000030 000030 4 8
2025-10-15 20:04:11[] INFO (uELF.c:346) [10] OTHER 002014 00002014 00002014 00003c 00003c 4 4
2025-10-15 20:04:11[] INFO (uELF.c:346) [11] OTHER 000000 00000000 00000000 000000 000000 6 16
2025-10-15 20:04:11[] INFO (uELF.c:346) [12] OTHER 002db8 00003db8 00003db8 000248 000248 4 1

$ readelf -l test/main

Elf 文件类型为 DYN (Position-Independent Executable file)
Entry point 0x1060
There are 13 program headers, starting at offset 64

程序头:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040
0x00000000000002d8 0x00000000000002d8 R 0x8
INTERP 0x0000000000000318 0x0000000000000318 0x0000000000000318
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000628 0x0000000000000628 R 0x1000
LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000
0x0000000000000195 0x0000000000000195 R E 0x1000
LOAD 0x0000000000002000 0x0000000000002000 0x0000000000002000
0x000000000000011c 0x000000000000011c R 0x1000
LOAD 0x0000000000002db8 0x0000000000003db8 0x0000000000003db8
0x0000000000000258 0x0000000000000260 RW 0x1000
DYNAMIC 0x0000000000002dc8 0x0000000000003dc8 0x0000000000003dc8
0x00000000000001f0 0x00000000000001f0 RW 0x8
NOTE 0x0000000000000338 0x0000000000000338 0x0000000000000338
0x0000000000000030 0x0000000000000030 R 0x8
NOTE 0x0000000000000368 0x0000000000000368 0x0000000000000368
0x0000000000000044 0x0000000000000044 R 0x4
GNU_PROPERTY 0x0000000000000338 0x0000000000000338 0x0000000000000338
0x0000000000000030 0x0000000000000030 R 0x8
GNU_EH_FRAME 0x0000000000002014 0x0000000000002014 0x0000000000002014
0x000000000000003c 0x000000000000003c R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x0000000000002db8 0x0000000000003db8 0x0000000000003db8
0x0000000000000248 0x0000000000000248 R 0x1

Section to Segment mapping:
段节...
00
01 .interp
02 .interp .note.gnu.property .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt
03 .init .plt .plt.got .plt.sec .text .fini
04 .rodata .eh_frame_hdr .eh_frame
05 .init_array .fini_array .dynamic .got .data .bss
06 .dynamic
07 .note.gnu.property
08 .note.gnu.build-id .note.ABI-tag
09 .note.gnu.property
10 .eh_frame_hdr
11
12 .init_array .fini_array .dynamic .got

6 load segment

uELF64_load_segments 函数中我们重点完成将 segments 加载到内存.

只有 PT_LOADsegments 我们才将它映射到内存当中.

期间我们要确定 ELF 文件的类型,这里从 ELF Header 就能进行分类.

类型 全称 中文含义 特点
PIE Position Independent Executable 位置无关可执行文件 加载地址不固定,可被随机化
non-PIE Normal Executable (Fixed Address) 固定地址可执行文件 加载到固定虚拟地址(例如 0x400000)
项目 non-PIE PIE
ELF 类型 ET_EXEC ET_DYN
编译参数 -no-pie -fPIE -pie
加载地址 固定(如 0x400000) 随机(由内核 ASLR 决定)
main 函数地址 不变 每次都变
可被 ASLR 随机化
本质 固定地址程序 类似共享库的可执行文件

具体过程如下.

  1. 获取系统页大小(sysconf)

  2. 统计所有 PT_LOAD 段(可加载段)

  3. 分配段加载追踪结构(用于后续 munmap)

  4. 计算 PIE 情况下需要的地址空间范围

  5. 分别加载每个段:

  • 对 PIE:先整体 mmap 一块连续空间,再把数据读进去;

  • 对 no-PIE:用 MAP_FIXED 把每段映射到指定虚拟地址。

  1. 设置内存保护权限

  2. 若加载失败则清理内存

下面的代码是计算 PIE 需要的布局情况,拿到最小和最大的映射地址,以此来计算应该给该程序预留多大的内存.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
for (int i = 0; i < ehdr->e_phnum; i++) {
uElf64_Phdr *ph = &elf_file->program_headers[i];
if (ph->p_type != UELF_PT_LOAD || ph->p_memsz == 0) {
continue;
}

uint64_t aligned_vaddr = uelf_align_down(ph->p_vaddr, (uint64_t)page_size);
uint64_t segment_end = uelf_align_up(ph->p_vaddr + ph->p_memsz, (uint64_t)page_size);
if (aligned_vaddr < min_vaddr) {
min_vaddr = aligned_vaddr;
}
if (segment_end > max_vaddr) {
max_vaddr = segment_end;
}
}
...

下面是 PIE 代码的关键.

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
...
if (is_pie && loadable_count > 0) {
size_t total_size = (size_t)(max_vaddr - min_vaddr);
if (total_size == 0) {
total_size = (size_t)page_size;
}
// 给待加载程序分配一块随机的空间,能够存放整个的数据.
void *reservation = mmap(NULL, total_size,
PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (reservation == MAP_FAILED) {
uELF_ERROR("Failed to reserve address space for PIE: %s", strerror(errno));
free(elf_file->loaded_segments);
elf_file->loaded_segments = NULL;
free(elf_file->loaded_segment_sizes);
elf_file->loaded_segment_sizes = NULL;
return -1;
}
// 这里分配了一个空间,由于 PID 位置是不固定的,
// 那么我就先计算出 min 的预留地址,
// 后面计算其他 segment 时保持想对布局不变就可以了.
base = (uintptr_t)reservation - min_vaddr;
// 整块内存的起始地址
elf_file->loaded_segments[0] = reservation;
// 映射大小
elf_file->loaded_segment_sizes[0] = total_size;
// 当前已加载段数量(目前只预留了 1 块空间)
elf_file->loaded_segment_count = 1;
}
...

之后我们看看将 segments 映射到内存的核心代码.

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
...
for (int i = 0; i < ehdr->e_phnum; i++) {
uElf64_Phdr *ph = &elf_file->program_headers[i];
if (ph->p_type != UELF_PT_LOAD || ph->p_memsz == 0) {
continue;
}

uint64_t aligned_vaddr = uelf_align_down(ph->p_vaddr, (uint64_t)page_size);
uint64_t segment_end = uelf_align_up(ph->p_vaddr + ph->p_memsz, (uint64_t)page_size);
size_t map_size = (size_t)(segment_end - aligned_vaddr);

int prot = uelf_prot_from_flags(ph->p_flags);

// PIE 时候使用
if (is_pie) {
uint8_t *segment_data = (uint8_t *)(base + ph->p_vaddr);
ssize_t read_bytes = pread(elf_file->fd, segment_data, ph->p_filesz, ph->p_offset);
if (read_bytes != (ssize_t)ph->p_filesz) {
uELF_ERROR("Failed to read segment %d contents", i);
goto fail;
}

if (ph->p_memsz > ph->p_filesz) {
size_t bss_size = (size_t)(ph->p_memsz - ph->p_filesz);
memset(segment_data + ph->p_filesz, 0, bss_size);
}

if (mprotect((void *)(base + aligned_vaddr), map_size, prot) < 0) {
uELF_WARN("mprotect failed for segment %d: %s", i, strerror(errno));
}

uELF_INFO("Loaded segment %d at 0x%lx (%zu bytes)",
i, (unsigned long)(base + aligned_vaddr), map_size);
continue;
}

int map_prot = prot | PROT_WRITE;
// 该 segment 映射的目标地址
uintptr_t target_addr = base + aligned_vaddr;
// MAP_FIXED 表示强制映射到 target_addr 地址处
// 因此在这里 mapping_base == target_addr
void *mapping_base = mmap((void *)target_addr, map_size, map_prot,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
if (mapping_base == MAP_FAILED) {
uELF_ERROR("mmap failed for segment %d: %s", i, strerror(errno));
goto fail;
}

// 这里把 ELF 文件的数据补充到映射的地址处
uint8_t *segment_data = (uint8_t *)(base + ph->p_vaddr);
ssize_t read_bytes = pread(elf_file->fd, segment_data, ph->p_filesz, ph->p_offset);
if (read_bytes != (ssize_t)ph->p_filesz) {
uELF_ERROR("Failed to read segment %d contents", i);
munmap(mapping_base, map_size);
goto fail;
}

if (ph->p_memsz > ph->p_filesz) {
size_t bss_size = (size_t)(ph->p_memsz - ph->p_filesz);
memset(segment_data + ph->p_filesz, 0, bss_size);
}

// 前面为了方便操作地址全部加了可写权限
// 这里方便映射真正的权限
if ((prot & PROT_WRITE) == 0) {
if (mprotect(mapping_base, map_size, prot) < 0) {
uELF_WARN("mprotect failed for segment %d: %s", i, strerror(errno));
}
}

elf_file->loaded_segments[segment_index] = mapping_base;
elf_file->loaded_segment_sizes[segment_index] = map_size;
elf_file->loaded_segment_count++;
segment_index++;

uELF_INFO("Loaded segment %d at 0x%lx (%zu bytes)",
i, (unsigned long)(base + aligned_vaddr), map_size);
}
...

7 relocations

重定位是编译器和链接器(或者运行时加载器)将符号引用转换为实际地址的过程.

重定位(顾名思义,重新定位)可以发生在三个时期.

阶段 参与者 重定位类型 说明
链接时重定位 ld(静态链接器) 静态重定位 把所有目标文件的符号解析并生成一个固定地址的可执行文件。
加载时重定位 动态链接器(ld-linux.so 动态重定位 程序运行时,根据加载的共享库地址重新修正引用。
运行时重定位 用户自定义 loader / JIT / dlopen() 手动或延迟重定位 比如你自己写的 ELF 加载器、PIE、或者 dlfcn 动态调用。

重定位的条目结构如下.

1
2
3
4
5
typedef struct {
uint64_t r_offset; // 要修正的位置
uint64_t r_info; // 含有符号索引与重定位类型
int64_t r_addend; // 常数加成(某些架构存在)
} uElf64_Rela;

而类型为如下的 sections 被标记为重定位 section.

类型常量 含义 说明
SHT_REL 不带加数的重定位表 每个条目是 Elf32_Rel / Elf64_Rel
SHT_RELA 带加数(addend)的重定位表 每个条目是 Elf32_Rela / Elf64_Rela

在我们的代码中可能只会看到 Elf64_Rela,这是因为.

“All relocations for the AMD64 architecture use the ELF64_Rela structure. Entries of type Elf64_Rel are not used.”

uELF64_apply_relocations 是实现重定位的主要函数.

其步骤如下.

  1. 检查是否有 section_headers

  2. 遍历所有节区

  • 如果是 SHT_RELA 则读取重定位表

    • 找符号表与字符串表

    • 遍历每个 Elf64_Rela

      • 解析 r_info 得到类型和符号索引

      • 根据类型选择修正公式

      • 写回 target 位置

当我们找到 relocation section 之后,我们先找到 relocs 表存放的位置.

一般 sh_link 表示当前节关联的另一个节的索引号.

对于每个重定位节会通过它的 sh_link 字段指向它所使用的符号表的节的索引(通常是 .dynsym 或 .symtab).

符号表节名 类型 (sh_type) 典型对应的重定位节 用途
.symtab SHT_SYMTAB .rel.text, .rela.text 静态链接时的符号表(目标文件 .o / 可执行文件 .exe
.dynsym SHT_DYNSYM .rel.dyn, .rela.dyn, .rel.plt, .rela.plt 动态链接符号表(共享库 .so / 动态可执行文件)

接下来解读 uELF64_apply_relocations 的核心代码.

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
36
37
38
    uElf64_Rela *relocs = malloc(sh->sh_size);
...
// 将该节的重定位表数据读取出来
if (pread(elf_file->fd, relocs, sh->sh_size, sh->sh_offset) != (ssize_t)sh->sh_size) {
uELF_ERROR("Failed to read relocation section %d", i);
free(relocs);
return -1;
}
// 如果 sh_link 的值超出 ELF 的节区数量(e_shnum)
if (sh->sh_link >= elf_file->elf_header.e_shnum) {
uELF_ERROR("Relocation section %d references invalid symbol table", i);
free(relocs);
return -1;
}
...
// 这里跟重定位表相关联的符号表需要进行确定,是 .dynsym 还是 .strtab.
// 根据不同的情况我们将解析重定位表所需的符号表和字符串表都进行抽取.
if (symtab_section == elf_file->symtab_section && elf_file->symtab) {
symtab_data = elf_file->symtab;
strtab_data = elf_file->strtab;
if (elf_file->strtab_section) {
strtab_size = elf_file->strtab_section->sh_size;
}
} else if (symtab_section == elf_file->dynsym_section && elf_file->dynsym) {
symtab_data = elf_file->dynsym;
strtab_data = elf_file->dynstr;
if (elf_file->dynstr_section) {
strtab_size = elf_file->dynstr_section->sh_size;
}
} else {
uELF_WARN("Skipping relocation section %d: unsupported symbol table index %u",
i, sh->sh_link);
free(relocs);
continue;
}

uELF64_arch_relocate(elf_file, relocs, count, symtab_section, symtab_data,
strtab_data, strtab_size, sym_entsize);

uELF64_arch_relocate 是重定位进行数据纠正的重要一步,因为涉及架构相关,我们进一步处理.

uELF64_x86_64_relocate.

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
static int uELF64_x86_64_relocate(uElf64_File *elf_file, uElf64_Rela *relocs, size_t count,
uElf64_Shdr *symtab_section, char *symtab_data,
char *strtab_data, size_t strtab_size, size_t sym_entsize) {
// 目前我们遍历重定位表,count 是目标重定位表的条目个数.
for (size_t rel_idx = 0; rel_idx < count; rel_idx++) {
uElf64_Rela *rela = &relocs[rel_idx];
uint32_t type = UELF64_R_TYPE(rela->r_info);
uint32_t sym_index = UELF64_R_SYM(rela->r_info);
// 这里的 target 是一个地址,表示重定位项实际在内存需要修正的内存地址
uintptr_t target = elf_file->load_base + rela->r_offset;

uint64_t value = 0;
const uElf64_Sym *sym = NULL;
const char *name = NULL;

if (sym_index != 0 && symtab_data) {
size_t symtab_size = symtab_section->sh_size;
size_t offset = (size_t)sym_index * sym_entsize;
if (offset + sym_entsize <= symtab_size) {
// 这里我们拿到跟重定位条目相关联的符号
sym = (const uElf64_Sym *)(symtab_data + offset);
if (sym && sym->st_name && strtab_data && (size_t)sym->st_name < strtab_size) {
name = strtab_data + sym->st_name;
}
} else {
uELF_ERROR("Relocation references invalid symbol index %u", sym_index);
free(relocs);
return -1;
}
}
...
}
}

下面的代码是重定位实际纠正生效的地方.

类型 名称 含义 计算公式
R_X86_64_NONE 无操作 忽略
R_X86_64_64 绝对重定位 写入符号绝对地址 S + A
R_X86_64_PC32 相对 PC 地址 相对于当前指令地址 S + A - P
R_X86_64_GLOB_DAT 全局变量重定位 修正 GOT 条目 S
R_X86_64_JUMP_SLOT 动态函数跳转表 修正 PLT/GOT S
R_X86_64_RELATIVE 基址相对 自身基址 + 偏移 B + A
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
36
37
38
...
switch (type) {
case UELF_R_X86_64_RELATIVE:
*(uint64_t *)target = elf_file->load_base + rela->r_addend;
break;
case UELF_R_X86_64_GLOB_DAT:
case UELF_R_X86_64_JUMP_SLOT:
if (!sym) {
uELF_ERROR("Relocation requires symbol but none provided (index %u)", sym_index);
free(relocs);
return -1;
}
if (uELF64_resolve_symbol_address(elf_file, sym, name, &value) < 0) {
free(relocs);
return -1;
}
*(uint64_t *)target = value;
break;
case UELF_R_X86_64_64:
if (!sym) {
uELF_ERROR("Relocation requires symbol but none provided (index %u)", sym_index);
free(relocs);
return -1;
}
if (uELF64_resolve_symbol_address(elf_file, sym, name, &value) < 0) {
free(relocs);
return -1;
}
*(uint64_t *)target = value + rela->r_addend;
break;
case UELF_R_X86_64_NONE:
break;
default:
uELF_WARN("Unsupported relocation type %u at offset 0x%lx", type,
(unsigned long)rela->r_offset);
break;
}
...

其中的 uELF64_resolve_symbol_address 函数负责对

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
static int uELF64_resolve_symbol_address(uElf64_File *elf_file,
const uElf64_Sym *sym,
const char *name,
uint64_t *value) {
if (!sym || !value) {
return -1;
}
// st_shndx 是该符号所在的节区索引(section index)
// 如果这个索引是普通段,就说明这个符号是在 当前 ELF 内部定义的
// 普通节区索引(0x0001 ~ 0xFEFF)
// 特殊保留值(≥ 0xFF00)
// ELF 标准: 所有保留或特殊用途的节索引都位于 0xFF00 ~ 0xFFFF 范围内。
// Symbol: my_function
// st_shndx = .text (index 5)
// st_value = 0x120
// load_base = 0x400000
// 最终地址 = 0x400000 + 0x120 = 0x400120
if (sym->st_shndx != 0 && sym->st_shndx < 0xff00) {
*value = elf_file->load_base + sym->st_value;
return 0;
}

// 处理未定义符号的情况
if (!name || name[0] == '\0') {
// 如果是弱符号则允许未定义.
// extern int weak_var __attribute__((weak));
if (UELF64_ST_BIND(sym->st_info) == UELF_STB_WEAK) {
*value = 0;
return 0;
}
return -1;
}

// 查找外部符号地址
// RTLD_DEFAULT 表示在当前进程所有已加载的共享库中搜索
dlerror();
// 这一步会帮我们解析 GOT/PLT 表,因此我们无需显式操作
void *addr = dlsym(RTLD_DEFAULT, name);
const char *err = dlerror();
if (err != NULL || addr == NULL) {
// 如果找不到并且是弱符号,警告.
if (UELF64_ST_BIND(sym->st_info) == UELF_STB_WEAK) {
uELF_WARN("Leaving weak external symbol '%s' unresolved", name);
*value = 0;
return 0;
}
uELF_ERROR("Failed to resolve external symbol '%s': %s", name,
err ? err : "unknown error");
return -1;
}

// 返回符号的地址
*value = (uint64_t)(uintptr_t)addr;
return 0;
}

上面关于 st_shndx 进一步说明.

如果是 SHN_UNDEF 表示符号未在本文件定义

  • 强符号: 必须在别处定义,否则链接/装载时报错.

  • 弱符号(STB_WEAK): 允许缺失,解析不到时可当 0 处理.

数值 含义 影响
SHN_UNDEF 0 未定义符号(在本文件中只被引用未被定义) 需要由链接器/动态链接器去别的目标里解析;若是弱符号可允许为 0
SHN_ABS 0xFFF1 绝对符号(不随重定位变化) st_value 即最终值,不能再按节进行重定位
SHN_COMMON 0xFFF2 公共块(未分配节的未初始化数据) 链接时由链接器分配到 .bss 一类节;st_size=大小,st_value=对齐
SHN_XINDEX 0xFFFF 扩展节索引标志 真正的节索引放在辅助节 SHT_SYMTAB_SHNDX
SHN_LORESERVE 0xFF00 保留区下界 通常用于 OS/Proc 特定
SHN_HIRESERVE 0xFFFF 保留区上界 ——

另外这里对 GOT/PLT 表进行说明.

动态库的加载地址在运行时才确定,编译时无法知道这些函数的真实地址.

GOT(Global Offset Table)和 PLT(Procedure Linkage Table)是 ELF 动态链接中两张配合使用的运行时跳转表.

它们是为了让 ELF 在 运行时加载时 仍然能正确调用外部函数、访问全局变量而设计的.

GOT 表: 保存需要运行时修正的地址. 程序运行时不直接访问符号地址,而是通过 GOT 表间接访问.

例如访问一个外部的全局变量 extern int global;,会直接经过 GOT 表.

编译器会生成.

1
2
mov rax, [rip + offset_to_GOT_entry]  ; 通过GOT表间接取 global 的地址
mov eax, [rax]

当符号后期需要的时候被解析,这里才会变成实际地址.

PLT 表: 让外部函数调用延迟到运行时再解析,每个外部函数对应一个 PLT “stub”.

PLT 表是为了间接访问 GOT 表来获取地址.

假设有函数 puts().

1
puts("Hello");

编译时实际生成的地址为.

1
call puts@plt

通过 PLT 我们间接地访问到 GOT 表,或者 GOT 表此时还没有实际地址,我们通过间接的逻辑来填充 GOT 表以此来实现延迟绑定.

我的理解是: GOT 存放“真实地址”,PLT 存放“跳转逻辑”.