0 概述

长时间不接触容易忘记 ELF 文件的各个字段含义, 本篇做一个 ELF 文件的实践.

  • 实验平台: x86_64

1 ELF file headers

1.1 实验文件代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
extern int *host_value_ptr;
extern int host_add(int a, int b);

static int local_offset(int x)
{
return x + 3;
}

static int del_offset(int x)
{
return x - 100;
}

int target_func(int x)
{
return host_add(local_offset(x), *host_value_ptr) + 7;
}

int unchanged_func(int x)
{
return x * 2;
}

1.2 file header 解析

给出 .o 文件, 和可执行文件分别分析.

相关实验可以直接看 examples/mini-upatch/labs/file_headers/entry_point.md, 里面把 Entry point address_startmainPT_LOAD 的关系单独拆成了一个最小实验.

可重定位文件.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ readelf -Wh diff_old.o
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 3392 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 28
Section header string table index: 27

可执行文件.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ readelf -Wh mini-diff
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Position-Independent Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x1320
Start of program headers: 64 (bytes into file)
Start of section headers: 55808 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 13
Size of section headers: 64 (bytes)
Number of section headers: 38
Section header string table index: 37

C 结构.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct {
unsigned char e_ident[16];
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;

1.2.1 Magic

对应 e_ident, 16 字节的字段是 ELF 文件的核心标识.

7f 45 4c 46 是 ELF 前 4 字节的固定魔数.

其中 45 4c 46 就是 ASCII 的 ELF. 第 5 个字节是 EI_CLASS, 0x01 表示 32 位, 0x02 表示 64 位. 第 6 个字节是 EI_DATA, 0x01 表示小端序, 0x02 表示大端序. 第 7 个字节是 EI_VERSION, 表示 ELF 的版本号, 第 8 个字节是 EI_OSABI, 表示 ABI 规范, 0x00 表示 System V, 0x03 表示 Linux. 第 9 个字节表示 EI_ABIVERSION, 注明了 ABI 的版本.

1.2.2 Class

对应 e_ident[EI_CLASS].

这个字段说明 ELF 文件采用的是 32 位格式还是 64 位格式. 常见取值如下:

  • ELFCLASSNONE = 0, 非法或未指定.
  • ELFCLASS32 = 1, 表示 32 位 ELF.
  • ELFCLASS64 = 2, 表示 64 位 ELF.

在我的实验里, diff_old.omini-diff 都显示为 ELF64, 说明两者都要按 Elf64_Ehdr, Elf64_Shdr, Elf64_Sym 等 64 位结构解释. 这只说明 ELF 格式本身是 64 位, 不直接等价于 CPU 架构, CPU 架构还要看后面的 Machine 字段.

1.2.3 Data

对应 e_ident[EI_DATA].

这个字段说明 ELF 中多字节整数的存储字节序. 常见取值如下:

  • ELFDATANONE = 0, 非法或未指定.
  • ELFDATA2LSB = 1, 小端序, 低位字节在前.
  • ELFDATA2MSB = 2, 大端序, 高位字节在前.

readelf 中显示的 2's complement, little endian 重点是 little endian, 表示当前文件按小端序解析. 例如一个 32 位整数 0x12345678, 在文件中的字节顺序是 78 56 34 12. 这和 x86-64 平台的默认字节序一致.

在我的实验里, diff_old.omini-diff 都是小端序, 这和 x86-64 平台的默认字节序一致.

1.2.4 Version

对应 e_ident[EI_VERSION].

这个字段表示 ELF 头标识区里的版本号. 目前几乎总是 1, 也就是 EV_CURRENT. 如果这里不是当前版本, 那么解析器通常会认为这个文件不是自己支持的 ELF 版本.

在我的实验里, .o 文件和可执行文件这里都显示 Version: 1 (current), 表示二者都使用当前 ELF 规范版本.

1.2.5 OS/ABI

对应 e_ident[EI_OSABI].

这个字段说明 ELF 文件声明自己面向哪种 OS/ABI 约定. 它更像一个 ABI 提示, 常见值如下:

  • ELFOSABI_SYSV = 0, 即 UNIX - System V.
  • ELFOSABI_LINUX = 3, Linux ABI.
  • 其他系统还可能使用 FreeBSD, NetBSD, Solaris 等不同取值.

需要注意的是, Linux 上生成的 ELF 文件也经常写成 System V, 这并不表示它不是 Linux 程序, 只是说它遵循通用 SysV 风格的 ELF 规则.

在我的实验里, diff_old.omini-diff 都显示为 UNIX - System V, 这是非常常见的结果.

1.2.6 ABI Version

对应 e_ident[EI_ABIVERSION].

这个字段是前面 OS/ABI 的子版本号. 对 System V 来说通常就是 0, 很多普通 ELF 文件也不会给出额外含义.

在我的实验里, 两个文件都显示 ABI Version: 0, 基本都可以理解为默认值, 没有更多特殊信息.

1.2.7 Type

对应 e_type.

这个字段决定 ELF 文件的类型, 常见值如下:

  • ET_REL, 可重定位文件, 也就是 .o.
  • ET_EXEC, 可执行文件.
  • ET_DYN, 共享库或 PIE 可执行文件.
  • ET_CORE, core dump 文件.

在我的实验里, diff_old.o 显示为 REL (Relocatable file), 表示它只是编译后的目标文件, 还没有完成最终链接. 这种文件通常包含 .text, .data, .symtab, .rela.* 等节, 供链接器继续处理, 因此它没有最终运行地址, 也通常没有程序头表.

mini-diff 显示为 DYN (Position-Independent Executable file). 这里虽然打印的是 DYN, 但它并不是共享库, 而是 PIE 可执行文件. 现代 Linux 默认会把开启 PIE 的可执行文件标记成 ET_DYN, 这样程序装载时就可以配合地址随机化, 不把代码固定在某个虚拟地址上.

1.2.8 Machine

对应 e_machine.

这个字段说明目标机器架构. 我的两个样例都显示 Advanced Micro Devices X86-64, 对应常量 EM_X86_64.

这里的含义是: 文件中的机器码、重定位类型、调用约定等都针对 x86-64 架构. 名字里虽然写了 AMD, 但它并不只支持 AMD CPU, Intel 的 64 位处理器也兼容这一架构.

1.2.9 Version

对应 e_version.

这是 ELF Header 正式字段里的版本号, 与前面的 e_ident[EI_VERSION] 含义类似, 也通常为 EV_CURRENT, 即十六进制的 0x1.

在我的实验里, .o 文件和可执行文件这里都显示 Version: 0x1, 说明头部主体结构也都使用当前 ELF 版本. 一个正常 ELF 文件通常会同时在 e_ident[EI_VERSION]e_version 里都写成当前版本.

1.2.10 Entry point address

对应 e_entry.

这个字段表示程序入口地址. 对可执行文件来说, 它通常指向 _start 或实际装载后跳转执行的位置.

如果要看一个可以直接复现的最小实验, 可以直接看 examples/mini-upatch/labs/file_headers/entry_point.md. 这个实验里已经把 mini-diffe_entry_startmain 和可执行 PT_LOAD 段放到同一组命令里核对了.

diff_old.o 来说, 当前文件类型是 ET_REL, 即可重定位目标文件, 还不是一个可以直接执行的映像, 所以 Entry point address0x0 是正常现象.

mini-diff 来说, 这里是 0x1320, 表示程序装载完成后, 进程最初会从映像基址加上 0x1320 的位置开始执行. 由于 mini-diff 是 PIE, 这个值更适合理解成映像内部偏移, 真正运行时虚拟地址还要加上装载基址.

1.2.11 Start of program headers

对应 e_phoff.

这个字段表示 Program Header Table 在文件中的起始偏移. Program Header 主要服务于加载器, 描述哪些段需要映射到内存、权限是什么、装载地址如何计算.

在我的实验里, diff_old.o 的值为 0, 说明文件里没有 Program Header Table. 这与 ET_REL 类型一致, 因为 .o 文件只是链接输入, 不是直接给内核装载执行的目标.

mini-diff 的值为 64, 说明 ELF Header 之后紧跟着就是 Program Header Table. 这也符合常见可执行文件布局: 前 64 字节是 Elf64_Ehdr, 从偏移 64 开始是若干个程序头项, 供内核和动态加载器装载使用.

1.2.12 Start of section headers

对应 e_shoff.

这个字段表示 Section Header Table 在文件中的起始偏移. Section Header 记录了每个节的元信息, 包括名字偏移、类型、标志、文件偏移、大小、对齐方式等.

在我的实验里, diff_old.o 的值是 3392, 说明从文件偏移 3392 字节开始是节头表. 对 .o 文件来说, Section Header 通常比 Program Header 更重要, 因为链接器主要围绕节、符号和重定位工作.

mini-diff 的值是 55808, 说明节头表被放在文件尾部附近. 这也是常见布局, 因为运行时装载主要依赖 Program Header, Section Header 更多是留给链接器、调试器和分析工具使用.

1.2.13 Flags

对应 e_flags.

这个字段用于存放架构相关的额外标志位. 某些架构会在这里编码 ABI 模式、浮点约定或其他特殊属性, 但在 x86-64 上通常没有额外含义.

在我的实验里, diff_old.omini-diff 都是 0x0, 这是 x86-64 ELF 中很常见的情况.

1.2.14 Size of this header

对应 e_ehsize.

这个字段表示 ELF Header 自身的大小. 对 64 位 ELF 来说, 正常值通常是 64 字节; 对 32 位 ELF 则通常是 52 字节.

在我的实验里, 两个文件这里都是 64 (bytes), 与 Elf64_Ehdr 的大小一致, 也再次印证当前文件是 ELF64.

1.2.15 Size of program headers

对应 e_phentsize.

这个字段表示每个 Program Header 条目的大小. 如果文件没有 Program Header Table, 这里通常就是 0.

在我的实验里, diff_old.o 的值为 0, 因为它根本没有程序头表.

mini-diff 的值为 56, 说明每个 Program Header 条目大小是 56 字节, 这正好对应 Elf64_Phdr 的标准大小.

1.2.16 Number of program headers

对应 e_phnum.

这个字段表示 Program Header 条目的个数. 可执行文件或共享库通常会有若干条目, 而 .o 文件通常没有.

在我的实验里, diff_old.o 这里是 0, 和前面的 e_phoff = 0, e_phentsize = 0 一起说明该文件没有 Program Header Table.

mini-diff 这里是 13, 表示它一共有 13 个程序头项. 这些条目里通常会包含 PT_PHDR, PT_INTERP, PT_LOAD, PT_DYNAMIC, PT_NOTE, PT_GNU_STACK, PT_GNU_RELRO 等信息, 内核和动态加载器会按这些段描述把程序映射到内存里.

1.2.17 Size of section headers

对应 e_shentsize.

这个字段表示每个 Section Header 条目的大小. 在 ELF64 中, Elf64_Shdr 通常是 64 字节; 在 ELF32 中, Elf32_Shdr 通常是 40 字节.

在我的实验里, 两个文件这里都显示 64 (bytes), 说明节头表中的每一项都是标准的 Elf64_Shdr 结构.

1.2.18 Number of section headers

对应 e_shnum.

这个字段表示 Section Header 条目的总数. 这里统计的是节头项数量, 包括第 0 个空节, 也包括 .symtab, .strtab, .rela.*, .debug_* 之类分析和链接需要的节.

在我的实验里, diff_old.o 的值是 28, 表示它一共含有 28 个节头项. 这并不意味着一定有 28 个功能性代码段, 因为其中还包含各种元数据节.

mini-diff 的值是 38, 比 .o 文件更多. 这通常是因为最终可执行文件除了基本代码和数据节以外, 还会带上 .interp, .dynamic, .dynsym, .dynstr, .got, .plt, .init_array, .fini_array 等与动态链接和运行时装载有关的节.

1.2.19 Section header string table index

对应 e_shstrndx.

这个字段表示“节名字符串表”所在的 Section Header 索引. ELF 中每个节的名字不是直接存放在节头里的, 节头只保存一个名字偏移, 真正的字符串集中保存在 .shstrtab 节中.

在我的实验里, diff_old.o 的值是 27, 表示编号 27 的节是节名字符串表.

mini-diff 的值是 37, 表示它的节名字符串表位于第 37 个节头项. 解析诸如 .text, .data, .dynamic, .plt 这些节名时, 也都要先找到这个字符串表, 再根据各节头里的 sh_name 偏移取出真实名字.

2 ELF section headers

2.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
$ readelf -WS diff_old.o
There are 28 section headers, starting at offset 0xd40:

Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 0] NULL 0000000000000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 0000000000000000 000040 000000 00 AX 0 0 1
[ 2] .data PROGBITS 0000000000000000 000040 000000 00 WA 0 0 1
[ 3] .bss NOBITS 0000000000000000 000040 000000 00 WA 0 0 1
[ 4] .text.local_offset PROGBITS 0000000000000000 000040 000013 00 AX 0 0 1
[ 5] .text.del_offset PROGBITS 0000000000000000 000053 000013 00 AX 0 0 1
[ 6] .text.target_func PROGBITS 0000000000000000 000066 000035 00 AX 0 0 1
[ 7] .rela.text.target_func RELA 0000000000000000 000868 000048 18 I 25 6 8
[ 8] .text.unchanged_func PROGBITS 0000000000000000 00009b 000012 00 AX 0 0 1
[ 9] .debug_info PROGBITS 0000000000000000 0000ad 000117 00 0 0 1
[10] .rela.debug_info RELA 0000000000000000 0008b0 000180 18 I 25 9 8
[11] .debug_abbrev PROGBITS 0000000000000000 0001c4 0000d9 00 0 0 1
[12] .debug_aranges PROGBITS 0000000000000000 00029d 000060 00 0 0 1
[13] .rela.debug_aranges RELA 0000000000000000 000a30 000078 18 I 25 12 8
[14] .debug_rnglists PROGBITS 0000000000000000 0002fd 000035 00 0 0 1
[15] .rela.debug_rnglists RELA 0000000000000000 000aa8 000060 18 I 25 14 8
[16] .debug_line PROGBITS 0000000000000000 000332 0000af 00 0 0 1
[17] .rela.debug_line RELA 0000000000000000 000b08 0000a8 18 I 25 16 8
[18] .debug_str PROGBITS 0000000000000000 0003e1 00010a 01 MS 0 0 1
[19] .debug_line_str PROGBITS 0000000000000000 0004eb 00008d 01 MS 0 0 1
[20] .comment PROGBITS 0000000000000000 000578 00002e 01 MS 0 0 1
[21] .note.GNU-stack PROGBITS 0000000000000000 0005a6 000000 00 0 0 1
[22] .note.gnu.property NOTE 0000000000000000 0005a8 000020 00 A 0 0 8
[23] .eh_frame PROGBITS 0000000000000000 0005c8 000098 00 A 0 0 8
[24] .rela.eh_frame RELA 0000000000000000 000bb0 000060 18 I 25 23 8
[25] .symtab SYMTAB 0000000000000000 000660 0001b0 18 26 14 8
[26] .strtab STRTAB 0000000000000000 000810 000057 00 0 0 1
[27] .shstrtab STRTAB 0000000000000000 000c10 00012b 00 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)

可执行文件.

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
$ readelf -WS mini-diff
There are 38 section headers, starting at offset 0xda00:

Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 0] NULL 0000000000000000 000000 000000 00 0 0 0
[ 1] .interp PROGBITS 0000000000000318 000318 00001c 00 A 0 0 1
[ 2] .note.gnu.property NOTE 0000000000000338 000338 000030 00 A 0 0 8
[ 3] .note.gnu.build-id NOTE 0000000000000368 000368 000024 00 A 0 0 4
[ 4] .note.ABI-tag NOTE 000000000000038c 00038c 000020 00 A 0 0 4
[ 5] .gnu.hash GNU_HASH 00000000000003b0 0003b0 000028 00 A 6 0 8
[ 6] .dynsym DYNSYM 00000000000003d8 0003d8 0002d0 18 A 7 1 8
[ 7] .dynstr STRTAB 00000000000006a8 0006a8 000140 00 A 0 0 1
[ 8] .gnu.version VERSYM 00000000000007e8 0007e8 00003c 02 A 6 0 2
[ 9] .gnu.version_r VERNEED 0000000000000828 000828 000060 00 A 7 1 8
[10] .rela.dyn RELA 0000000000000888 000888 0000d8 18 A 6 0 8
[11] .rela.plt RELA 0000000000000960 000960 000228 18 AI 6 24 8
[12] .init PROGBITS 0000000000001000 001000 00001b 00 AX 0 0 4
[13] .plt PROGBITS 0000000000001020 001020 000180 10 AX 0 0 16
[14] .plt.got PROGBITS 00000000000011a0 0011a0 000010 10 AX 0 0 16
[15] .plt.sec PROGBITS 00000000000011b0 0011b0 000170 10 AX 0 0 16
[16] .text PROGBITS 0000000000001320 001320 003ee6 00 AX 0 0 16
[17] .fini PROGBITS 0000000000005208 005208 00000d 00 AX 0 0 4
[18] .rodata PROGBITS 0000000000006000 006000 000620 00 A 0 0 16
[19] .eh_frame_hdr PROGBITS 0000000000006620 006620 000164 00 A 0 0 4
[20] .eh_frame PROGBITS 0000000000006788 006788 000590 00 A 0 0 8
[21] .init_array INIT_ARRAY 0000000000007d08 007d08 000008 08 WA 0 0 8
[22] .fini_array FINI_ARRAY 0000000000007d10 007d10 000008 08 WA 0 0 8
[23] .dynamic DYNAMIC 0000000000007d18 007d18 0001f0 10 WA 7 0 8
[24] .got PROGBITS 0000000000007f08 007f08 0000f8 08 WA 0 0 8
[25] .data PROGBITS 0000000000008000 008000 000010 00 WA 0 0 8
[26] .bss NOBITS 0000000000008020 008010 000010 00 WA 0 0 32
[27] .comment PROGBITS 0000000000000000 008010 00002d 01 MS 0 0 1
[28] .debug_aranges PROGBITS 0000000000000000 00803d 000030 00 0 0 1
[29] .debug_info PROGBITS 0000000000000000 00806d 00232e 00 0 0 1
[30] .debug_abbrev PROGBITS 0000000000000000 00a39b 000307 00 0 0 1
[31] .debug_line PROGBITS 0000000000000000 00a6a2 0015c7 00 0 0 1
[32] .debug_str PROGBITS 0000000000000000 00bc69 000b03 01 MS 0 0 1
[33] .debug_line_str PROGBITS 0000000000000000 00c76c 00017a 01 MS 0 0 1
[34] .debug_rnglists PROGBITS 0000000000000000 00c8e6 000022 00 0 0 1
[35] .symtab SYMTAB 0000000000000000 00c908 000930 18 36 57 8
[36] .strtab STRTAB 0000000000000000 00d238 00064a 00 0 0 1
[37] .shstrtab STRTAB 0000000000000000 00d882 00017a 00 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)

2 ELF sections headers

readelf -WS 对应的是 Section Header Table, 也就是节头表.

如果说 ELF Header 是整个 ELF 文件的总目录, 那么 Section Header Table 就像每一章的分目录, 说明:

  • 每个 section 叫什么名字
  • 它是什么类型
  • 它在文件里的偏移是多少
  • 它有多大
  • 它是否要装载进内存
  • 它和其他 section 是否存在关联

对链接器、调试器和分析工具来说, section 比 segment 更重要. 尤其对 .o 文件来说, 几乎所有工作都是围绕 section, symbol 和 relocation 展开的.

2.1 对应的 C 结构

readelf -WS 打印的每一行, 本质上都来自一个 Elf64_Shdr.

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct {
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;

这些字段和 readelf -WS 各列的对应关系如下:

  • Name 对应 sh_name
  • Type 对应 sh_type
  • Address 对应 sh_addr
  • Off 对应 sh_offset
  • Size 对应 sh_size
  • Flg 对应 sh_flags
  • ES 对应 sh_entsize
  • Lk 对应 sh_link
  • Inf 对应 sh_info
  • Al 对应 sh_addralign

其中 Name 本身不是直接存字符串, 而是一个偏移, 需要到 .shstrtab 里查真正的 section 名字.

如果要单独看一个关于 sh_name 的最小实验, 可以直接看 examples/mini-upatch/labs/section_headers/sh_name.md.

2.2 readelf -WS 各列含义

先看表头:

1
[Nr] Name Type Address Off Size ES Flg Lk Inf Al

2.2.1 Name

section 的名字.

例如:

  • .text
  • .data
  • .bss
  • .rela.text.target_func
  • .dynsym
  • .shstrtab

名字本身不直接存在 Elf64_Shdr 里, 而是通过 sh_name.shstrtab 查到. 其中 sh_name 更准确地说是“相对 .shstrtab 起始位置的字节偏移”, 不是直接的字符串, 也不是抽象编号. 具体可以直接看 examples/mini-upatch/labs/section_headers/sh_name.md.

2.2.2 Type

section 的类型, 决定这个 section 存什么语义的数据.

在这两个样例里最常见的类型有:

  • NULL
    • 第 0 项保留空 section.
  • PROGBITS
    • 最常见, 表示普通原始数据.
    • 代码段、只读数据段、注释段、调试段很多都属于它.
  • NOBITS
    • 占内存, 但不占文件内容.
    • 典型例子就是 .bss.
  • RELA
    • 带显式 addend 的重定位表.
  • NOTE
    • note 信息, 用来存 ABI build-id property 等元数据.
  • SYMTAB: 完整符号表, section 类型是 SHT_SYMTAB, 每个表项通常是一个 Elf64_Sym, 里面会记录符号名、符号值、大小、绑定属性、符号类型、所属 section 等信息. 它通常比 DYNSYM 更全, 会包含 local symbol, global symbol, undefined symbol, section symbol 等.
    • 在我的实验里:
      • diff_old.o.symtab 是核心元数据之一, 因为 .rela.text.target_funcLk = 25, 指向的就是 .symtab.
      • mini-diff 也保留了 .symtab, 方便调试和分析, 但它不是运行时动态链接所必需的那一套符号表.
  • DYNSYM: 动态链接用的符号表, section 类型是 SHT_DYNSYM, 每个表项同样通常是 Elf64_Sym. 它只保留运行时动态链接真正需要的那一部分符号, 通常比 SYMTAB 更精简. 主要给动态加载器使用, 例如解析共享库符号、处理 .rela.dyn.rela.plt.
    • 在我的实验里:
      • diff_old.o 没有 .dynsym, 因为它只是 ET_REL 目标文件.
      • mini-diff.dynsym, 因为它是最终可执行文件, 需要参与动态链接.
  • STRTAB: 字符串表, section 类型是 SHT_STRTAB, ELF 里很多结构体并不直接保存字符串, 而是保存一个偏移, 再去某个字符串表里取真正的名字.
    • 在我的实验里:
      • .strtab.symtab 提供符号名字符串
      • .dynstr.dynsym 提供动态符号名字符串
      • .shstrtab 给 section header 的 sh_name 提供 section 名字字符串
  • GNU_HASH
    • 动态符号查找用的 GNU 哈希表.
  • VERSYM
    • 动态符号版本索引表.
  • VERNEED
    • 依赖的版本需求表.
  • INIT_ARRAY
    • 构造函数数组.
  • FINI_ARRAY
    • 析构函数数组.
  • DYNAMIC
    • 动态链接器要读取的元数据表.

2.2.3 Address

section 运行时地址, 对应 sh_addr.

这个字段在 .o 文件和可执行文件里的意义差别很大:

  • .o 文件来说, 地址通常还没有最终确定, 所以大多是 0.
  • 对最终可执行文件来说, 这是链接后 section 在映像中的虚拟地址.

在我的实验里:

  • diff_old.o 所有 section 基本都是 0
  • mini-diff.text 地址是 0x1320
  • mini-diff.rodata 地址是 0x6000
  • mini-diff.data 地址是 0x8000

如果要单独看一个关于 sh_addr 和运行时装载基址关系的最小实验, 可以直接看 examples/mini-upatch/labs/section_headers/address_runtime_layout.md.

在我的实验里:

  • mini-diff.textsh_addr = 0x1320
  • 运行时装载基址是 0x555555554000
  • _start 的实际地址是 0x555555555320

正好满足:

  • 0x555555554000 + 0x1320 = 0x555555555320

这说明对 PIE 可执行文件来说, sh_addr 更适合理解成“相对映像基址的虚拟地址”, 真正的运行时绝对地址还要再加上装载基址.

2.2.4 Off

section 在 ELF 文件中的文件偏移, 对应 sh_offset.

这个值说明“从文件开头数多少字节能找到这个 section 的原始内容”.

在我的实验里:

  • diff_old.o.text.target_func 偏移是 0x66
  • mini-diff.text 偏移是 0x1320
  • mini-diff.dynamic 偏移是 0x7d18

2.2.5 Size

section 大小, 对应 sh_size.

如果是 NOBITS 类型, 比如 .bss, 这个大小表示运行时要分配多大内存, 但文件里不真正占这段字节内容.

例如:

  • diff_old.o.text.target_func 大小是 0x35
  • mini-diff.text 大小是 0x3ee6
  • mini-diff.bss 大小是 0x10

这里我们结合 Off 和 Size 看一下 .text.target_func 的内容.

1
2
3
4
5
6
$ hexdump -C -s 0x66 -n 0x35 diff_old.o
00000066 f3 0f 1e fa 55 48 89 e5 53 48 83 ec 18 89 7d ec |....UH..SH....}.|
00000076 48 8b 05 00 00 00 00 8b 18 8b 45 ec 89 c7 e8 00 |H.........E.....|
00000086 00 00 00 89 de 89 c7 e8 00 00 00 00 83 c0 07 48 |...............H|
00000096 8b 5d f8 c9 c3 |.]...|
0000009b

然后我们反汇编内容查看是否正确, 这里依然查看 .text.target_func.

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
$ objdump -d -j .text.target_func --start-address=0x0 --stop-address=0x35 diff_old.o

diff_old.o: file format elf64-x86-64


Disassembly of section .text.target_func:

0000000000000000 <target_func>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: 53 push %rbx
9: 48 83 ec 18 sub $0x18,%rsp
d: 89 7d ec mov %edi,-0x14(%rbp)
10: 48 8b 05 00 00 00 00 mov 0x0(%rip),%rax # 17 <target_func+0x17>
17: 8b 18 mov (%rax),%ebx
19: 8b 45 ec mov -0x14(%rbp),%eax
1c: 89 c7 mov %eax,%edi
1e: e8 00 00 00 00 call 23 <target_func+0x23>
23: 89 de mov %ebx,%esi
25: 89 c7 mov %eax,%edi
27: e8 00 00 00 00 call 2c <target_func+0x2c>
2c: 83 c0 07 add $0x7,%eax
2f: 48 8b 5d f8 mov -0x8(%rbp),%rbx
33: c9 leave
34: c3 ret

2.2.6 ES

entry size, 对应 sh_entsize.

只有“由固定大小条目组成的 section”这个字段才有明显意义.

常见的:

  • RELAES = 0x18, 因为每个 Elf64_Rela 是 24 字节
  • SYMTAB / DYNSYMES = 0x18, 因为每个 Elf64_Sym 是 24 字节
  • .pltES = 0x10, 表示这里的表项以 16 字节为一个单位组织
  • 普通代码段和数据段通常 ES = 0

2.2.7 Flg

section 标志位, 对应 sh_flags.

在这两个样例里最常见的是:

  • W: 可写
  • A: 运行时需要装载进内存
  • X: 可执行
  • M: 可合并
  • S: 是字符串表风格的数据
  • I: 这个 section 的 sh_info 有特殊索引意义, 常见于 relocation section (实验 TODO)

常见的典型例子:

  • AX: 可装载且可执行, 典型是代码段
  • WA: 可写且可装载, 典型是 .data.bss.got
  • MS: 可合并的字符串型数据, 常见于 .comment.debug_str
  • AI: 可装载, 且 sh_info 有索引意义, 例如 .rela.plt

2.2.8 Lk

link 字段, 对应 sh_link.

这个字段的含义依赖于 section 类型.

最常见的情况:

  • SYMTAB / DYNSYM, 它通常指向关联的字符串表
  • RELA, 它通常指向该 relocation section 使用的符号表
  • GNU_HASH / VERSYM / VERNEED, 它也常指向动态字符串表或动态符号表

例如在我的实验里:

  • diff_old.o.rela.text.target_funcLk = 25, 指向 .symtab
  • diff_old.o.symtabLk = 26, 指向 .strtab
  • mini-diff.dynsymLk = 7, 指向 .dynstr

2.2.9 Inf

info 字段, 对应 sh_info.

这个字段同样依赖于 section 类型.

最常见的情况:

  • RELA, 它通常指向“它要修正哪个目标 section”
  • SYMTAB, 它通常表示第一个非本地符号的索引
  • 对版本表和其他特殊表, 也可能有额外含义

在我的实验里:

  • diff_old.o.rela.text.target_funcInf = 6, 指向 .text.target_func
  • diff_old.o.rela.debug_infoInf = 9, 指向 .debug_info
  • mini-diff.rela.pltInf = 24, 指向 .got
  • mini-diff.symtabInf = 57, 表示前 57 个符号属于 local 范围

2.2.10 Al

alignment, 对应 sh_addralign.

它表示这个 section 在文件和内存中通常需要满足的对齐要求.

在我的实验里:

  • 代码段经常是 16 字节对齐
  • .dynamic.got.eh_frame 常见 8 字节对齐
  • .bss 有时会对齐到 32

对齐要求越高, 链接器在布局时越要插入 padding.

2.3 先看 .o 和可执行文件的总体差异

在我的实验里, diff_old.omini-diff 的 section 组织差别非常明显:

  • diff_old.o
    • 以“编译单元”为中心
    • 代码按函数被拆成 .text.local_offset.text.target_func 这种小 section
    • 有很多 .rela.* 记录链接前尚未确定的引用
    • 没有 .interp.dynamic.dynsym.plt.got
  • mini-diff
    • 以“最终运行映像”为中心
    • 有统一的 .text.rodata.data.bss
    • 带上动态链接所需的 .interp.dynamic.dynsym.dynstr
    • 带上 PLT/GOT 和版本信息
    • 仍然保留调试节和普通符号表, 方便分析和调试

也就是说:

  • .o 文件更像链接输入材料
  • 可执行文件更像运行时最终成品

2.4 可重定位文件 diff_old.o 的 section 解析

先按功能分组来看.

2.4.1 空 section

1
[ 0] NULL

第 0 项永远保留为空 section. 这是 ELF 规范规定的.

很多索引字段如果填 0, 往往就表示“没有关联对象”或者“未定义”.

2.4.2 代码与数据 section

1
2
3
4
5
6
7
[ 1] .text
[ 2] .data
[ 3] .bss
[ 4] .text.local_offset
[ 5] .text.del_offset
[ 6] .text.target_func
[ 8] .text.unchanged_func

这里最值得注意的是, 真正有内容的代码不在总 .text, 而是被拆成了多个函数级 section:

  • .text.local_offset
  • .text.del_offset
  • .text.target_func
  • .text.unchanged_func

这通常来自 -ffunction-sections 一类编译策略. 这样做的好处是:

  • 链接器可以做更细粒度的垃圾回收
  • diff/patch 工具可以按函数为单位追踪变更
  • upatch-diff 这类工具更容易把“函数变化”映射到“section 变化”

其中:

  • .text 是空的, 大小为 0
  • .data 是空的
  • .bss 也是空的

这说明当前这个样例里真正关心的是代码和外部引用, 并没有在目标文件内定义静态全局数据.

2.4.3 relocation section

1
2
3
4
5
6
[ 7]  .rela.text.target_func
[10] .rela.debug_info
[13] .rela.debug_aranges
[15] .rela.debug_rnglists
[17] .rela.debug_line
[24] .rela.eh_frame

这些 RELA section 是 .o 文件的核心信息之一.

以:

1
[ 7] .rela.text.target_func RELA ... Lk=25 Inf=6

为例, 可以这样理解:

  • 它是一个 relocation 表
  • Lk = 25, 说明它引用的符号表是 .symtab
  • Inf = 6, 说明它要修正的目标 section 是第 6 项 .text.target_func

这正符合 target_func() 的源码:

1
return host_add(local_offset(x), *host_value_ptr) + 7;

这里至少涉及三类尚未最终确定的引用:

  • 对本地函数 local_offset 的引用
  • 对外部变量 host_value_ptr 的引用
  • 对外部函数 host_add 的引用

这些位置在编译成 .o 时还不能完全写死, 所以编译器会先生成占位指令和 relocation 记录, 等链接器后续修正.

此外还有几组 .rela.debug_*, 它们不是修正运行时代码的, 而是修正 DWARF 调试信息中对代码地址、范围、字符串等对象的引用.

2.4.4 调试信息 section

1
2
3
4
5
6
7
[ 9]  .debug_info
[11] .debug_abbrev
[12] .debug_aranges
[14] .debug_rnglists
[16] .debug_line
[18] .debug_str
[19] .debug_line_str

这些 section 都属于 DWARF 调试信息.

可以简单理解为:

  • .debug_info
    • 调试主信息, 描述函数、变量、类型、编译单元
  • .debug_abbrev
    • 调试信息的“语法模板”
  • .debug_aranges
    • 地址范围到编译单元的映射
  • .debug_rnglists
    • 更灵活的地址范围列表
  • .debug_line
    • 机器地址到源代码行号的映射
  • .debug_str
    • 调试信息使用的字符串池
  • .debug_line_str
    • 行号信息专用字符串池

这些 section 通常:

  • Address = 0
  • 没有 A 标志

因为它们不给程序运行时使用, 只给调试器和分析工具使用.

2.4.5 其他辅助 section

1
2
3
4
[20] .comment
[21] .note.GNU-stack
[22] .note.gnu.property
[23] .eh_frame

这几个 section 也值得单独说明.

  • .comment
    • 通常存编译器版本字符串, MS 表示可合并的字符串数据
  • .note.GNU-stack
    • 用来告诉链接器/装载器“这个对象文件不需要可执行栈”, 它自己一般是空 section
  • .note.gnu.property
    • GNU property note, 用来记录一些 ABI/硬件特性属性
  • .eh_frame
    • 异常展开和栈回溯信息, 即使不是 C++ 异常, 许多平台上的回溯也会依赖它

.eh_frame.o 里常常也带一个对应的 .rela.eh_frame, 因为里面的地址引用同样需要链接后修正.

2.4.6 字符串表和符号表

1
2
3
[25] .symtab
[26] .strtab
[27] .shstrtab

这三个 section 是分析 .o 文件时最关键的元数据区.

  • .symtab
    • 完整符号表
    • 记录本地符号、全局符号、未定义符号、section 符号等
  • .strtab
    • 普通符号字符串表
    • .symtab 中的名字偏移要到这里查
  • .shstrtab
    • section 名字字符串表
    • section header 中的 sh_name 要到这里查

在我的实验里:

  • .symtabLk = 26, 表示它关联的字符串表是 .strtab
  • .symtabInf = 14, 表示前 14 个符号是 local symbol

2.5 可执行文件 mini-diff 的 section 解析

最终可执行文件的 section 类型更丰富, 因为它不仅要保留符号和调试信息, 还要给动态加载器和运行时提供完整支持.

2.5.1 程序解释器与 note section

1
2
3
4
[ 1] .interp
[ 2] .note.gnu.property
[ 3] .note.gnu.build-id
[ 4] .note.ABI-tag
  • .interp
    • 指定动态加载器路径
    • 在我的实验里就是 /lib64/ld-linux-x86-64.so.2
  • .note.gnu.property
    • 记录 GNU property
  • .note.gnu.build-id
    • 记录 build-id, 方便调试和符号定位
  • .note.ABI-tag
    • 描述 ABI 兼容信息

这些 section 都带 A, 因为它们会被装载到进程映像中.

2.5.2 动态链接元数据

1
2
3
4
5
6
7
[ 5] .gnu.hash
[ 6] .dynsym
[ 7] .dynstr
[ 8] .gnu.version
[ 9] .gnu.version_r
[10] .rela.dyn
[11] .rela.plt

这组 section 是动态链接的核心元数据.

  • .gnu.hash
    • 动态符号查找加速结构
  • .dynsym
    • 动态链接需要导出或引用的符号表
  • .dynstr
    • 动态符号字符串表
  • .gnu.version
    • 动态符号版本索引
  • .gnu.version_r
    • 依赖的外部版本信息
  • .rela.dyn
    • 普通动态重定位
  • .rela.plt
    • PLT 相关的重定位

.o 文件相比, 这里的 relocation 已经不是给静态链接器用的“编译中间态”, 而是给动态加载器在程序启动时或首次调用时处理的.

2.5.3 可执行代码 section

1
2
3
4
5
6
[12] .init
[13] .plt
[14] .plt.got
[15] .plt.sec
[16] .text
[17] .fini

这些都是可执行 section, 都有 AX 标志.

  • .init
    • 早期初始化入口
  • .plt
    • Procedure Linkage Table, 外部函数调用跳板
  • .plt.got
    • 与 GOT 配合使用的一小段跳板代码
  • .plt.sec
    • 新版工具链中常见的 PLT 补充分区
  • .text
    • 主体代码段
  • .fini
    • 结束清理代码

在我的实验里:

  • .text 地址是 0x1320
  • 程序入口 e_entry 也是 0x1320

所以 _start 就位于 .text 的起始位置.

2.5.4 只读数据和回溯信息

1
2
3
[18] .rodata
[19] .eh_frame_hdr
[20] .eh_frame
  • .rodata
    • 只读常量区, 比如格式化字符串、只读表
  • .eh_frame_hdr
    • .eh_frame 的索引头
  • .eh_frame
    • 栈展开和回溯信息

这些 section 都带 A, 但不带 WX, 说明它们会进入内存, 但既不可写也不可执行.

2.5.5 可写运行时数据

1
2
3
4
5
6
[21] .init_array
[22] .fini_array
[23] .dynamic
[24] .got
[25] .data
[26] .bss

这些 section 都带 WA, 表示它们需要装载进内存并且可写.

  • .init_array
    • 构造函数指针数组
  • .fini_array
    • 析构函数指针数组
  • .dynamic
    • 动态加载器读取的动态信息表
  • .got
    • Global Offset Table
  • .data
    • 已初始化可写数据
  • .bss
    • 未初始化可写数据

尤其要注意:

  • .got
    • 在动态链接、PIE 和外部符号访问中非常关键
  • .bss
    • Type = NOBITS, 说明文件里不真正存这块内容, 运行时只分配零初始化内存

2.5.6 不参与运行时装载的调试与注释 section

1
2
3
4
5
6
7
8
[27] .comment
[28] .debug_aranges
[29] .debug_info
[30] .debug_abbrev
[31] .debug_line
[32] .debug_str
[33] .debug_line_str
[34] .debug_rnglists

这些 section 的 Address 都是 0, 也没有 A 标志.

这说明它们虽然保留在 ELF 文件中, 但不会参与运行时映射, 仅用于:

  • 调试
  • 反汇编
  • 符号恢复
  • 源码级分析

2.5.7 普通符号表与字符串表

1
2
3
[35] .symtab
[36] .strtab
[37] .shstrtab

即使可执行文件已经有 .dynsym, 通常仍然可能保留一份 .symtab.

两者差别是:

  • .dynsym
    • 给动态加载器用
    • 只保留动态链接必需的符号
  • .symtab
    • 给调试器和分析工具用
    • 更完整

在我的实验里:

  • .symtab 大小明显比 .dynsym
  • 这符合“完整符号表比动态符号表更全”的预期

2.6 结合这两个样例理解 section 的关键词

2.6.1 PROGBITS

最常见的 section 类型.

它只是说“这里是普通原始字节内容”, 但并不限定用途. 所以:

  • 代码可以是 PROGBITS
  • 只读数据可以是 PROGBITS
  • 调试信息也可以是 PROGBITS

真正区分用途, 还要结合:

  • section 名字
  • Flg
  • 是否在运行时映射

2.6.2 NOBITS

表示“运行时有, 文件里没有”.

最典型的就是 .bss.

它的特点是:

  • Size
  • 有可能带 WA
  • 文件偏移 Off 只是逻辑位置, 不会在文件里真正存这一段数据字节

2.6.3 RELA

表示 relocation with addend.

在 x86-64 上很常见, 每个条目通常对应 Elf64_Rela.

它的职责是:

  • 告诉链接器或加载器
  • 哪个位置要修正
  • 用哪个符号来修正
  • 再附加一个显式 addend

.o 文件中的 RELA 主要给静态链接器用, 可执行文件里的 .rela.dyn / .rela.plt 主要给动态加载器用.

2.6.4 SYMTABDYNSYM

都表示符号表, 但用途不同:

  • SYMTAB
    • 完整符号表
    • 更适合静态分析、调试、链接阶段
  • DYNSYM
    • 动态符号表
    • 更适合运行时动态链接

它们的核心区别可以直接概括成:

  • SYMTAB
    • 尽量完整
    • 面向链接器、调试器、离线分析
  • DYNSYM
    • 只保留运行时真正需要的子集
    • 面向动态加载器

结合我的两个样例来看最直观:

  • diff_old.o 里只有 .symtab, 没有 .dynsym
    • 因为 .o 文件还没有进入“运行时动态链接”这个阶段
    • 它的重点是给静态链接器和分析工具提供完整符号信息
  • mini-diff 里同时有 .dynsym.symtab
    • .dynsym 给运行时动态链接用
    • .symtab 给调试和分析用

可以把它们理解成:

  • .symtab 像“完整通讯录”
  • .dynsym 像“运行时真正会用到的联系人子集”

2.6.5 STRTAB

字符串表.

ELF 很少直接在结构体里存字符串, 往往都是:

  • 结构体里存偏移
  • 再去某个字符串表 section 中查真实字符串

常见字符串表有:

  • .strtab
    • 普通符号名字
  • .dynstr
    • 动态符号名字
  • .shstrtab
    • section 名字

如果把 SYMTAB / DYNSYM 看成“记录表”, 那 STRTAB 就是“名字池”.

举个和当前实验直接相关的例子:

  • diff_old.o.symtab 里会有 target_funclocal_offsethost_add 这些符号记录
  • 但这些名字字符串本身不直接塞在每个 Elf64_Sym 结构里
  • Elf64_Sym 里只会存一个名字偏移
  • 真正的字符串内容要去 .strtab

同理:

  • mini-diff.dynsym 里记录了运行时动态链接关心的符号
  • 真正的名字字符串要去 .dynstr

而 section header 这边的名字又是另一套:

  • Elf64_Shdr 里的 sh_name 不是直接名字
  • 它要去 .shstrtab 查 section 名

2.6.6 AX / WA / MS

这些组合一眼就能看出 section 的大类:

  • AX
    • 可执行代码
  • WA
    • 可写运行时数据
  • MS
    • 可合并字符串类元数据

3 ELF program headers

3.1 实验文件代码

可重定位文件.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ readelf -Ws diff_old.o

Symbol table '.symtab' contains 18 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS diff_old.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 4 .text.local_offset
3: 0000000000000000 19 FUNC LOCAL DEFAULT 4 local_offset
4: 0000000000000000 0 SECTION LOCAL DEFAULT 5 .text.del_offset
5: 0000000000000000 19 FUNC LOCAL DEFAULT 5 del_offset
6: 0000000000000000 0 SECTION LOCAL DEFAULT 6 .text.target_func
7: 0000000000000000 0 SECTION LOCAL DEFAULT 8 .text.unchanged_func
8: 0000000000000000 0 SECTION LOCAL DEFAULT 9 .debug_info
9: 0000000000000000 0 SECTION LOCAL DEFAULT 11 .debug_abbrev
10: 0000000000000000 0 SECTION LOCAL DEFAULT 14 .debug_rnglists
11: 0000000000000000 0 SECTION LOCAL DEFAULT 16 .debug_line
12: 0000000000000000 0 SECTION LOCAL DEFAULT 18 .debug_str
13: 0000000000000000 0 SECTION LOCAL DEFAULT 19 .debug_line_str
14: 0000000000000000 53 FUNC GLOBAL DEFAULT 6 target_func
15: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND host_value_ptr
16: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND host_add
17: 0000000000000000 18 FUNC GLOBAL DEFAULT 8 unchanged_func

可执行文件.

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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
$ readelf -Ws mini-diff

Symbol table '.dynsym' contains 30 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND free@GLIBC_2.2.5 (2)
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND putchar@GLIBC_2.2.5 (2)
3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.34 (3)
4: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTable
5: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5 (2)
6: 0000000000000000 0 FUNC GLOBAL DEFAULT UND fclose@GLIBC_2.2.5 (2)
7: 0000000000000000 0 FUNC GLOBAL DEFAULT UND strlen@GLIBC_2.2.5 (2)
8: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __stack_chk_fail@GLIBC_2.4 (4)
9: 0000000000000000 0 FUNC GLOBAL DEFAULT UND mmap@GLIBC_2.2.5 (2)
10: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@GLIBC_2.2.5 (2)
11: 0000000000000000 0 FUNC GLOBAL DEFAULT UND memset@GLIBC_2.2.5 (2)
12: 0000000000000000 0 FUNC GLOBAL DEFAULT UND close@GLIBC_2.2.5 (2)
13: 0000000000000000 0 FUNC GLOBAL DEFAULT UND memcmp@GLIBC_2.2.5 (2)
14: 0000000000000000 0 FUNC GLOBAL DEFAULT UND calloc@GLIBC_2.2.5 (2)
15: 0000000000000000 0 FUNC GLOBAL DEFAULT UND strcmp@GLIBC_2.2.5 (2)
16: 0000000000000000 0 FUNC GLOBAL DEFAULT UND fprintf@GLIBC_2.2.5 (2)
17: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
18: 0000000000000000 0 FUNC GLOBAL DEFAULT UND memcpy@GLIBC_2.14 (5)
19: 0000000000000000 0 FUNC GLOBAL DEFAULT UND malloc@GLIBC_2.2.5 (2)
20: 0000000000000000 0 FUNC GLOBAL DEFAULT UND realloc@GLIBC_2.2.5 (2)
21: 0000000000000000 0 FUNC GLOBAL DEFAULT UND munmap@GLIBC_2.2.5 (2)
22: 0000000000000000 0 FUNC GLOBAL DEFAULT UND open@GLIBC_2.2.5 (2)
23: 0000000000000000 0 FUNC GLOBAL DEFAULT UND fopen@GLIBC_2.2.5 (2)
24: 0000000000000000 0 FUNC GLOBAL DEFAULT UND perror@GLIBC_2.2.5 (2)
25: 0000000000000000 0 FUNC GLOBAL DEFAULT UND fwrite@GLIBC_2.2.5 (2)
26: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
27: 0000000000000000 0 FUNC GLOBAL DEFAULT UND fstat@GLIBC_2.33 (6)
28: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5 (2)
29: 0000000000008020 8 OBJECT GLOBAL DEFAULT 26 stderr@GLIBC_2.2.5 (2)

Symbol table '.symtab' contains 98 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS Scrt1.o
2: 000000000000038c 32 OBJECT LOCAL DEFAULT 4 __abi_tag
3: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c
4: 0000000000001350 0 FUNC LOCAL DEFAULT 16 deregister_tm_clones
5: 0000000000001380 0 FUNC LOCAL DEFAULT 16 register_tm_clones
6: 00000000000013c0 0 FUNC LOCAL DEFAULT 16 __do_global_dtors_aux
7: 0000000000008028 1 OBJECT LOCAL DEFAULT 26 completed.0
8: 0000000000007d10 0 OBJECT LOCAL DEFAULT 22 __do_global_dtors_aux_fini_array_entry
9: 0000000000001400 0 FUNC LOCAL DEFAULT 16 frame_dummy
10: 0000000000007d08 0 OBJECT LOCAL DEFAULT 21 __frame_dummy_init_array_entry
11: 0000000000000000 0 FILE LOCAL DEFAULT ABS mini_diff.c
12: 0000000000001409 329 FUNC LOCAL DEFAULT 16 map_file
13: 0000000000001552 70 FUNC LOCAL DEFAULT 16 unmap_file
14: 0000000000001598 540 FUNC LOCAL DEFAULT 16 open_elf_object
15: 00000000000017b4 35 FUNC LOCAL DEFAULT 16 close_elf_object
16: 00000000000017d7 82 FUNC LOCAL DEFAULT 16 section_name
17: 0000000000001829 37 FUNC LOCAL DEFAULT 16 symbol_name
18: 000000000000184e 109 FUNC LOCAL DEFAULT 16 is_defined_function
19: 00000000000018bb 155 FUNC LOCAL DEFAULT 16 find_function_symbol
20: 0000000000001956 144 FUNC LOCAL DEFAULT 16 find_primary_symbol_in_section
21: 00000000000019e6 184 FUNC LOCAL DEFAULT 16 get_symbol_bytes
22: 0000000000001a9e 115 FUNC LOCAL DEFAULT 16 find_rela_for_section
23: 0000000000001b11 277 FUNC LOCAL DEFAULT 16 dep_list_push_unique
24: 0000000000001c26 34 FUNC LOCAL DEFAULT 16 dep_list_destroy
25: 0000000000001c48 290 FUNC LOCAL DEFAULT 16 string_list_push_unique
26: 0000000000001d6a 34 FUNC LOCAL DEFAULT 16 string_list_destroy
27: 0000000000001d8c 56 FUNC LOCAL DEFAULT 16 align_up
28: 0000000000001dc4 117 FUNC LOCAL DEFAULT 16 string_buffer_init
29: 0000000000001e39 34 FUNC LOCAL DEFAULT 16 string_buffer_destroy
30: 0000000000001e5b 264 FUNC LOCAL DEFAULT 16 string_buffer_add
31: 0000000000001f63 746 FUNC LOCAL DEFAULT 16 collect_included_sections
32: 000000000000224d 291 FUNC LOCAL DEFAULT 16 collect_included_symbols
33: 0000000000002370 625 FUNC LOCAL DEFAULT 16 compose_output_sections
34: 00000000000025e1 671 FUNC LOCAL DEFAULT 16 append_output_symbol
35: 0000000000002880 629 FUNC LOCAL DEFAULT 16 build_output_symtab
36: 0000000000002af5 126 FUNC LOCAL DEFAULT 16 write_padding
37: 0000000000006610 16 OBJECT LOCAL DEFAULT 18 zero.0
38: 0000000000002b73 106 FUNC LOCAL DEFAULT 16 destroy_output_sections
39: 0000000000002bdd 1091 FUNC LOCAL DEFAULT 16 compose_output_relocations
40: 0000000000003020 359 FUNC LOCAL DEFAULT 16 collect_function_deps
41: 0000000000003187 433 FUNC LOCAL DEFAULT 16 print_function_status
42: 0000000000003338 557 FUNC LOCAL DEFAULT 16 compare_functions
43: 0000000000003565 198 FUNC LOCAL DEFAULT 16 compare_symbol_payload
44: 000000000000362b 433 FUNC LOCAL DEFAULT 16 print_dependency
45: 00000000000037dc 102 FUNC LOCAL DEFAULT 16 print_section_include
46: 0000000000003842 113 FUNC LOCAL DEFAULT 16 print_symbol_and_section_include
47: 00000000000038b3 494 FUNC LOCAL DEFAULT 16 emit_function_plan_item
48: 0000000000003aa1 504 FUNC LOCAL DEFAULT 16 collect_patch_roots
49: 0000000000003c99 4139 FUNC LOCAL DEFAULT 16 generate_patch_elf
50: 0000000000004cc4 800 FUNC LOCAL DEFAULT 16 emit_patch_plan
51: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c
52: 0000000000006d14 0 OBJECT LOCAL DEFAULT 20 __FRAME_END__
53: 0000000000000000 0 FILE LOCAL DEFAULT ABS
54: 0000000000007d18 0 OBJECT LOCAL DEFAULT 23 _DYNAMIC
55: 0000000000006620 0 NOTYPE LOCAL DEFAULT 19 __GNU_EH_FRAME_HDR
56: 0000000000007f08 0 OBJECT LOCAL DEFAULT 24 _GLOBAL_OFFSET_TABLE_
57: 0000000000000000 0 FUNC GLOBAL DEFAULT UND free@GLIBC_2.2.5
58: 0000000000000000 0 FUNC GLOBAL DEFAULT UND putchar@GLIBC_2.2.5
59: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.34
60: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTable
61: 0000000000008000 0 NOTYPE WEAK DEFAULT 25 data_start
62: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5
63: 0000000000008010 0 NOTYPE GLOBAL DEFAULT 25 _edata
64: 0000000000000000 0 FUNC GLOBAL DEFAULT UND fclose@GLIBC_2.2.5
65: 0000000000005208 0 FUNC GLOBAL HIDDEN 17 _fini
66: 0000000000000000 0 FUNC GLOBAL DEFAULT UND strlen@GLIBC_2.2.5
67: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __stack_chk_fail@GLIBC_2.4
68: 0000000000000000 0 FUNC GLOBAL DEFAULT UND mmap@GLIBC_2.2.5
69: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@GLIBC_2.2.5
70: 0000000000000000 0 FUNC GLOBAL DEFAULT UND memset@GLIBC_2.2.5
71: 0000000000000000 0 FUNC GLOBAL DEFAULT UND close@GLIBC_2.2.5
72: 0000000000000000 0 FUNC GLOBAL DEFAULT UND memcmp@GLIBC_2.2.5
73: 0000000000000000 0 FUNC GLOBAL DEFAULT UND calloc@GLIBC_2.2.5
74: 0000000000008000 0 NOTYPE GLOBAL DEFAULT 25 __data_start
75: 0000000000000000 0 FUNC GLOBAL DEFAULT UND strcmp@GLIBC_2.2.5
76: 0000000000000000 0 FUNC GLOBAL DEFAULT UND fprintf@GLIBC_2.2.5
77: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
78: 0000000000008008 0 OBJECT GLOBAL HIDDEN 25 __dso_handle
79: 0000000000000000 0 FUNC GLOBAL DEFAULT UND memcpy@GLIBC_2.14
80: 0000000000006000 4 OBJECT GLOBAL DEFAULT 18 _IO_stdin_used
81: 0000000000000000 0 FUNC GLOBAL DEFAULT UND malloc@GLIBC_2.2.5
82: 0000000000008030 0 NOTYPE GLOBAL DEFAULT 26 _end
83: 0000000000001320 38 FUNC GLOBAL DEFAULT 16 _start
84: 0000000000000000 0 FUNC GLOBAL DEFAULT UND realloc@GLIBC_2.2.5
85: 0000000000008020 0 NOTYPE GLOBAL DEFAULT 26 __bss_start
86: 0000000000000000 0 FUNC GLOBAL DEFAULT UND munmap@GLIBC_2.2.5
87: 0000000000004fe4 546 FUNC GLOBAL DEFAULT 16 main
88: 0000000000000000 0 FUNC GLOBAL DEFAULT UND open@GLIBC_2.2.5
89: 0000000000000000 0 FUNC GLOBAL DEFAULT UND fopen@GLIBC_2.2.5
90: 0000000000000000 0 FUNC GLOBAL DEFAULT UND perror@GLIBC_2.2.5
91: 0000000000000000 0 FUNC GLOBAL DEFAULT UND fwrite@GLIBC_2.2.5
92: 0000000000008010 0 OBJECT GLOBAL HIDDEN 25 __TMC_END__
93: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
94: 0000000000000000 0 FUNC GLOBAL DEFAULT UND fstat@GLIBC_2.33
95: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5
96: 0000000000001000 0 FUNC GLOBAL HIDDEN 12 _init
97: 0000000000008020 8 OBJECT GLOBAL DEFAULT 26 stderr@GLIBC_2.2.5

3.2 对应的 C 结构

1
2
3
4
5
6
7
8
typedef struct {
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;

3.3 readelf -Ws 各表项含义

3.3.1 Num

Num 不直接对应 Elf64_Sym 结构体里的某个字段.

它表示“当前符号在当前符号表中的表项序号”, 也就是数组下标.

可以把符号表理解成:

1
Elf64_Sym symtab[];

那么:

  • Num = 0 就是 symtab[0]
  • Num = 1 就是 symtab[1]
  • Num = 14 就是 symtab[14]

它的计算方式很直接:

  • 从当前符号表起始位置开始
  • Elf64_Sym 的大小逐项编号
  • 第几个表项, Num 就是多少

这一点很重要, 因为 ELF 里很多地方都会通过“符号表索引”引用某个符号.

例如 relocation 条目里的符号引用, 本质上就是在说:

  • “我依赖的是符号表第 N 项”

在我的实验里:

  • readelf -Ws diff_old.o.symtab 有 18 项, 所以 Num 范围是 0 ~ 17
  • readelf -Ws mini-diff.dynsym 有 30 项, 所以它自己的 Num 范围是 0 ~ 29
  • readelf -Ws mini-diff.symtab 有 98 项, 所以它自己的 Num 范围是 0 ~ 97

要注意:

  • Num 是“每张符号表内部单独编号”的
  • .dynsym.symtab 各自从 0 开始重新编号
  • 不能把不同符号表里的 Num 直接混为同一个全局编号

符号表第 0 项通常还是一个保留空项, 很多工具链都会这样组织. 例如:

1
0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND

这个空项存在的意义和 section header 第 0 项有点类似:

  • 作为保留值
  • 给“未定义/空引用”留出一个安全索引

3.3.2 Value

对应 st_value, 符号值.

这个字段的含义依赖于符号类型和文件类型.

最常见的理解方式是:

  • 对已定义函数/变量
    • 它通常表示符号的地址, 或者相对所属 section 的偏移
  • 对未定义符号 UND
    • 它通常是 0
  • 对 section 符号 SECTION
    • 它通常表示该 section 内的偏移, 常见为 0

.o 文件里:

  • st_value 往往不是最终进程虚拟地址
  • 而更像“相对 section 起点的偏移”

例如在我的实验里:

  • diff_old.o 里的 target_funcValue = 0
    • 因为它所在的 .text.target_func 是单独 section, 函数正好从该 section 的起始位置开始
  • diff_old.o 里的 host_addUND
    • 所以 Value = 0

在最终可执行文件里:

  • st_value 通常就是链接后的映像虚拟地址

例如在我的实验里:

  • mini-diff_start0x1320
  • main0x4fe4

这和前面 sh_addr, e_entry 的分析是能对上的.

3.3.3 Size

对应 st_size, 符号的大小.

这个字段说明一个符号覆盖多少字节.

最常见的情况:

  • 对函数符号 FUNC
    • 表示函数机器码长度
  • 对对象符号 OBJECT
    • 表示对象大小
  • FILE, SECTION、未定义符号
    • 经常是 0

例如在我的实验里:

  • diff_old.o 中:
    • local_offsetSize = 19
    • target_funcSize = 53
    • unchanged_funcSize = 18
  • mini-diff 中:
    • map_fileSize = 329
    • open_elf_objectSize = 540

这和函数实际反汇编出来的指令长度是一致的.

3.3.4 Type

st_info 拆解出来, 用宏 ELF64_ST_TYPE(st_info) 来提取.

常见的数值有:

  • STT_NOTYPE
  • STT_OBJECT
  • STT_FUNC
  • STT_SECTION
  • STT_FILE

这些类型可以这样理解:

  • STT_NOTYPE: 没有给出更具体的类型, 常见于未定义外部符号, 尤其是某些目标文件里的外部函数/变量引用.
  • STT_OBJECT: 普通数据对象, 例如全局变量、静态对象、数组、运行时对象槽位.
  • STT_FUNC: 函数符号, 表示这是可执行代码入口.
  • STT_SECTION: section 符号, 它不是普通 C 源码层面的函数或变量, 而是“代表某个 section 本身”的符号, 在 relocation、调试信息、链接内部处理中很常见.
  • STT_FILE: 文件符号, 用来记录编译单元对应的源文件名, 一般 Ndx = ABS

除了这些常见类型, ELF 里还可能出现:

  • STT_COMMON: 历史上用于 common symbol
  • STT_TLS: 线程局部存储变量

但在当前两个样例里没有重点出现.

3.3.5 Bind

st_info 拆解出来, 用宏 ELF64_ST_BIND(st_info) 来提取.

st_info 把两部分塞在一个字节里:

  • 高 4 位: Bind
  • 低 4 位: Type

常见的数值有:

  • STB_LOCAL
  • STB_GLOBAL
  • STB_WEAK

这些绑定属性可以这样理解:

  • STB_LOCAL: 只在当前目标文件内部可见, 不会参与跨文件符号导出, 常见于 static 函数、局部 section 符号、文件符号
  • STB_GLOBAL: 全局可见, 可以被其他目标文件引用, 也可以去解析其他文件里的同名未定义符号
  • STB_WEAK: 弱符号, 如果有强符号同名定义, 强符号优先, 如果没有强符号, 弱符号也可以工作, 常见于某些运行库辅助符号

简单理解:

  • LOCAL 像“仅本文件可见”
  • GLOBAL 像“正式对外公开”
  • WEAK 像“优先级较低的公开定义/引用”

3.3.6 Vis

st_other 拆解出来, 用宏 ELF64_ST_VISIBILITY(st_other) 来提取.

这个字段表示符号可见性, 也就是“即便它是全局绑定, 外部到底能不能正常看到它”.

最常见的值有:

  • STV_DEFAULT: 默认可见性, 最常见
  • STV_HIDDEN: 符号对本对象内部可见, 但不会正常对外导出
  • STV_PROTECTED: 对外可见, 但本对象内部引用时具有更强的本地绑定语义
  • STV_INTERNAL: 处理器/ABI 特定的内部可见性, 比较少见

在我的实验里大多数符号都显示为 DEFAULT, 这也是最常见的情况.

3.3.7 Ndx

对应 st_shndx, 表示属于的 section.

这个字段表示“该符号归属于哪个 section”.

如果是普通已定义符号:

  • Ndx 通常是一个 section 索引号

例如在我的实验里:

  • diff_old.o 里的 target_funcNdx = 6
    • 对应 .text.target_func
  • diff_old.o 里的 unchanged_funcNdx = 8
    • 对应 .text.unchanged_func
  • mini-diff 里的 mainNdx = 16
    • 对应 .text

还有一些特殊取值:

  • UND
    • undefined
    • 表示符号在当前文件里没有定义, 需要外部提供
    • 例如:
      • diff_old.ohost_add
      • mini-diff 里的 printf@GLIBC_2.2.5
  • ABS
    • absolute
    • 表示这是绝对符号, 不依附于某个普通 section
    • 常见于 FILE 符号
    • 例如:
      • diff_old.c
      • mini_diff.c

所以 Ndx 一眼就可以先判断:

  • 这个符号是否已定义
  • 如果已定义, 它属于哪个 section
  • 如果未定义, 它是不是等待外部解析

3.3.8 Name

对应 st_name, 相对字符串表的偏移,要去 .strtab.dynstr 查.

和前面 sh_name 的逻辑类似, st_name 也不是直接存字符串.

它存的是:

  • 相对字符串表起始位置的字节偏移

但这里使用哪张字符串表, 要看当前是哪张符号表:

  • 如果当前是 .symtab
    • 就去 .strtab
  • 如果当前是 .dynsym
    • 就去 .dynstr

例如在我的实验里:

  • diff_old.o.symtab
    • target_func
    • local_offset
    • host_add
    • 这些名字都来自 .strtab
  • mini-diff.dynsym
    • printf@GLIBC_2.2.5
    • malloc@GLIBC_2.2.5
    • __libc_start_main@GLIBC_2.34
    • 这些名字都来自 .dynstr

可以把它理解成:

  • Elf64_Sym 里存“名字指针的偏移”
  • 真正字符串统一保存在字符串表里

4 relocations

4.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
$ readelf -Wr diff_old.o

Relocation section '.rela.text.target_func' at offset 0x868 contains 3 entries:
Offset Info Type Symbol's Value Symbol's Name + Addend
0000000000000013 0000000f00000002 R_X86_64_PC32 0000000000000000 host_value_ptr - 4
000000000000001f 0000000200000002 R_X86_64_PC32 0000000000000000 .text.local_offset - 4
0000000000000028 0000001000000004 R_X86_64_PLT32 0000000000000000 host_add - 4

Relocation section '.rela.debug_info' at offset 0x8b0 contains 16 entries:
Offset Info Type Symbol's Value Symbol's Name + Addend
0000000000000008 000000090000000a R_X86_64_32 0000000000000000 .debug_abbrev + 0
000000000000000d 0000000c0000000a R_X86_64_32 0000000000000000 .debug_str + 23
0000000000000012 0000000d0000000a R_X86_64_32 0000000000000000 .debug_line_str + 36
0000000000000016 0000000d0000000a R_X86_64_32 0000000000000000 .debug_line_str + 0
000000000000001a 0000000a0000000a R_X86_64_32 0000000000000000 .debug_rnglists + c
0000000000000026 0000000b0000000a R_X86_64_32 0000000000000000 .debug_line + 0
000000000000002b 0000000c0000000a R_X86_64_32 0000000000000000 .debug_str + ee
0000000000000044 0000000c0000000a R_X86_64_32 0000000000000000 .debug_str + 1a
000000000000005f 0000000c0000000a R_X86_64_32 0000000000000000 .debug_str + b
000000000000006a 0000000700000001 R_X86_64_64 0000000000000000 .text.unchanged_func + 0
000000000000008e 0000000c0000000a R_X86_64_32 0000000000000000 .debug_str + e2
0000000000000099 0000000600000001 R_X86_64_64 0000000000000000 .text.target_func + 0
00000000000000bd 0000000c0000000a R_X86_64_32 0000000000000000 .debug_str + 0
00000000000000c8 0000000400000001 R_X86_64_64 0000000000000000 .text.del_offset + 0
00000000000000ec 0000000c0000000a R_X86_64_32 0000000000000000 .debug_str + fd
00000000000000f7 0000000200000001 R_X86_64_64 0000000000000000 .text.local_offset + 0

Relocation section '.rela.debug_aranges' at offset 0xa30 contains 5 entries:
Offset Info Type Symbol's Value Symbol's Name + Addend
0000000000000006 000000080000000a R_X86_64_32 0000000000000000 .debug_info + 0
0000000000000010 0000000200000001 R_X86_64_64 0000000000000000 .text.local_offset + 0
0000000000000020 0000000400000001 R_X86_64_64 0000000000000000 .text.del_offset + 0
0000000000000030 0000000600000001 R_X86_64_64 0000000000000000 .text.target_func + 0
0000000000000040 0000000700000001 R_X86_64_64 0000000000000000 .text.unchanged_func + 0

Relocation section '.rela.debug_rnglists' at offset 0xaa8 contains 4 entries:
Offset Info Type Symbol's Value Symbol's Name + Addend
000000000000000d 0000000200000001 R_X86_64_64 0000000000000000 .text.local_offset + 0
0000000000000017 0000000400000001 R_X86_64_64 0000000000000000 .text.del_offset + 0
0000000000000021 0000000600000001 R_X86_64_64 0000000000000000 .text.target_func + 0
000000000000002b 0000000700000001 R_X86_64_64 0000000000000000 .text.unchanged_func + 0

Relocation section '.rela.debug_line' at offset 0xb08 contains 7 entries:
Offset Info Type Symbol's Value Symbol's Name + Addend
0000000000000022 0000000d0000000a R_X86_64_32 0000000000000000 .debug_line_str + 41
000000000000002c 0000000d0000000a R_X86_64_32 0000000000000000 .debug_line_str + 77
0000000000000031 0000000d0000000a R_X86_64_32 0000000000000000 .debug_line_str + 82
000000000000003b 0000000200000001 R_X86_64_64 0000000000000000 .text.local_offset + 0
0000000000000054 0000000400000001 R_X86_64_64 0000000000000000 .text.del_offset + 0
000000000000006f 0000000600000001 R_X86_64_64 0000000000000000 .text.target_func + 0
0000000000000099 0000000700000001 R_X86_64_64 0000000000000000 .text.unchanged_func + 0

Relocation section '.rela.eh_frame' at offset 0xbb0 contains 4 entries:
Offset Info Type Symbol's Value Symbol's Name + Addend
0000000000000020 0000000200000002 R_X86_64_PC32 0000000000000000 .text.local_offset + 0
0000000000000040 0000000400000002 R_X86_64_PC32 0000000000000000 .text.del_offset + 0
0000000000000060 0000000600000002 R_X86_64_PC32 0000000000000000 .text.target_func + 0
0000000000000080 0000000700000002 R_X86_64_PC32 0000000000000000 .text.unchanged_func + 0

可执行文件.

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
$ readelf -Wr mini-diff 

Relocation section '.rela.dyn' at offset 0x888 contains 9 entries:
Offset Info Type Symbol's Value Symbol's Name + Addend
0000000000007d08 0000000000000008 R_X86_64_RELATIVE 1400
0000000000007d10 0000000000000008 R_X86_64_RELATIVE 13c0
0000000000008008 0000000000000008 R_X86_64_RELATIVE 8008
0000000000007fd8 0000000300000006 R_X86_64_GLOB_DAT 0000000000000000 __libc_start_main@GLIBC_2.34 + 0
0000000000007fe0 0000000400000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_deregisterTMCloneTable + 0
0000000000007fe8 0000001100000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
0000000000007ff0 0000001a00000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_registerTMCloneTable + 0
0000000000007ff8 0000001c00000006 R_X86_64_GLOB_DAT 0000000000000000 __cxa_finalize@GLIBC_2.2.5 + 0
0000000000008020 0000001d00000005 R_X86_64_COPY 0000000000008020 stderr@GLIBC_2.2.5 + 0

Relocation section '.rela.plt' at offset 0x960 contains 23 entries:
Offset Info Type Symbol's Value Symbol's Name + Addend
0000000000007f20 0000000100000007 R_X86_64_JUMP_SLOT 0000000000000000 free@GLIBC_2.2.5 + 0
0000000000007f28 0000000200000007 R_X86_64_JUMP_SLOT 0000000000000000 putchar@GLIBC_2.2.5 + 0
0000000000007f30 0000000500000007 R_X86_64_JUMP_SLOT 0000000000000000 puts@GLIBC_2.2.5 + 0
0000000000007f38 0000000600000007 R_X86_64_JUMP_SLOT 0000000000000000 fclose@GLIBC_2.2.5 + 0
0000000000007f40 0000000700000007 R_X86_64_JUMP_SLOT 0000000000000000 strlen@GLIBC_2.2.5 + 0
0000000000007f48 0000000800000007 R_X86_64_JUMP_SLOT 0000000000000000 __stack_chk_fail@GLIBC_2.4 + 0
0000000000007f50 0000000900000007 R_X86_64_JUMP_SLOT 0000000000000000 mmap@GLIBC_2.2.5 + 0
0000000000007f58 0000000a00000007 R_X86_64_JUMP_SLOT 0000000000000000 printf@GLIBC_2.2.5 + 0
0000000000007f60 0000000b00000007 R_X86_64_JUMP_SLOT 0000000000000000 memset@GLIBC_2.2.5 + 0
0000000000007f68 0000000c00000007 R_X86_64_JUMP_SLOT 0000000000000000 close@GLIBC_2.2.5 + 0
0000000000007f70 0000000d00000007 R_X86_64_JUMP_SLOT 0000000000000000 memcmp@GLIBC_2.2.5 + 0
0000000000007f78 0000000e00000007 R_X86_64_JUMP_SLOT 0000000000000000 calloc@GLIBC_2.2.5 + 0
0000000000007f80 0000000f00000007 R_X86_64_JUMP_SLOT 0000000000000000 strcmp@GLIBC_2.2.5 + 0
0000000000007f88 0000001000000007 R_X86_64_JUMP_SLOT 0000000000000000 fprintf@GLIBC_2.2.5 + 0
0000000000007f90 0000001200000007 R_X86_64_JUMP_SLOT 0000000000000000 memcpy@GLIBC_2.14 + 0
0000000000007f98 0000001300000007 R_X86_64_JUMP_SLOT 0000000000000000 malloc@GLIBC_2.2.5 + 0
0000000000007fa0 0000001400000007 R_X86_64_JUMP_SLOT 0000000000000000 realloc@GLIBC_2.2.5 + 0
0000000000007fa8 0000001500000007 R_X86_64_JUMP_SLOT 0000000000000000 munmap@GLIBC_2.2.5 + 0
0000000000007fb0 0000001600000007 R_X86_64_JUMP_SLOT 0000000000000000 open@GLIBC_2.2.5 + 0
0000000000007fb8 0000001700000007 R_X86_64_JUMP_SLOT 0000000000000000 fopen@GLIBC_2.2.5 + 0
0000000000007fc0 0000001800000007 R_X86_64_JUMP_SLOT 0000000000000000 perror@GLIBC_2.2.5 + 0
0000000000007fc8 0000001900000007 R_X86_64_JUMP_SLOT 0000000000000000 fwrite@GLIBC_2.2.5 + 0
0000000000007fd0 0000001b00000007 R_X86_64_JUMP_SLOT 0000000000000000 fstat@GLIBC_2.33 + 0

4.2 C 结构体

1
2
3
4
5
typedef struct {
Elf64_Addr r_offset;
Elf64_Xword r_info;
Elf64_Sxword r_addend;
} Elf64_Rela;

4.3 readelf -Wr 表项解析

readelf -Wr 打印的每一行 relocation, 本质上都来自一个 Elf64_Rela.

和前面 section header、symbol table 的思路一样, readelf 只是把结构体字段再加上一些辅助解码结果打印出来.

4.3.1 Offset

对应 r_offset.

这个字段表示: “要修正的位置”在目标 section 中的偏移

注意这里不是“符号地址”, 也不是“文件绝对偏移”, 而是: 相对于被重定位 section 起始位置的偏移

在我的实验里:

1
2
3
4
Relocation section '.rela.text.target_func' ...
000000000013 ... host_value_ptr - 4
00000000001f ... .text.local_offset - 4
000000000028 ... host_add - 4

这里的 Offset 分别是:

  • 0x13
  • 0x1f
  • 0x28

它们的含义就是:

  • 要修正的地方位于 .text.target_func 内部的第 0x130x1f0x28 字节处

如果只写到这里, 还是会有点抽象.
可以直接看 examples/mini-upatch/labs/relocations/r_offset.md, 里面专门做了一个最小实验, 把:

  • .text.target_funcsh_offset = 0x66
  • relocation 里的 r_offset = 0x13 / 0x1f / 0x28
  • hexdump 里的真实文件字节位置
  • objdump 里的具体指令字段

四者直接对起来了.

在那个实验里可以直接验证:

  • 0x66 + 0x13 = 0x79
  • 0x66 + 0x1f = 0x85
  • 0x66 + 0x28 = 0x8e

也就是说:

  • r_offset 本身是“相对 section 的偏移”
  • 真正落到文件里的位置, 还要再加上目标 section 的 sh_offset

4.3.2 Info

对应 r_info.

这个字段本身不是单一含义, 而是把两部分信息打包在一起:

  • 高位部分: 符号表索引
  • 低位部分: relocation 类型

在 ELF64 里通常用这两个宏拆:

1
2
ELF64_R_SYM(r_info)
ELF64_R_TYPE(r_info)

也就是说:

  • Info 本质上是在同时表达:
    • “这条 relocation 依赖符号表里的哪一个符号”
    • “要按哪种 relocation 规则计算”

例如:

1
000000000013  000f00000002 ...

可以拆成:

  • 符号索引 = 0x000f = 15
  • relocation 类型 = 0x0002 = R_X86_64_PC32

再去 diff_old.o.symtab 看第 15 项:

1
15: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND host_value_ptr

这就对上了:

  • .rela.text.target_func 第 1 条 relocation, 引用的是 .symtab[15], 也就是 host_value_ptr.

4.3.3 Type

这是从 r_info 拆出来的 relocation 类型, 一般通过:

1
ELF64_R_TYPE(r_info)

当前样例里最重要的几种类型有:

  • R_X86_64_PC32

    • 32 位 PC-relative relocation
    • 常用于 RIP 相对寻址, 或某些近距离 call/jmp
    • 最终写入的是“目标地址相对当前位置的位移”
  • R_X86_64_PLT32

    • 也是 32 位 PC-relative 风格
    • 但语义偏向函数调用, 链接器可能通过 PLT 处理
    • 常见于对外部函数的 call
  • R_X86_64_32

    • 写入 32 位绝对值
    • 在调试信息里很常见
  • R_X86_64_64

    • 写入 64 位绝对值
    • 在调试信息和某些数据引用里常见

当前最值得关注的是 .rela.text.target_func 这三条:

  • R_X86_64_PC32 host_value_ptr - 4
  • R_X86_64_PC32 .text.local_offset - 4
  • R_X86_64_PLT32 host_add - 4

它们分别对应:

  • 对外部变量 host_value_ptr 的 RIP 相对访问
  • 对本地函数 local_offsetcall
  • 对外部函数 host_addcall

更多应该查看对应架构的 ABI 手册的对应部分.

4.3.4 Sym.Value

这是 readelf 为了方便阅读额外打印出来的“关联符号值”.

它不是 Elf64_Rela 里的字段, 而是:

  • 先通过 ELF64_R_SYM(r_info) 找到关联的符号表项
  • 再把那个符号的 st_value 打印出来

也就是说, Sym.Value 本质上来自:

  • 被引用符号的 Elf64_Sym.st_value

例如在我的实验里:

1
000000000013 ... host_value_ptr - 4

其中 Sym.Value = 0, 是因为:

  • host_value_ptrdiff_old.o 里是 UND
  • 它本地还没有定义, st_value 自然是 0

而对 .text.local_offset 这种 section/local symbol 来说, 它也常常显示为 0, 因为它在自己的 section 里就是从偏移 0 开始.

4.3.5 Sym. Name + Addend

这部分也是 readelf 为了可读性拼出来的结果:

  • 关联符号名
  • 加号或减号后的 addend

其中:

  • 符号名来自 ELF64_R_SYM(r_info) 找到的那个符号
  • addend 对应 r_addend

例如:

1
2
3
host_value_ptr - 4
.text.local_offset - 4
host_add - 4

这里的 - 4 就来自 r_addend = -4.

4.3.6 Addend

对应 r_addend.

这是 RELAREL 的核心区别之一:

  • REL
    • addend 不单独存字段, 要从目标位置原始内容里取
  • RELA
    • addend 单独存放在结构体里

当前 x86-64 样例主要使用 RELA, 所以 r_addend 是显式可见的.

.rela.text.target_func 里三条都是 -4, 原因和 x86-64 的 PC-relative 编码规则有关.

像:

1
e8 xx xx xx xx

这样的 call rel32 指令, 位移是相对于“下一条指令地址”计算的, 而 relocation 位置往往指向这 4 字节立即数字段本身.
因此在很多 x86-64 PC-relative relocation 里, 都会看到一个 -4 的 addend, 用来把计算基准调整到正确位置.

可以把它先粗略理解成:

  • “类型规则决定怎么算”
  • “符号提供目标基准值”
  • “addend 再做最后的常数修正”

4.3.7 一条 relocation 是怎么工作的

以这条为例:

1
000000000028  001000000004 R_X86_64_PLT32    0000000000000000 host_add - 4

它可以拆成:

  • Offset = 0x28
    • 要修正 .text.target_func 内第 0x28 字节开始的字段
  • Info
    • 符号索引 = 0x10 = 16
    • 类型 = 0x04 = R_X86_64_PLT32
  • Sym.Name = host_add
    • 说明目标符号是 host_add
  • Addend = -4
    • 这是额外常数修正

而前面的反汇编里:

1
27: e8 00 00 00 00

正好对应:

  • opcode e8
  • 后面 4 字节立即数暂时全是 0
  • 链接器后续会根据 host_add 的实际位置, 按 R_X86_64_PLT32 规则把这 4 字节改成正确位移

4.3.8 为什么 .o 里看到很多 0

这正是 relocation 存在的原因.

.o 文件里:

  • 编译器已经知道“这里将来要引用谁”
  • 但它还不知道“最终链接后那个符号到底在哪里”

所以它先做两件事:

  1. 在指令或数据里留一个占位值
  2. 生成 relocation 表项, 记录后续如何修正

因此像下面这种机器码:

1
2
48 8b 05 00 00 00 00
e8 00 00 00 00

看起来像“全是 0”, 实际上不是错误, 而是“等待链接器填空”.

5 ELF program headers

5.1 实验实例

可重定位文件.

1
2
3
$ readelf -Wl diff_old.o

There are no program headers in this file.

可执行文件.

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
$ readelf -Wl mini-diff 

Elf file type is DYN (Position-Independent Executable file)
Entry point 0x1320
There are 13 program headers, starting at offset 64

Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000040 0x0000000000000040 0x0000000000000040 0x0002d8 0x0002d8 R 0x8
INTERP 0x000318 0x0000000000000318 0x0000000000000318 0x00001c 0x00001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x000000 0x0000000000000000 0x0000000000000000 0x000b88 0x000b88 R 0x1000
LOAD 0x001000 0x0000000000001000 0x0000000000001000 0x004215 0x004215 R E 0x1000
LOAD 0x006000 0x0000000000006000 0x0000000000006000 0x000d18 0x000d18 R 0x1000
LOAD 0x007d08 0x0000000000007d08 0x0000000000007d08 0x000308 0x000328 RW 0x1000
DYNAMIC 0x007d18 0x0000000000007d18 0x0000000000007d18 0x0001f0 0x0001f0 RW 0x8
NOTE 0x000338 0x0000000000000338 0x0000000000000338 0x000030 0x000030 R 0x8
NOTE 0x000368 0x0000000000000368 0x0000000000000368 0x000044 0x000044 R 0x4
GNU_PROPERTY 0x000338 0x0000000000000338 0x0000000000000338 0x000030 0x000030 R 0x8
GNU_EH_FRAME 0x006620 0x0000000000006620 0x0000000000006620 0x000164 0x000164 R 0x4
GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW 0x10
GNU_RELRO 0x007d08 0x0000000000007d08 0x0000000000007d08 0x0002f8 0x0002f8 R 0x1

Section to Segment mapping:
Segment Sections...
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

5.2 表项解释

readelf -Wl 打印的是 Program Header Table, 也就是程序头表.

和 section header table 不同, program header 主要是给:

  • 内核
  • 动态加载器
  • 运行时装载器

使用的.

如果说:

  • section header 更偏“链接视角”
  • 那 program header 更偏“装载视角”

也就是说, 当一个可执行文件真的要进入内存时, 系统更关心的是 program header, 而不是 section header.

5.3 对应的 C 结构

readelf -Wl 的每一行, 本质上都来自一个 Elf64_Phdr.

1
2
3
4
5
6
7
8
9
10
typedef struct {
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;

readelf -Wl 各列的对应关系如下:

  • Type 对应 p_type
  • Offset 对应 p_offset
  • VirtAddr 对应 p_vaddr
  • PhysAddr 对应 p_paddr
  • FileSiz 对应 p_filesz
  • MemSiz 对应 p_memsz
  • Flg 对应 p_flags
  • Align 对应 p_align

5.4 表示解析

5.4.1 Type

对应 p_type.

这个字段表示 program header 条目的类型, 也就是“这条段描述到底是干什么的”.

在我的实验里最重要的类型有:

  • PT_PHDR
    • 描述 program header table 自己
  • PT_INTERP
    • 指定动态加载器路径
    • 在我的实验里是 /lib64/ld-linux-x86-64.so.2
  • PT_LOAD
    • 最重要, 表示一个真正需要映射到内存里的可装载段
  • PT_DYNAMIC
    • 动态链接器要读取的动态信息
  • PT_NOTE
    • note 信息
  • PT_GNU_PROPERTY
    • GNU property 元数据
  • PT_GNU_EH_FRAME
    • .eh_frame_hdr 相关信息
  • PT_GNU_STACK
    • 栈权限信息
  • PT_GNU_RELRO
    • RELRO 保护范围

其中最关键的是 PT_LOAD, 因为程序真正进入内存, 靠的就是这一类条目.

5.4.2 Offset

对应 p_offset.

它表示:

  • 这个 segment 在文件中的起始偏移

在我的实验里:

  • 可执行代码段:
    • LOAD Offset = 0x001000
  • 只读数据段:
    • LOAD Offset = 0x006000
  • 可写数据段:
    • LOAD Offset = 0x007d08

也就是说, 装载器需要从文件的这些偏移位置开始取字节, 再映射到内存中对应的位置.

5.4.3 VirtAddr

对应 p_vaddr.

它表示:

  • 这个 segment 在进程虚拟地址空间中的映射地址

ET_DYN / PIE 来说, 这里更适合理解成:

  • 相对映像基址的虚拟地址

在我的实验里:

  • 可执行代码段的 VirtAddr = 0x1000
  • 只读数据段的 VirtAddr = 0x6000
  • 可写数据段的 VirtAddr = 0x7d08

运行时真正的绝对地址, 通常还要加上这次装载的基址.

5.4.4 PhysAddr

对应 p_paddr.

它表示物理地址.

在现代 Linux 用户态 ELF 里, 这个字段通常没有实际使用价值, 常常只是和 VirtAddr 一样, 或者被简单保留.

所以分析普通 Linux 可执行文件时, 这个字段一般不重点关注.

5.4.5 FileSiz

对应 p_filesz.

它表示:

  • 这个 segment 在文件里实际占了多少字节

装载器会从文件里读出这么多字节拷进内存.

例如:

  • 代码段 FileSiz = 0x4215
  • 只读段 FileSiz = 0x0d18
  • 可写段 FileSiz = 0x0308

5.4.6 MemSiz

对应 p_memsz.

它表示:

  • 这个 segment 在内存中最终要占多少字节

如果 p_memsz > p_filesz

就说明:

  • 文件里只存了前半部分
  • 剩下的部分进入内存后要补零

这正是 .bss 一类未初始化数据的来源.

在我的实验里, 最后一个可写 LOAD 段:

  • FileSiz = 0x0308
  • MemSiz = 0x0328

多出来的 0x20 字节, 就说明运行时还要额外补零.

5.4.7 Flg

对应 p_flags.

这个字段表示内存保护权限.

常见取值:

  • R: 可读
  • W: 可写
  • E: 可执行

在我的实验里:

  • R E
    • 可执行代码段
  • R
    • 只读段
  • RW
    • 可写数据段

这和 section header 里的 AX / WA 有点像, 但层级不同:

  • section header 的 flag 是 section 属性
  • program header 的 flag 是最终内存映射权限

5.4.8 Align

对应 p_align.

它表示这个 segment 的文件偏移和虚拟地址应该满足的对齐要求.

例如在我的实验里:

  • 主要 LOAD 段都是 0x1000 对齐

这很常见, 因为:

  • Linux 页大小通常就是 0x1000
  • 按页对齐最方便映射

通常装载器要求:

  • p_offset % p_align == p_vaddr % p_align

这样文件布局和内存布局才能正确对应.

5.4.9 Section to Segment mapping 是什么

最后这一块:

1
2
3
Section to Segment mapping:
Segment Sections...
03 .init .plt .plt.got .plt.sec .text .fini

不是 Elf64_Phdr 的字段, 而是 readelf 额外帮我做的整理结果.

它的作用是:

  • 把“逻辑上的 section”
  • 映射到“运行时装载的 segment”

这样我就能同时看到:

  • section 视角:
    • .text
    • .rodata
    • .dynamic
  • segment 视角:
    • 哪些内容一起被映射成 R E
    • 哪些内容一起被映射成 R
    • 哪些内容一起被映射成 RW

5.5 program headers section 和 relocation 是怎么联动起来完成装载的

前面几节其实已经分别拆开讲了三件事:

  • section header 负责描述“文件被组织成哪些逻辑块”
  • relocation 负责描述“哪些字节后面还要被修正”
  • program header 负责描述“运行时到底把哪些内容映射进内存”

如果把这三部分重新串起来, 一个更接近真实 loader 的心智模型是:

  1. 链接器先按 section 组织内容.
  2. 再把需要一起装载的 section 折叠进若干个 PT_LOAD segment.
  3. 程序启动时, 内核和动态加载器先按 program header 把 segment 映射进内存.
  4. 然后动态加载器再通过 PT_DYNAMIC 找到 .dynamic.
  5. 再由 .dynamic 里的指针找到 .rela.dyn.rela.plt.dynsym.dynstr.got.
  6. 最后把 relocation 真正写进已经映射好的那块内存里.

也就是说, section 决定“构建时怎么分块”, program header 决定“运行时怎么装载”, relocation 决定“装载后哪些位置还要修正”.

下面直接用 mini-diff 做一个串联实验.

5.5.1 先看 section 是怎么被折叠进 PT_LOAD

先看 section header:

1
$ readelf -S mini-diff

在我的实验里先记几组关键 section:

1
2
3
4
5
6
7
8
9
[10] .rela.dyn  RELA      Address 0000000000000888 Offset 00000888
[11] .rela.plt RELA Address 0000000000000960 Offset 00000960
[13] .plt PROGBITS Address 0000000000001020 Offset 00001020
[15] .plt.sec PROGBITS Address 00000000000011b0 Offset 000011b0
[16] .text PROGBITS Address 0000000000001320 Offset 00001320
[23] .dynamic DYNAMIC Address 0000000000007d18 Offset 00007d18
[24] .got PROGBITS Address 0000000000007f08 Offset 00007f08
[25] .data PROGBITS Address 0000000000008000 Offset 00008000
[26] .bss NOBITS Address 0000000000008020 Offset 00008010

再看 program header:

1
$ readelf -Wl mini-diff

关键结果如下:

1
2
3
4
LOAD 0x000000 vaddr 0x0000000000000000 filesz 0x000b88 memsz 0x000b88 R
LOAD 0x001000 vaddr 0x0000000000001000 filesz 0x004215 memsz 0x004215 R E
LOAD 0x006000 vaddr 0x0000000000006000 filesz 0x000d18 memsz 0x000d18 R
LOAD 0x007d08 vaddr 0x0000000000007d08 filesz 0x000308 memsz 0x000328 RW

以及 readelf 自动整理出的映射关系:

1
2
03     .init .plt .plt.got .plt.sec .text .fini
05 .init_array .fini_array .dynamic .got .data .bss

这一步能直接看到:

  • .plt.plt.sec.text 被折叠进同一个可执行 PT_LOAD
  • .dynamic.got.data.bss 被折叠进同一个可写 PT_LOAD
  • .rela.dyn.rela.plt 自己并不是“单独映射一段”, 它们只是落在最前面的只读 PT_LOAD 里, 作为 loader 后续要读取的元数据

这里最关键的认知是:

  • 运行时真正被 mmap 的单位是 segment
  • 不是 section

section 只是帮助链接器和分析工具理解:

  • 哪块是代码
  • 哪块是符号表
  • 哪块是 relocation 表

但真正装载时, 这些东西已经被打包进更粗粒度的 PT_LOAD 里了.

5.5.2 用 mini-loader 直接看 “按 segment 装载” 的动作

mini-upatch 目录里有一个最小 loader:

1
$ ./mini-loader ./mini-diff

在我的实验里输出类似:

1
2
3
4
5
6
7
8
9
10
11
ELF type:    3
Machine: 62
Entry: 0x0000000000001320
PH off: 0x0000000000000040
PH num: 13
Allocated simulated image at 0x..., span 0x9000 bytes
PT_LOAD[2]: off=0x000000 vaddr=0x0000000000000000 filesz=0x00b88 memsz=0x00b88 -> mapped=... flags=r--
PT_LOAD[3]: off=0x001000 vaddr=0x0000000000001000 filesz=0x04215 memsz=0x04215 -> mapped=... flags=r-x
PT_LOAD[4]: off=0x006000 vaddr=0x0000000000006000 filesz=0x00d18 memsz=0x00d18 -> mapped=... flags=r--
PT_LOAD[5]: off=0x007d08 vaddr=0x0000000000007d08 filesz=0x00308 memsz=0x00328 -> mapped=... flags=rw-
Simulated entry address: ...

这个程序做的事情非常接近最小装载逻辑:

  • 扫描所有 PT_LOAD
  • 先求出整个映像的虚拟地址跨度
  • 分配一整块匿名内存
  • 对每个 PT_LOAD 执行:
    • memcpy(file + p_offset, image + (p_vaddr - min_vaddr), p_filesz)
    • 如果 p_memsz > p_filesz, 再把多出来的部分清零

这也正好解释了:

  • 为什么 .bsssh_type = NOBITS
  • 但运行时依然会在内存里存在

因为真正决定 .bss 是否出现的不是 section 自己, 而是:

  • 它所在的那个 PT_LOAD
  • p_memszp_filesz 更大

所以 loader 不需要从文件里拷 .bss, 只需要在 segment 尾部补零即可.

5.5.3 装载完 segment 以后, relocation 从哪里找

只靠 PT_LOAD 还不够.

因为 loader 现在只知道:

  • 哪些字节被搬进内存了
  • 每段的权限是什么

但它还不知道:

  • 哪些位置要做动态重定位
  • 去哪里找符号表
  • .plt / .got 应该怎么补齐

这一步靠的是 PT_DYNAMIC.

先看它:

1
$ readelf -Wd mini-diff

在我的实验里关键条目如下:

1
2
3
4
5
6
7
8
(SYMTAB)   0x3d8
(STRTAB) 0x6a8
(PLTGOT) 0x7f08
(JMPREL) 0x960
(PLTRELSZ) 552 (bytes)
(RELA) 0x888
(RELASZ) 216 (bytes)
(RELACOUNT) 3

把它和前面的 section 地址对一下:

  • SYMTAB = 0x3d8 对应 .dynsym
  • STRTAB = 0x6a8 对应 .dynstr
  • RELA = 0x888 对应 .rela.dyn
  • JMPREL = 0x960 对应 .rela.plt
  • PLTGOT = 0x7f08 对应 .got

于是 loader 的工作链就变成:

  1. 先按 PT_LOAD 把这些 section 所在的 segment 全部映射进内存.
  2. 再通过 PT_DYNAMIC 找到 .dynamic.
  3. 再由 .dynamic 给出的地址, 找到 .dynsym, .dynstr, .rela.dyn, .rela.plt, .got.
  4. 然后逐条执行 relocation.

这说明:

  • program header 决定“数据先被搬到哪里”
  • .dynamic 决定“搬过去以后该继续解析哪些运行时元数据”

5.5.4 relocation 实际上是在修正已经映射好的 PT_LOAD 内容

先看 mini-diff 的动态 relocation:

1
$ readelf -Wr mini-diff

关键部分如下:

1
2
3
4
5
6
7
8
9
10
11
Relocation section '.rela.dyn' at offset 0x888 contains 9 entries:
0000000000007d08 R_X86_64_RELATIVE 1400
0000000000007d10 R_X86_64_RELATIVE 13c0
0000000000008008 R_X86_64_RELATIVE 8008
0000000000007fd8 R_X86_64_GLOB_DAT __libc_start_main + 0
...

Relocation section '.rela.plt' at offset 0x960 contains 23 entries:
0000000000007f20 R_X86_64_JUMP_SLOT free + 0
0000000000007f28 R_X86_64_JUMP_SLOT putchar + 0
...

这些 Offset.o 文件里的 r_offset 有一个非常重要的区别:

  • .o 文件里的 ET_REL
    • r_offset 是“相对目标 section 起始位置的偏移”
  • 对已经链接好的 ET_DYN / ET_EXEC
    • 这里打印出来的 offset 已经是“映像虚拟地址”

所以:

  • 0x7d08
  • 0x7fd8
  • 0x7f20

这些值本身就已经直接落在某个已装载的 PT_LOAD 范围里.

例如前面可写段是:

1
LOAD 0x007d08 vaddr 0x0000000000007d08 filesz 0x000308 memsz 0x000328 RW

于是:

  • 0x7d08 落在这个 RW 段里, 对应 .init_array
  • 0x7fd8 落在这个 RW 段里, 对应 .got
  • 0x8008 落在这个 RW 段里, 对应 .data

这就说明动态 relocation 的本质是:

  • 先由 PT_LOAD.got.data.init_array 这些目标区域映射出来
  • 再由 .rela.dyn / .rela.plt 去修正这些区域里的具体槽位

换句话说:

  • relocation 不是凭空生效
  • 它一定是在“已经映射好的 segment 内存”上改字节

5.5.5 .plt.got.rela.plt 是怎么串起来的

再看一下 .plt.sec 的反汇编:

1
$ objdump -d -j .plt -j .plt.sec mini-diff

出现.

1
2
0000000000001220 <printf@plt>:
1224: ff 25 2e 6d 00 00 jmp *0x6d2e(%rip) # 7f58 <printf@GLIBC_2.2.5>

这里 printf@plt 这条跳转最终会去读地址 0x7f58.

而前面 .rela.plt 里刚好有:

1
0000000000007f58  R_X86_64_JUMP_SLOT printf + 0

再结合 .dynamic:

1
2
(PLTGOT) 0x7f08
(JMPREL) 0x960

这三者的关系就很清楚了:

  • .plt / .plt.sec
    • 放的是跳转模板代码
  • .got
    • 放的是真正会被改写的函数入口槽位
  • .rela.plt
    • 描述“.got 里哪些槽位对应哪个外部符号”

也就是说, 调用链不是:

  • 代码直接写死跳到 printf

而是:

  • 代码先进 printf@plt
  • printf@plt 再去读 .got 里的槽位
  • 动态加载器根据 .rela.plt 把这个槽位补成真正的 printf 地址

这里又能看出三层职责:

  • section 负责把 .plt / .got / .rela.plt 分开存
  • program header 负责把它们所属的段装载进内存
  • relocation 负责把 .got 里的槽位修成正确地址

6 ELF dynamic struct

6.1 对应结构体

1
2
3
4
5
6
7
typedef struct {
Elf64_Sxword d_tag;
union {
Elf64_Xword d_val;
Elf64_Addr d_ptr;
} d_un;
} Elf64_Dyn;

6.2 readelf -Wd 实例

可重定位文件.

1
2
3
$ readelf -Wd diff_old.o

There is no dynamic section in this file.

可执行文件.

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
$ readelf -Wd mini-diff 

Dynamic section at offset 0x7d18 contains 27 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000c (INIT) 0x1000
0x000000000000000d (FINI) 0x5208
0x0000000000000019 (INIT_ARRAY) 0x7d08
0x000000000000001b (INIT_ARRAYSZ) 8 (bytes)
0x000000000000001a (FINI_ARRAY) 0x7d10
0x000000000000001c (FINI_ARRAYSZ) 8 (bytes)
0x000000006ffffef5 (GNU_HASH) 0x3b0
0x0000000000000005 (STRTAB) 0x6a8
0x0000000000000006 (SYMTAB) 0x3d8
0x000000000000000a (STRSZ) 320 (bytes)
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000015 (DEBUG) 0x0
0x0000000000000003 (PLTGOT) 0x7f08
0x0000000000000002 (PLTRELSZ) 552 (bytes)
0x0000000000000014 (PLTREL) RELA
0x0000000000000017 (JMPREL) 0x960
0x0000000000000007 (RELA) 0x888
0x0000000000000008 (RELASZ) 216 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x000000000000001e (FLAGS) BIND_NOW
0x000000006ffffffb (FLAGS_1) Flags: NOW PIE
0x000000006ffffffe (VERNEED) 0x828
0x000000006fffffff (VERNEEDNUM) 1
0x000000006ffffff0 (VERSYM) 0x7e8
0x000000006ffffff9 (RELACOUNT) 3
0x0000000000000000 (NULL) 0x0

6.3 表项解释

loader 的典型读取顺序其实是:

  1. 先读 ELF header.
  2. 再读 program header table.
  3. 找到 PT_DYNAMIC.
  4. 把那一段运行时地址解释成 Elf64_Dyn[].
  5. 一直读到 d_tag = DT_NULL.

所以 .dynamic 是内容本身, PT_DYNAMIC 是“去哪里找这块内容”

伪代码是:

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
Elf64_Phdr *phdrs = ...;
Elf64_Dyn *dyn = NULL;

for (i = 0; i < ehdr->e_phnum; i++) {
if (phdrs[i].p_type == PT_DYNAMIC) {
dyn = (Elf64_Dyn *)(base + phdrs[i].p_vaddr);
break;
}
}

for (; dyn->d_tag != DT_NULL; dyn++) {
switch (dyn->d_tag) {
case DT_STRTAB:
strtab = base + dyn->d_un.d_ptr;
break;
case DT_SYMTAB:
symtab = base + dyn->d_un.d_ptr;
break;
case DT_RELA:
rela = base + dyn->d_un.d_ptr;
break;
case DT_RELASZ:
relasz = dyn->d_un.d_val;
break;
}
}

6.3.1 TagValue

readelf -Wd 最左边两列通常可以先这样理解:

  • Tag: 对应 d_tag, 表示这一项到底描述什么.
  • Value: 对应 d_un, 具体值可能是:
    • 一个地址/指针
    • 一个大小
    • 一个计数
    • 一个标志位
    • 一个字符串在字符串表中的偏移

也就是说, Elf64_Dyn 是一个:

  • “tag + payload”

风格的结构.

不同的 d_tag, 决定 d_un 应该被解释成 d_ptr 或者 d_val.

6.3.2 NEEDED

例如:

1
0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]

这个 tag 对应:

  • DT_NEEDED

它表示:

  • 当前 ELF 运行时依赖哪个共享库

这里 readelf 已经帮我把值解码成了:

  • libc.so.6

更底层一点理解, 原始值其实是:

  • 一个在 .dynstr 里的字符串偏移

loader 后续就会根据这些 DT_NEEDED 条目去加载依赖库.

6.3.3 INIT / FINI

例如:

1
2
(INIT) 0x1000
(FINI) 0x5208

这两个分别对应:

  • DT_INIT
  • DT_FINI

它们表示:

  • 初始化函数入口
  • 析构函数入口

这些值通常是:

  • 映像内虚拟地址

在当前样例里: 0x1000 正好落在 .init, 0x5208 正好落在 .fini.

6.3.4 INIT_ARRAY / FINI_ARRAY

例如:

1
2
3
4
(INIT_ARRAY)   0x7d08
(INIT_ARRAYSZ) 8 (bytes)
(FINI_ARRAY) 0x7d10
(FINI_ARRAYSZ) 8 (bytes)

这些对应:

  • DT_INIT_ARRAY
  • DT_INIT_ARRAYSZ
  • DT_FINI_ARRAY
  • DT_FINI_ARRAYSZ

含义是:

  • 初始化函数数组在哪里
  • 这块数组有多大
  • 析构函数数组在哪里
  • 这块数组有多大

在当前样例里, 这些地址都落在 RW 的 PT_LOAD 里.

这也能说明:

  • dynamic table 里不只是“符号/重定位”信息
  • 它还描述运行时初始化流程需要的数据

6.3.5 GNU_HASH / SYMTAB / STRTAB

例如:

1
2
3
4
5
(GNU_HASH) 0x3b0
(SYMTAB) 0x3d8
(STRTAB) 0x6a8
(STRSZ) 320 (bytes)
(SYMENT) 24 (bytes)

这些条目描述的是动态符号解析所需的基础表.

其中:

  • DT_GNU_HASH
    • GNU 风格哈希表的位置
    • loader 可用它加速动态符号查找
  • DT_SYMTAB
    • 动态符号表位置
    • 对应 .dynsym
  • DT_STRTAB
    • 动态字符串表位置
    • 对应 .dynstr
  • DT_STRSZ
    • .dynstr 大小
  • DT_SYMENT
    • 每个动态符号项的大小
    • 对 ELF64 来说通常就是 sizeof(Elf64_Sym) = 24

结合前面的 section header 可以直接对上:

  • SYMTAB = 0x3d8 对应 .dynsym
  • STRTAB = 0x6a8 对应 .dynstr

所以 DT_NEEDEDDT_SONAME 一类字符串型条目, 最终都要依赖 DT_STRTAB 去解码.

6.3.6 PLTGOT / JMPREL / PLTRELSZ / PLTREL

例如:

1
2
3
4
(PLTGOT)   0x7f08
(PLTRELSZ) 552 (bytes)
(PLTREL) RELA
(JMPREL) 0x960

这些条目描述的是:

  • PLT/GOT 这一条动态调用链需要的元数据

具体来说:

  • DT_PLTGOT
    • PLT 相关 GOT 区域的位置
    • 当前样例里对上 .got
  • DT_JMPREL
    • 专门给 PLT 使用的 relocation 表位置
    • 当前样例里对上 .rela.plt
  • DT_PLTRELSZ
    • 这张表的总大小
  • DT_PLTREL
    • 表项类型是什么
    • 当前样例里是 RELA

也就是说, 外部函数调用这一条线通常是:

  1. 代码先进 .plt / .plt.sec
  2. .plt 再去读 .got 里的槽位
  3. loader 根据 DT_JMPREL 指向的 .rela.plt 去修这些槽位

6.3.7 RELA / RELASZ / RELAENT / RELACOUNT

例如:

1
2
3
4
(RELA)      0x888
(RELASZ) 216 (bytes)
(RELAENT) 24 (bytes)
(RELACOUNT) 3

这些条目描述的是:

  • 一般动态 relocation 表在哪里

对应关系通常是:

  • DT_RELA
    • relocation 表位置
    • 当前样例里对上 .rela.dyn
  • DT_RELASZ
    • 整张表大小
  • DT_RELAENT
    • 每个表项大小
    • ELF64 下通常就是 sizeof(Elf64_Rela) = 24
  • DT_RELACOUNT
    • 某些前缀 relocation 的个数
    • 常用于 R_X86_64_RELATIVE 这一类可快速批处理的项

前面 readelf -Wr mini-diff 里已经看到:

  • .rela.dyn 里有若干 R_X86_64_RELATIVE
  • 也有 R_X86_64_GLOB_DAT
  • .rela.plt 里有很多 R_X86_64_JUMP_SLOT

这里 DT_RELADT_JMPREL 的分工就很清楚了:

  • DT_RELA
    • 管一般动态重定位
  • DT_JMPREL
    • 管 PLT 相关重定位

6.3.8 FLAGS / FLAGS_1

例如:

1
2
(FLAGS)   BIND_NOW
(FLAGS_1) Flags: NOW PIE

这些是动态装载相关标志.

在这个样例里可以先这样理解:

  • BIND_NOW / NOW
    • 倾向于在启动时就把需要的符号解析好
    • 而不是把所有外部调用都拖到第一次调用时再懒绑定
  • PIE
    • 说明这是 position-independent executable

这和前面看到的:

  • ELF type = DYN

是互相呼应的.

6.3.9 VERNEED / VERNEEDNUM / VERSYM

例如:

1
2
3
(VERNEED)    0x828
(VERNEEDNUM) 1
(VERSYM) 0x7e8

这些条目描述的是:

  • 符号版本控制信息

简单理解就是:

  • 某个外部符号不只叫 printf
  • 还可能要求它必须来自某个特定版本的 glibc

这就是为什么在别的输出里经常会看到:

  • printf@GLIBC_2.2.5
  • __libc_start_main@GLIBC_2.34

这些版本信息并不是凭空来的, 它们就依赖这些 version 相关表.

6.3.10 DEBUG

例如:

1
(DEBUG) 0x0

这个条目在文件静态状态下通常没什么特别内容.

它主要是留给运行时调试器/动态加载器协作使用的.

在 ELF 文件刚落盘时, 常常看到的就是:

  • 0x0

6.3.11 NULL

最后一项:

1
(NULL) 0x0

对应:

  • DT_NULL

它的作用不是提供业务含义, 而是:

  • 表示 dynamic table 到这里结束

loader 一般就是一直遍历 Elf64_Dyn[], 直到遇到这项为止.

6.4 GOT / PLT 最小实验

前面已经分别讲过:

  • .dynamic 里有哪些索引项
  • .rela.dyn / .rela.plt 是动态 relocation 表
  • .got / .plt / .plt.sec 是动态调用链的一部分

但如果只停在概念层面, 还是很容易混淆:

  • 哪些 section 是放“代码”的
  • 哪些 section 是放“地址槽位”的
  • 哪些 section 是放“如何修这些槽位”的描述

这一节直接用 mini-diff 的真实输出把它们串起来.

实验目录:

1
2
cd examples/mini-upatch
make mini-diff

6.4.1 先看这些 section 分别是干什么的

先看相关 section:

1
readelf -WS mini-diff | grep -E '\.dynsym|\.dynstr|\.rela\.dyn|\.rela\.plt|\.plt|\.got'

在我的实验里关键输出如下:

1
2
3
4
5
6
7
[ 6] .dynsym           DYNSYM          00000000000003d8 0003d8 ...
[ 7] .dynstr STRTAB 00000000000006a8 0006a8 ...
[10] .rela.dyn RELA 0000000000000888 000888 ...
[11] .rela.plt RELA 0000000000000960 000960 ...
[14] .plt.got PROGBITS 00000000000011a0 0011a0 ...
[15] .plt.sec PROGBITS 00000000000011b0 0011b0 ...
[24] .got PROGBITS 0000000000007f08 007f08 ...

这些 section 可以先这样记:

  • .dynsym: 动态符号表, 运行时真正需要参与动态解析的符号放在这里.
  • .dynstr: 动态字符串表, .dynsym.dynamic、版本信息里的名字最后都要来这里取字符串.
  • .rela.dyn: 一般动态 relocation 表, 常见目标包括 .got.data.init_array.fini_array.
  • .rela.plt: PLT 专用 relocation 表, 常见类型是 R_X86_64_JUMP_SLOT.
  • .got: 全局偏移表, 里面放的是“会在装载后被修正的地址槽位”
  • .plt.sec: 外部函数调用常见的跳板代码区, 每个 xxx@plt 通常都会在这里有一小段 stub.
  • .plt.got: 一小段与 GOT 配合的辅助跳板代码, 在现代工具链里常见, 但不是理解主链路时最核心的部分.

所以最重要的分工其实是:

  • .plt / .plt.sec: 放跳板代码.
  • .got: 放可修正的地址槽位.
  • .rela.plt / .rela.dyn: 放“这些槽位该怎么修”的描述.

6.4.2 先用 printf@plt 验证: PLT 里放的是跳板代码

1
objdump -d -j .plt -j .plt.sec mini-diff | sed -n '/<printf@plt>/,+4p'

在我的实验里输出如下:

1
2
3
4
0000000000001220 <printf@plt>:
1220: f3 0f 1e fa endbr64
1224: ff 25 2e 6d 00 00 jmp *0x6d2e(%rip) # 7f58 <printf@GLIBC_2.2.5>
122a: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1)

这里最关键的是: printf@plt 自己位于 0x1220, 它并没有直接写死真正的 printf 地址, 它执行的是一条 jmp *disp32(%rip). 也就是先根据 RIP-relative 位移算出一个内存地址, 再从那个内存地址里取出真正要跳转的目标地址.

objdump 注释出来的 # 7f58 是它根据当前指令末尾地址 0x122a 位移 0x6d2e 推算出来的目标内存地址: 0x122a + 0x6d2e = 0x7f58.

这里要特别注意被动态加载器修正的不是 1224 这条指令里的 2e 6d 00 00 被修正的是内存地址 0x7f58 那个槽位里的内容.

也就是说 .plt.sec 里的这条指令本身通常已经是完整可执行的它只是把控制流间接转发到 .got 某个槽位.

6.4.3 再看 0x7f58 为什么能说明它落在 GOT 里

前面已经看到 .got 的起始地址是 0x7f08, .got 的大小是 0xf8. 所以 .got 覆盖范围就是 [0x7f08, 0x8000). 而 printf@plt 跳到的那个槽位地址是 0x7f58. 显然满足 0x7f08 <= 0x7f58 < 0x8000. 所以它确实落在 .got 里. 还可以继续算出它相对 .got 起点的偏移 0x7f58 - 0x7f08 = 0x50. 也就是说 printf@plt 实际是在读 .got + 0x50 这个槽位.

6.4.4 再用 .rela.plt 验证: 这个 GOT 槽位确实就是 printf 的槽位

1
readelf -Wr mini-diff | grep printf

在我的实验里输出如下:

1
0000000000007f58  0000000a00000007 R_X86_64_JUMP_SLOT     0000000000000000 printf@GLIBC_2.2.5 + 0

这里最关键的是relocation 类型是 R_X86_64_JUMP_SLOT, r_offset = 0x7f58, 符号名是 printf.

把它和前面的反汇编对起来 printf@plt 的跳板代码读的是 0x7f58. .rela.pltprintf 的 relocation 目标偏移也是 0x7f58. 这就能直接证明 .plt.sec 里的printf@plt stub, .got 里的那个槽位, .rela.plt 里的 printf 重定位条目三者不是抽象关联, 而是通过同一个地址 0x7f58 严格对应起来的.

所以 PLT -> GOT -> .rela.plt 最好理解成:

  1. 代码先跳进 printf@plt
  2. printf@plt 再去读 .got 里的 0x7f58 槽位
  3. .rela.plt 说明这个槽位对应的外部符号就是 printf
  4. 动态加载器把这个槽位修成真正的 printf 地址

6.4.5 再拿 free@plt 做一次交叉验证

1
2
objdump -d -j .plt -j .plt.sec mini-diff | sed -n '/<free@plt>/,+4p'
readelf -Wr mini-diff | grep ' free@'

在我的实验里关键输出如下:

1
2
3
4
00000000000011b0 <free@plt>:
11b4: ff 25 66 6d 00 00 jmp *0x6d66(%rip) # 7f20 <free@GLIBC_2.2.5>

0000000000007f20 0000000100000007 R_X86_64_JUMP_SLOT 0000000000000000 free@GLIBC_2.2.5 + 0

这里同样能对上:

  • free@plt 读取的槽位地址是 0x7f20
  • .rela.pltfreer_offset 也是 0x7f20

这说明前面 printf 的现象不是偶然, 而是 PLT/GOT 的普遍工作方式.