【Linux0.11代码分析】09 之 ELF可执行程序02 - Section Headers解析
- 一、ELF概述
- 二、ELF的组成结构
- 2.1 ELF header:解析出 section headers 含31个section节和 program headers 含13个segment段
- 2.2 Section Headers:获取当前程序的31个section节区信息
- 2.2.1 [30] .shstrtab 字符串表节:保存各section节的字符串名 sh_name
- 2.2.2 [0] NULL 节:数据为空
- 2.2.3 [1] .interp 节:指定在程序运行时所要使用的动态链接器路径和名称
- 2.2.4 [2] .note.gnu.property 节:存储GNU 相关的属性信息
- 2.2.5 [3] .note.gnu.build-id ELF Note节:存储编译时生成的Build ID,用于快速判断两个二进制文件是否一致
- 2.2.6 [4] .note.ABI-tag 节:存储支持的操作系统的版本信息
- 2.2.7 [5] .gnu.hash 节:包含了动态链接器用于快速查找符号的哈希表
- 2.2.8 [6] .dynsym 节: 包含了动态链接器在程序执行过程中需要使用的外部函数和变量的符号表
- 2.2.9 [7] .dynstr 字符串表节: 用于存储动态链接器需要的符号名称字符串
- 2.2.10 [8] .gnu.version 符号表节:存储与符号版本相关的信息
- 2.2.11 [9] .gnu.version_r 节: 存储版本需求信息
- 2.2.12 [10] .rela.dyn 节:存储动态重定位表
- 2.2.13 [11] .rela.plt 节:存储 PLT(Procedure Linkage Table)的动态重定位表
- 2.2.14 [12] .init 节:存储了在程序启动时需要执行的初始化代码
- 2.2.15 [13] .plt 节: 用于实现动态链接的延迟绑定机制(Lazy Binding)
- 2.2.16 [14] .plt.got 节:用于实现动态链接库中全局变量的延迟绑定
- 2.2.17 [15] .plt.sec 节:用于实现对 PLT 表项中跳转到 GOT 表项地址的保护
- 2.2.18 [16] .text 节:存放程序代码的二进制指令
- 2.2.19 [17] .fini 节:存储了程序结束时需要执行的代码
- 2.2.20 [18] .rodata 节:存储只读数据,如常量
- 2.2.21 [19] .eh_frame_hdr 节: 存储了 .eh_frame节中的调试信息的压缩版本,可以使调试信息的解析更加高效
- 2.2.22 [20] .eh_frame 节:存储了异常处理框架信息,用于程序的调试和分析
- 2.2.23 [21] .init_array 节:存储了在程序开始执行时需要运行的一系列初始化函数
- 2.2.24 [22] .fini_array 节:存储了在程序退出时需要运行的一系列清理函数
- 2.2.25 [23] .dynamic 节:存储了动态链接时需要用到的信息,用于实现程序启动时的动态链接机制
- 2.2.26 [24] .got 节:存储了全局变量或函数的地址
- 2.2.27 [25] .data 节:存储了程序在运行时需要用到的初始化的全局变量和静态变量的数据
- 2.2.28 [26] .bss 节:存储了程序中未初始化的全局变量或静态变量的空间
- 2.2.29 [27] .comment 节:包含了编译器和链接器的一些注释信息
- 2.2.30 [28] .symtab 节:存储程序中的符号表信息,包括全局变量、函数名和其他符号等
- 2.2.31 [29] .strtab 节: 存储程序中的字符串表信息
- 2.3 Program Headers:解析13个segment段 及 对应的虚拟地址和物理地址
- 2.4 Program Headers:解析13个segment 段和31个section节的映射关系
系列文章如下:
系列文章汇总:《【Linux0.11代码分析】之 系列文章链接汇总(全)》
https://blog.csdn.net/Ciellee/article/details/130510069
.
1.《【Linux0.11代码分析】01 之 代码目录分析》
2.《【Linux0.11代码分析】02 之 bootsect.s 启动流程》
3.《【Linux0.11代码分析】03 之 setup.s 启动流程》
4.《【Linux0.11代码分析】04 之 head.s 启动流程》
5.《【Linux0.11代码分析】05 之 kernel 0号进程初始化 init\main.c 代码分析》
6.《【Linux0.11代码分析】06 之 kernel 1号进程 init() 代码分析》
7.《【Linux0.11代码分析】07 之 kernel execve() 函数 实现原理》
8.《【Linux0.11代码分析】08 之 ELF可执行程序01 - ELF header解析》
9.《【Linux0.11代码分析】09 之 ELF可执行程序02 - Section Headers解析》
10.《【Linux0.11代码分析】10 之 ELF可执行程序03 - Program Headers解析》
本文承接前文:《【Linux0.11代码分析】08 之 ELF可执行程序01 - ELF header解析》
本文,我们来详细分析下 Section Headers
中各section
的含义:
一、ELF概述
二、ELF的组成结构
2.1 ELF header:解析出 section headers 含31个section节和 program headers 含13个segment段
以上内容见:
《【Linux0.11代码分析】08 之 ELF可执行程序01 - ELF header解析》
2.2 Section Headers:获取当前程序的31个section节区信息
Section Headers
描述了所有的section
的信息,这是从编译和链接的角度来看ELF
文件的,通过它可以找到相应的 .section
节的内容,从而把它加载到相应的 segment
段区内存中。
当前 程序所包含的 section 节区信息如下 :
CielleeX:~/work/hello$ readelf -a hello
......
Section Headers:
[Nr] Name Type Address Offset Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 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 000000000000008f 0000000000000000 A 0 0 1
[ 8] .gnu.version VERSYM 0000000000000510 00000510 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 0000000000000176 0000000000000000 AX 0 0 16
[17] .fini PROGBITS 00000000000011d8 000011d8 000000000000000d 0000000000000000 AX 0 0 4
[18] .rodata PROGBITS 0000000000002000 00002000 0000000000000022 0000000000000000 A 0 0 4
[19] .eh_frame_hdr PROGBITS 0000000000002024 00002024 000000000000003c 0000000000000000 A 0 0 4
[20] .eh_frame PROGBITS 0000000000002060 00002060 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 000000000000002d 0000000000000001 MS 0 0 1
[28] .symtab SYMTAB 0000000000000000 00003040 0000000000000378 0000000000000018 29 18 8
[29] .strtab STRTAB 0000000000000000 000033b8 00000000000001e1 0000000000000000 0 0 1
[30] .shstrtab STRTAB 0000000000000000 00003599 000000000000011a 0000000000000000 0 0 1
从前面 elf header 解析中,我们能够知道:
section headers
的偏移地址是 14008 bytes
,大小为64 bytes
,总共包含 31
个 section
,也就是共占 31x64=1984bytes
获取所有 section
的命令为: hexdump -C -s 14008 -n 1984 hello
我们先读三个 section
内容出来看下,如下:
如上,第一个section
均为0
,使用 *
省略了,我们把它展开来,如下:
lihaiyan@CielleeX:~/work/hello$ hexdump -C -s 14008 -n 192 hello
第一个section 节区
000036b8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000036c8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000036d8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000036e8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
第二个section 节区:当前section 的name,保存在.shstrtab 的节区中偏移 0x1b 的位置处
000036f8 【1b 00 00 00】 01 00 00 00 02 00 00 00 00 00 00 00 |................|
00003708 18 03 00 00 00 00 00 00 18 03 00 00 00 00 00 00 |................|
00003718 1c 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00003728 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
第三个section 节区:当前section 的name,保存在.shstrtab 的节区中偏移 0x23 的位置处
00003738 23 00 00 00 07 00 00 00 02 00 00 00 00 00 00 00 |#...............|
00003748 38 03 00 00 00 00 00 00 38 03 00 00 00 00 00 00 |8.......8.......|
00003758 30 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |0...............|
00003768 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00003778
......
各section
节结构体信息定义如下:
typedef struct {
Elf64_Word sh_name; // 4 B (B for bytes)
===>ELF文件中的每个节都有一个唯一的名称,如果sh_name 属性值为 0x00000035,表示该节的名称在节名称字符串表中的第 0x35 个字节处开始。
Elf64_Word sh_type; // 4 B
===============>
+ sh_type 是一个表示 ELF 文件中每个节的类型的属性值,通常使用 Elf64_Word 类型来存储。其取值范围是由标准定义的一些枚举值,例如:
+ SHT_NULL: 0,表示该节头部表项无效。
+ SHT_PROGBITS: 1,表示该节包含程序定义的信息,如代码、数据等。
+ SHT_SYMTAB: 2,表示该节包含符号表,存储在文件中的全局和局部符号信息。
+ SHT_STRTAB: 3,表示该节包含字符串表,存储以 NULL 结尾的 ASCII 字符串。
+ SHT_RELA: 4,表示该节包含重定位条目,为保证程序的可执行性而进行修正。
+ SHT_HASH: 5,表示该节包含哈希表,用于加速符号查找。
+ SHT_DYNAMIC: 6,表示该节包含动态连接信息。
+ SHT_NOTE: 7,表示该节包含一些附加信息,如版本信息。
+ SHT_REL: 9,表示该节包含重定位条目,不含添加常量值的重定位信息。
+ SHT_DYNSYM: 11,表示该节包含动态符号表,只包含全局符号信息。
<===============
Elf64_Xword sh_flags; // 8 B
===============>
+ sh_flags 是一个表示 ELF 文件中每个节的属性标志值的属性,通常使用 Elf64_Xword 类型来存储。
+ sh_flags 属性的各个标志位对应的含义如下:
+ SHF_WRITE (0x1):该节包含可写数据。
+ SHF_ALLOC (0x2):该节会被加载到内存中。
+ SHF_EXECINSTR (0x4):该节包含可执行的指令。
+ SHF_MERGE (0x10):该节中的相邻数据可以合并为较大的数据块。
+ SHF_STRINGS (0x20):该节包含以 NULL 结尾的 ASCII 字符串。
+ SHF_INFO_LINK (0x40):该节的 sh_info 字段包含相关信息
<===============
Elf64_Addr sh_addr; // 8 B 表示 ELF 文件中每个节的内存地址或偏移量
Elf64_Off sh_offset; // 8 B 指定了该节在 ELF 文件中的起始位置的偏移量,以便在ELF文件中正确地定位该节的数据
Elf64_Xword sh_size; // 8 B 表示 ELF 文件中每个节的大小
===============>
+ 对于在进程虚拟地址空间中加载的节(即 sh_flags 属性包含 SHF_ALLOC),sh_size 属性指定了该节在进程虚拟地址空间中占用的总字节数;
+ 对于在文件中存储的节(即 sh_flags 属性不包含 SHF_ALLOC),sh_size 属性指定了该节在 ELF 文件中所占用的总字节数。
<===============
Elf64_Word sh_link; // 4 B 表示 ELF 文件中每个节关联的其他节的索引值
===============>
+ 它指定了该节关联的其他节的索引值,也就是和该节有依赖关系的其他节的索引号
+ 对于一些特殊的节类型,如符号表节和重定位节,它们通常会通过 sh_link 属性来关联到其他的节。
+ 在符号表节中,sh_link 属性通常指向字符串表节,以便找到该符号名在字符串表中的位置;
+ 在重定位节中,sh_link 属性通常指向需要进行重定位操作的节,以便确定需要修正哪些地址。
<===============
Elf64_Word sh_info; // 4 B 表示 ELF 文件中每个节的附加信息的属性
===============>
+ 对于符号表节(SHT_SYMTAB)和动态符号表节(SHT_DYNSYM),sh_info 属性指定了该表中局部符号的数量。
+ 局部符号是指只在当前目标文件或共享对象中可见的符号,而不会被其他目标文件或共享对象引用。
+ 全局符号是指可以被其他目标文件或共享对象引用的符号。
+
+ 对于重定位节(SHT_REL 和 SHT_RELA),sh_info 属性指定了需要进行重定位的符号表在符号表节中的起始索引。
+ 在执行重定位时,程序首先会遍历重定位节中的所有项,确定需要进行重定位的地址,
+ 然后通过该属性指定的符号表来查找需要进行修正的符号。
<===============
Elf64_Xword sh_addralign; // 8 B 表示 ELF 文件中每个节的对齐方式, 1,则表示该节的数据按字节对齐
Elf64_Xword sh_entsize; // 8 B 表示每个节项的大小,即每个节中的元素大小
===============>
+ ELF 文件格式中,除了某些特殊的节类型(如 SHT_NULL 和 SHT_NOBITS)外,每个节的大小都是固定的。
+ 对于这些节,sh_entsize 属性就用来表示每个节项的大小,即每个节中的元素大小。
+
+ sh_entsize 属性通常使用 Elf64_Xword 类型来存储,它表示每个节项的大小,以字节为单位。
+ 例如, 在符号表节(SHT_SYMTAB 或 SHT_DYNSYM)中,每个符号表项的大小就由 sh_entsize 属性来指定。
+ 在字符串表节(SHT_STRTAB)中,每个字符串的大小也由该属性来确定。
<===============
} Elf64_Shdr; // total size: 64 B
下面,我们对每个节进行分析:
2.2.1 [30] .shstrtab 字符串表节:保存各section节的字符串名 sh_name
之所以优先分析 .shstrtab
节,是因为它保存了所有 section
节区的字符串名 sh_name
,
先把它的信息读取出来吧:
.shstrtab
节的偏移位置为:
14008
b
y
t
e
s
+
30
∗
64
=
15928
14008 bytes + 30 * 64 = 15928
14008bytes+30∗64=15928
我们来对它解析下:
CielleeX:~/work/hello$ hexdump -C -s 15928 -n 64 hello
00003e38 11 00 00 00 03 00 00 00 00 00 00 00 00 00 00 00 |................|
00003e48 00 00 00 00 00 00 00 00 99 35 00 00 00 00 00 00 |.........5......|
00003e58 1a 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00003e68 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00003e78
typedef struct {
Elf64_Word sh_name; // 4 B // 节名称位于偏移 0x11 处
---> 00003e38 【11 00 00 00】 03 00 00 00 00 00 00 00 00 00 00 00
Elf64_Word sh_type; // 4 B // 03对应SHT_STRTAB类型,表示该节包含字符串表,存储以 NULL 结尾的 ASCII 字符串
---> 00003e38 11 00 00 00 【03 00 00 00】 00 00 00 00 00 00 00 00
Elf64_Xword sh_flags; // 8 B // 无flag属性
---> 00003e38 11 00 00 00 03 00 00 00 【00 00 00 00 00 00 00 00】
Elf64_Addr sh_addr; // 8 B // 无虚拟入口
---> 00003e48 【00 00 00 00 00 00 00 00】 99 35 00 00 00 00 00 00
Elf64_Off sh_offset; // 8 B // 该 section 在当前文件中的偏移为 0x00003599
---> 00003e48 00 00 00 00 00 00 00 00 【99 35 00 00 00 00 00 00】
Elf64_Xword sh_size; // 8 B // 大小为 0x011a = 282 bytes
---> 00003e58 【1a 01 00 00 00 00 00 00】 00 00 00 00 00 00 00 00
Elf64_Word sh_link; // 4 B // 无相关的 section
---> 00003e58 1a 01 00 00 00 00 00 00 【00 00 00 00】 00 00 00 00
Elf64_Word sh_info; // 4 B // 无额外的section 信息
---> 00003e58 1a 01 00 00 00 00 00 00 00 00 00 00 【00 00 00 00】
Elf64_Xword sh_addralign; // 8 B // 该 section的对齐宽度为 01
---> 00003e68 【01 00 00 00 00 00 00 00】 00 00 00 00 00 00 00 00
Elf64_Xword sh_entsize; // 8 B // 该section 不包含 table,所该字段为0
---> 00003e68 01 00 00 00 00 00 00 00 【00 00 00 00 00 00 00 00】
} Elf64_Shdr; // total size: 64 B
通过解析,我们得知:
.shstrtab
节 的内容位于文件偏移 0x00003599 (13721)
处,大小为 282 bytes
,我们读出来看看,
如下 ,果然是各个section
节区的sh_name
:
比如,当前 section
的 sh_name
为 0x11 (17)
,
对应的二进制为: 2e 73 68 73 74 72 74 61 62 00
,对应的字符串为:.shstrtab
CielleeX:~/work/hello$ hexdump -C -s 13721 -n 282 hello
00003599 00 2e 73 79 6d 74 61 62 00 2e 73 74 72 74 61 62 |..symtab..strtab|
000035a9 00 2e 73 68 73 74 72 74 61 62 00 2e 69 6e 74 65 |..shstrtab..inte|
000035b9 72 70 00 2e 6e 6f 74 65 2e 67 6e 75 2e 70 72 6f |rp..note.gnu.pro|
000035c9 70 65 72 74 79 00 2e 6e 6f 74 65 2e 67 6e 75 2e |perty..note.gnu.|
000035d9 62 75 69 6c 64 2d 69 64 00 2e 6e 6f 74 65 2e 41 |build-id..note.A|
000035e9 42 49 2d 74 61 67 00 2e 67 6e 75 2e 68 61 73 68 |BI-tag..gnu.hash|
000035f9 00 2e 64 79 6e 73 79 6d 00 2e 64 79 6e 73 74 72 |..dynsym..dynstr|
00003609 00 2e 67 6e 75 2e 76 65 72 73 69 6f 6e 00 2e 67 |..gnu.version..g|
00003619 6e 75 2e 76 65 72 73 69 6f 6e 5f 72 00 2e 72 65 |nu.version_r..re|
00003629 6c 61 2e 64 79 6e 00 2e 72 65 6c 61 2e 70 6c 74 |la.dyn..rela.plt|
00003639 00 2e 69 6e 69 74 00 2e 70 6c 74 2e 67 6f 74 00 |..init..plt.got.|
00003649 2e 70 6c 74 2e 73 65 63 00 2e 74 65 78 74 00 2e |.plt.sec..text..|
00003659 66 69 6e 69 00 2e 72 6f 64 61 74 61 00 2e 65 68 |fini..rodata..eh|
00003669 5f 66 72 61 6d 65 5f 68 64 72 00 2e 65 68 5f 66 |_frame_hdr..eh_f|
00003679 72 61 6d 65 00 2e 69 6e 69 74 5f 61 72 72 61 79 |rame..init_array|
00003689 00 2e 66 69 6e 69 5f 61 72 72 61 79 00 2e 64 79 |..fini_array..dy|
00003699 6e 61 6d 69 63 00 2e 64 61 74 61 00 2e 62 73 73 |namic..data..bss|
000036a9 00 2e 63 6f 6d 6d 65 6e 74 00 |..comment.|
000036b3
2.2.2 [0] NULL 节:数据为空
第一个section 节,数据全为空,如下:
lihaiyan@CielleeX:~/work/hello$ hexdump -C -s 14008 -n 64 hello
000036b8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000036c8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000036d8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000036e8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000036f8
2.2.3 [1] .interp 节:指定在程序运行时所要使用的动态链接器路径和名称
.interp
存储了一个以 NULL
结尾的字符串,用于指定在程序运行时所要使用的动态链接器路径和名称。
在 ELF
文件加载时,如果该文件需要依赖某些共享库,那么系统会自动载入并链接这些库。
此时系统需要查找动态链接器,以便在运行时解析这些共享库的符号引用。
动态链接器的路径和名称通常是由 .interp
节来指定的。
在大多数情况下,.interp
节只包含一个字符串 "/lib/ld-linux.so.2"
,表示使用 GNU
的动态链接器。
但是,在某些情况下,用户可以通过修改 .interp
节来指定其它的动态链接器路径和名称。
.interp
节的类型是 SHT_PROGBITS
,其 sh_flags
属性标志位 SHF_ALLOC、SHF_EXECINSTR
,即该节的数据在运行时是可执行的,并且需要被载入内存。
我们把.interp
节的内容打印出来,它的位置位于 14008 + 64 = 14072
byte
CielleeX:~/work/hello$ hexdump -C -s 14072 -n 64 hello
000036f8 1b 00 00 00 01 00 00 00 02 00 00 00 00 00 00 00 |................|
00003708 18 03 00 00 00 00 00 00 18 03 00 00 00 00 00 00 |................|
00003718 1c 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00003728 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00003738
每个 section header
中包含 10
个条目,每个条目占用 4
字节进行描述,
因为当前 elf
文件是小端,所以所有的字节都以小端模式进行解析,section header
解析:
字段类型 | value | 注释 |
---|---|---|
sh_name 4Byte | 1b 00 00 00 | 该section name 被保存在 .shstrtab 中,偏移地址为 0x1b |
sh_type 4Byte | 01 00 00 00 | 01 对应PROGBITS 类型,表示当前类型为程序数据 |
sh_flags 8Byte | 02 00 00 00 00 00 00 00 | 02 对应 SHF_ALLOC ,表示程序加载时需要向内核 申请内存 |
sh_addr 8Byte | 18 03 00 00 00 00 00 00 | 程序的虚拟地址入口为 0x00000318 |
sh_offset 8Byte | 18 03 00 00 00 00 00 00 | 该 section 在当前文件中的偏移为 0x00000318 |
sh_size 8Byte | 1c 00 00 00 00 00 00 00 | 该 section 的 size 为 0x0000001c |
sh_link 4Byte | 00 00 00 00 | 无相关的 section |
sh_info 4Byte | 00 00 00 00 | 无额外的section 信息 |
sh_addralign 8Byte | 01 00 00 00 00 00 00 00 | 该 section的对齐宽度为 01 |
sh_entsize 8Byte | 00 00 00 00 00 00 00 00 | 该section 不包含 table,所该字段为0 |
可以看到,
-
当前
section
节区名保存存偏移0x1b
处,对应十六进制为【2e 69 6e 74 65 72 70 00】
, 名为.interp
000035a9 00 2e 73 68 73 74 72 74 61 62 00 【2e 69 6e 74 65 |..shstrtab..inte|
000035b9 72 70 00】 2e 6e 6f 74 65 2e 67 6e 75 2e 70 72 6f |rp..note.gnu.pro|
-
.interp
的数据,保存在文件第0x318
的位置处,总长度为0x1c
,我们把这一块的数据打印出来看一下:
可以看出,它保存的内容为:/lib64/ld-linux-x86-64.so.2
, 是编译时的 ld
链接库的名字
2.2.4 [2] .note.gnu.property 节:存储GNU 相关的属性信息
.note.gnu.property
用于存储 GNU
属性(GNU Properties
)信息。
在 GNU
工具链中,通过向 ELF
文件注入 .note.gnu.property
节来实现各种功能,比如增加某些特定的属性信息,以及在编译过程中生成一些辅助信息。
.note.gnu.property
节通常包含多个属性条目(Property Entry
),每个条目由一个 magic number
和一个或多个属性项组成。
其中,magic number
用于标识这个属性条目的类型,而属性项则是一些键值对形式的数据,用于存储具体的属性信息。
不同类型的条目具有不同的属性项,可以通过读取 .note.gnu.property
节来获取特定类型的属性信息。
我们把.note.gnu.property
节的内容打印出来,它的位置位于 14008 + 2x64 = 14136
byte
我们来对它解析下:
CielleeX:~/work/hello$ hexdump -C -s 14136 -n 64 hello
00003738 23 00 00 00 【07 00 00 00】 【02 00 00 00 00 00 00 00】 |#...............|
// 0x07:该节包含一些附加信息,如版本信息
// 0x02: 该节会被加载到内存中
00003748 【38 03 00 00 00 00 00 00】 【38 03 00 00 00 00 00 00】 |8.......8.......|
// 虚拟入口地址为: 0x0338
// 该 section 在当前文件中的偏移为 0x00003338
00003758 【30 00 00 00 00 00 00 00】 00 00 00 00 00 00 00 00 |0...............|
// 该section 大小为 0x30 (48bytes)
00003768 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00003778
从上得知.note.gnu.property
节 的数据保存在 0x0338 (824)
偏移处, 大小为48 bytes
, 读取数据如下:
.note.gnu.property
的结构体定义如下 :
struct elf_property_note {
Elf64_Word n_namesz; // 名称长度 4bytes
Elf64_Word n_descsz; // 描述信息长度 4bytes
Elf64_Word n_type; // 类型 4bytes
===========>
+ 0x05:GNU 版本属性(GNU_PROPERTY_TYPE_GNU_ABI_TAG),用于指定编译时所使用的 ABI 版本。
+ 0x10:GNU 标志属性(GNU_PROPERTY_TYPE_GNU_BUILD_ID),用于存储由 ld --build-id 生成的 Build ID。
+ 0x20:GNU 扩展属性(GNU_PROPERTY_TYPE_GNU_PROPERTY),用于存储其他类型的属性信息。
<===========
char n_name[]; // 名称
char n_desc[]; // 描述信息
};
各字段含义如下:
n_namesz:属性名称字符串的长度(以字节为单位),不包括结尾的 NULL 字符。
n_descsz:属性描述信息的长度(以字节为单位)。
n_type:该条目的类型,通常是一个特定的 magic number。
n_name:属性名称字符串,以 NULL 结尾。
n_desc:属性的描述信息,可以是任意二进制数据。
来对它进行解析下:
CielleeX:~/work/hello$ hexdump -C -s 824 -n 48 hello
00000338 04 00 00 00 20 00 00 00 05 00 00 00 47 4e 55 00 |.... .......GNU.|
00000348 02 00 00 c0 04 00 00 00 03 00 00 00 00 00 00 00 |................|
00000358 02 80 00 c0 04 00 00 00 01 00 00 00 00 00 00 00 |................|
00000368
struct elf_property_note {
Elf64_Word n_namesz; // 名称长度 4bytes
---> 00000338 【04 00 00 00】 20 00 00 00 05 00 00 00 47 4e 55 00 // n_namesz = 4 bytes
Elf64_Word n_descsz; // 描述信息长度 4bytes
---> 00000338 04 00 00 00 【20 00 00 00】 05 00 00 00 47 4e 55 00 // n_descsz = 0x20 (32bytes)
Elf64_Word n_type; // 类型 4bytes
---> 00000338 04 00 00 00 20 00 00 00 【05 00 00 00】 47 4e 55 00 // 0X5: 指定编译时所使用的 ABI 版本
char n_name[]; // 名称
---> 00000338 04 00 00 00 20 00 00 00 05 00 00 00 【47 4e 55 00】 // n_name[0] = GNU
char n_desc[]; // 描述信息
---> 00000348 【02 00 00 c0】 【04 00 00 00】 【03 00 00 00】 【00 00 00 00】
---> 00000358 【02 80 00 c0】 【04 00 00 00】 【01 00 00 00】 【00 00 00 00】
// 键:0xc0000002 = 0x00000004 表示的是 .note.gnu.build-id 节的内容为 0x04, 用于唯一标识与该节相关联的可执行程序或共享库
---> Build ID 是 GNU 工具链提供的一个特性,用于在编译或链接过程中
---> 将一个固定长度的哈希值(默认为 SHA-1 哈希算法)注入到生成的 ELF 可执行文件或共享库中。
---> 这个哈希值可以通过读取 .note.gnu.build-id 节来获取,从而判断两个二进制文件是否具有相同的编译环境和源代码版本。
// 键:0x00000003 = 0x00000000 表示的是 .note.gnu.gold-version 节的内容为0x0
// 键:0xc0008002 = 0x00000004 表示的是 GNU 系统工具链的特定属性之一:GNU_PROPERTY_X86_ISA_1_USED, 用于指示 ELF 文件中所使用的 x86 ISA 版本
---> 0 表示未使用任何特定的 x86 ISA 特性; 1 表示使用 SSE2 指令集;
---> 2 表示使用 SSE3 指令集; 3 表示使用 SSSE3 指令集;
---> 4 表示使用 SSE4.1 指令集; 5 表示使用 SSE4.2 指令集;
---> 6 表示使用 AVX 指令集; 7 表示使用 AVX2 指令集。
// 键:0x00000000 = 0x00000001 表示的是 GNU 系统工具链的特定属性之一:GNU_PROPERTY_X86_ISA_1_NEEDED, 指示 ELF 文件所必须使用的最低 x86 ISA 版本, 也就是 使用 SSE2 指令集
};
2.2.5 [3] .note.gnu.build-id ELF Note节:存储编译时生成的Build ID,用于快速判断两个二进制文件是否一致
.note.gnu.build-id
是 ELF 二进制文件中的一个特殊节 (section
),用于存储编译时生成的构建 ID。
GNU Binutils
工具链使用 Build ID
技术来唯一标识每个可执行文件和共享库,这有助于在不同系统间轻松比较二进制文件的版本。
在 .note.gnu.build-id
节中,如果存在该节,则会包含一个 ELF note
头,其名字是 "GNU"
。
在这个 ELF note
的描述信息(note desc
)中,会包含一个 16
字节的构建 ID(Build ID)
。
Build ID
的生成方式是对可执行文件或共享库的所有内容计算散列值,因此可以保证对于同一份代码、同一编译环境生成的构建 ID 是唯一的。
通过比较两个 ELF
文件的 .note.gnu.build-id
节中的 Build ID
,可以快速判断两个二进制文件是否一致。
我们把.note.gnu.property
节的内容打印出来,它的位置位于 14008 + 3x64 = 14200 byte
CielleeX:~/work/hello$ hexdump -C -s 14200 -n 64 hello
00003778 36 00 00 00 07 00 00 00 02 00 00 00 00 00 00 00 |6...............|
00003788 【68 03 00 00 00 00 00 00】 【68 03 00 00 00 00 00 00】 |h.......h.......|
// 虚拟地址为: 0x0368
// 该 section 在当前文件中的偏移为 0x0368
00003798 【24 00 00 00 00 00 00 00】 00 00 00 00 00 00 00 00 |$...............|
// 大小为0x24,36bytes
000037a8 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000037b8
从上得知.note.gnu.build-id
节 的数据保存在 0x0368 (872)
偏移处, 大小为36 bytes
, 读取数据如下:
.note.gnu.build-id
节是一个 ELF note
,其头部信息 (note header) 和描述信息 (note desc) 的结构体定义如下:
// .note.gnu.build-id ELF note header
struct elf_note {
uint32_t n_namesz; // Name size
uint32_t n_descsz; // Content size
uint32_t n_type; // Type of the note
};
// Description of .note.gnu.build-id ELF note
struct build_id_note {
uint8_t bld_id[16]; // Build ID content (16 bytes)
char name[]; // Note name
};
其中,elf_note 结构体定义了 ELF note 的头部信息。它包含三个成员变量:
n_namesz
:名字的大小,指定了该 ELF note 名字的长度(不包括末尾的 ‘\0’ 字符)。n_descsz
:内容的大小,指定了该 ELF note 描述信息的长度。n_type
:类型,指定了该 ELF note 描述信息的类型。
而 build_id_note
结构体定义了.note.gnu.build-id
的描述信息。它包含两个成员变量:
name
:名字,指定了该ELF note
的名字。在.note.gnu.build-id
节中,该名字通常被设置为 “GNU”。bld_id
:内容,指定了该ELF note
描述信息的具体内容,即 16 字节的构建 ID。
我们来解析看下:
CielleeX:~/work/hello$ hexdump -C -s 872 -n 36 hello
00000368 04 00 00 00 14 00 00 00 03 00 00 00 47 4e 55 00 |............GNU.|
00000378 5d 4e 34 d6 bc 89 06 f6 0f bd fe 41 a9 65 4a 2c |]N4........A.eJ,|
00000388 d9 ef 59 37 |..Y7|
0000038c
// .note.gnu.build-id ELF note header
struct elf_note {
uint32_t n_namesz; // Name size = 0x4
---> 00000368 【04 00 00 00】 14 00 00 00 03 00 00 00 47 4e 55 00
uint32_t n_descsz; // Content size = 0x14
---> 00000368 04 00 00 00 【14 00 00 00】 03 00 00 00 47 4e 55 00
uint32_t n_type; // Type of the note = 0x3
---> 00000368 04 00 00 00 14 00 00 00 【03 00 00 00】 47 4e 55 00
};
// Description of .note.gnu.build-id ELF note
struct build_id_note {
char name[]; // Note name = GNU
---> 00000368 04 00 00 00 14 00 00 00 03 00 00 00【47 4e 55 00】 // GNU
uint8_t bld_id[16]; // Build ID content (16 bytes)
---> 00000378 【5d 4e 34 d6 bc 89 06 f6 0f bd fe 41 a9 65 4a 2c】
};
可以看出 build_id
为 5d 4e 34 d6 bc 89 06 f6 0f bd fe 41 a9 65 4a 2c
2.2.6 [4] .note.ABI-tag 节:存储支持的操作系统的版本信息
.note.ABI-tag
(Advanced Binary Interface tag
) 是 ELF
文件格式中的一种特殊类型的段(section
)
它包含了描述目标文件与其所在操作系统 ABI (Application Binary Interface) 版本信息的数据。
目标文件的编译器和链接器通常会在构建时自动添加该段以实现与操作系统 ABI 版本的兼容性。
.note.ABI-tag
的结构体定义如下:
typedef struct {
Elf64_Word n_namesz; /* Length of the note's name */
Elf64_Word n_descsz; /* Length of the note's descriptor */
Elf64_Word n_type; /* Type of the note */
} Elf64_Nhdr;
n_namesz 和 n_descsz 分别表示段名及其描述信息的大小,以字节为单位;n_type 表示该段的类型,通常为 NT_ABI。
一些常见的 ABI 版本类型值及其用途如下:
+ NT_BUILD_ID:指定目标文件的构建 ID。该值与 .note.gnu.build-id 段类似。
+ NT_GNU_ABI_TAG: 指定目标文件所采用的 GNU ABI 版本号,以及相关额外的信息。
该值包含以下字段:
abi: 一个整数,表示 GNU ABI 的版本号。当前最新版本号为 0x010100,即 1.1。
abi_desc: 描述此目标文件所需的其他 ABI 特定信息的字符串。
note_tag: 该值固定为 "GNU".
我们把.note.gnu.property
节的内容打印出来,它的位置位于 14008 + 4x64 = 14264 byte
可以看出,.note.ABI-tag
的内容位于 0x038c (908)
处,大小为 0x20 (32byte)
解析如下:
0000038c 04 00 00 00 ; n_namesz = 4 (Name size)
00000390 10 00 00 00 ; n_descsz = 16 (Content size)
00000394 01 00 00 00 ; n_type = 1 (Type of the note)
00000398 47 4e 55 00 ; Note name: "GNU"
0000039c 00 00 00 00 ; Alignment padding
000003a0 03 00 00 00 ; ABI version number (0x00000003) ABI版本号为 0x00000003
000003a4 02 00 00 00 ; Type of the object file (0x00000002) 对象文件类型为 0x00000002
000003a8 00 00 00 00 ; Padding 对齐
2.2.7 [5] .gnu.hash 节:包含了动态链接器用于快速查找符号的哈希表
.gnu.hash
包含了动态链接器用于快速查找符号的哈希表。
相比 .hash
段,.gnu.hash
段提供了更高效的符号查找机制,能够在较短的时间内快速定位符号。
我们把.gnu.hash
节的内容打印出来,它的位置位于 14008 + 5x64 = 14328 byte
可以看出,它位于 0x03b0处,大小为0x24, 分析方法同上,此处就不分析了。
.gnu.hash
段由以下结构体进行描述:
typedef struct {
Elf32_Word nbuckets; /* Number of buckets */
Elf32_Word symndx; /* First symbol index */
Elf32_Word maskwords; /* Number of Bloom filter words */
Elf32_Word *bloom; /* The Bloom filter */
Elf32_Word *buckets; /* The bucket array */
Elf32_Word *hashval; /* The hash value array */
} Elf32_GnuHash;
typedef struct {
Elf64_Word nbuckets; /* Number of buckets */
Elf64_Word symndx; /* First symbol index */
Elf64_Word maskwords; /* Number of Bloom filter words */
Elf64_Word *bloom; /* The Bloom filter */
Elf64_Word *buckets; /* The bucket array */
Elf64_Word *hashval; /* The hash value array */
} Elf64_GnuHash;
其中,
- nbuckets 表示哈希表的桶数量。
- symndx 表示第一个符号的索引值。
- maskwords 表示使用的布隆过滤器(Bloom filter)的单词数量(即 32 位字数)。
- bloom 表示指向布隆过滤器的指针。
- buckets 表示指向桶数组的指针。
- hashval 表示指向哈希值数组的指针。
2.2.8 [6] .dynsym 节: 包含了动态链接器在程序执行过程中需要使用的外部函数和变量的符号表
用于存储动态链接器需要使用的符号信息, 该段包含了动态链接器在程序执行过程中需要使用的外部函数和变量的符号表
我们把.dynsym
节的内容打印出来,它的位置位于 14008 + 6x64 = 14392 byte
可以看出,它位于 0x03d8 (984)处,大小为0xa8(168bytes)
.dynsym 的结构体定义:
typedef struct {
Elf64_Word st_name; /* Symbol name (string tbl index) */ 4B
unsigned char st_info; /* Symbol type and binding */ 1B
unsigned char st_other; /* Symbol visibility */ 1B
Elf64_Section st_shndx; /* Section index */ 2B
Elf64_Addr st_value; /* Symbol value */ 8B
Elf64_Xword st_size; /* Symbol size */ 8B
} Elf64_Sym;
其中,各字段的含义如下:
- st_name 表示该符号的名称在字符串表中的索引,对应于 .dynstr 段中的字符串。
- st_info 包含两个部分:符号类型和绑定属性。
- st_other 用于表示符号的其他属性,如可见性。
- st_shndx 表示该符号所属的节表索引
- st_value 表示该符号在地址空间中的值。
- st_size 表示该符号所占空间的大小。
需要注意的是,在使用 .dynsym 段时,通常需要结合 .dynstr 和 .plt、.got 等段来进行使用,以达到链接共享库和分发程序等目的。
解析如下:
第一个符号表项(位于偏移量 0x00 处)全为 0,表示该符号未定义。
第二个符号表项(位于偏移量 0x10 处)如下:
st_name 值为 0x00000012,在字符串表中对应字符串 "printf"。
st_value 值为 0,表示该符号在该文件中未定义。
st_size 值为 0,表示该符号的大小为 0。
st_info 值为 0x12,其中高 4 位表示该符号的类型为函数(值为 0x02),低 4 位表示该符号的绑定属性为本地可见(值为 0x00)。
st_other 值为 0,表示该符号的其他属性为默认值。
st_shndx 值为 0,表示该符号所属的节为未定义。
第三个符号表项(位于偏移量 0x22 处)如下:
st_name 值为 0x00000012,在字符串表中对应字符串 "scanf"。
st_value 值为 0,表示该符号在该文件中未定义。
st_size 值为 0,表示该符号的大小为 0。
st_info 值为 0x12,其中高 4 位表示该符号的类型为函数(值为 0x02),低 4 位表示该符号的绑定属性为本地可见(值为 0x00)。
st_other 值为 0,表示该符号的其他属性为默认值。
st_shndx 值为 0,表示该符号所属的节为未定义。
第四个符号表项(位于偏移量 0x66 处)如下:
st_name 值为 0x00000020,在字符串表中对应字符串 "GLOBAL_OFFSET_TABLE"。
st_value 值为 0x000000000000004a,表示该符号在虚拟地址空间中的地址为 0x4a。
st_size 值为 0,表示该符号的大小为 0。
st_info 值为 0x11,其中高 4 位表示该符号的类型为对象(值为 0x01),低 4 位表示该符号的绑定属性为全局可见(值为 0x01)。
st_other 值为 0,表示该符号的其他属性为默认值。
st_shndx 值为 0,表示该符号所属的节为未定义。
第五个符号表项(位于偏移量 0x78 处)如下:
st_name 值为 0x00000020,在字符串表中对应字符串 "_IO_stdin_used"。
st_value 值为 0x0000000000000075,表示该符号在虚拟地址空间中的地址为 0x75。
st_size 值为 0,表示该符号的大小为 0。
st_info 值为 0x10,其中高 4 位表示该符号的类型为对象(值为 0x01),低 4 位表示该符号的绑定属性为全局可见(值为 0x01)。
st_other 值为 0,表示该符号的其他属性为默认值。
st_shndx 值为 0,表示该符号所属的节为未定义。
最后两个字节(位于偏移量 0x80 处)全为 0,表示 .dynsym 段解析结束。
2.2.9 [7] .dynstr 字符串表节: 用于存储动态链接器需要的符号名称字符串
.dynstr 节是一个字符串表节,用于存储动态链接器需要的符号名称字符串。
在 ELF 文件中,该节通常紧随 .dynsym
节之后,因为 .dynsym
节中的每个符号条目都包含一个指向对应符号名称在 .dynstr 节中的偏移量。
.dynstr 节的数据类型不是固定大小的,它只是一系列以 null 字符(‘\0’)结尾的字符串按顺序排列而成的字符数组。
在解析 .dynsym
节时,动态链接器通过读取 .dynstr
节中相应偏移量处的字符串来获取符号名称。
我们把.dynstr
节的内容打印出来,它的位置位于 14008 + 7x64 = 14456 byte
可以看出,它位于 0x0480
(1152
)处,大小为0x8f
(143 bytes
)
2.2.10 [8] .gnu.version 符号表节:存储与符号版本相关的信息
.gnu.version 节是 GNU 风格的版本符号表节,用于在 ELF 文件中存储与符号版本相关的信息。
通过使用 .gnu.version 节,我们可以将每个符号分配到不同的版本组中,并为每个版本组分配一个唯一的版本号。
.gnu.version 节由两部分组成:版本数组和版本需求数组。
其中,版本数组是一个包含所有版本号的字符串数组,它在整个 ELF 文件中只出现一次,并且以 null 字符(‘\0’)结尾。
而版本需求数组则包含一个或多个版本需求条目,每个条目包含一个符号的版本需求信息。
我们把.gnu.version
节的内容打印出来,它的位置位于 14008 + 8x64 = 14520 byte
可以看出,它位于 0x0510
处,大小为0x0e
,分析方法同上,就不一一分析了
其结构体定义如下:
struct Elf64_Verdef {
Elf64_Half vd_version; // 版本号
Elf64_Half vd_flags; // 标识位
Elf64_Half vd_ndx; // 版本索引
Elf64_Half vd_cnt; // 与此版本项相关联的符号数
Elf64_Word vd_hash; // 此版本项关联哈希表项的指针
Elf64_Word vd_aux; // 指向版本定义信息(Elf32_Verdaux)数组的指针
Elf64_Word vd_next; // 指向下一个版本定义条目的指针
};
2.2.11 [9] .gnu.version_r 节: 存储版本需求信息
.gnu.version_r
节是 ELF
文件中用于存储版本需求信息的节。
这些版本需求信息描述了一个 DSO
(动态共享对象) 文件需要哪些版本和符号,以及它们所在的位置等信息。
这些信息是在链接时由生成 DSO
的编译器和链接器自动生成的。
.gnu.version_r
节通常由多个版本需求条目组成。
每个版本需求条目都包含了一个 DSO
文件和一个或多个版本需求描述符,以指定该 DSO
文件需要哪些版本和符号。
我们把.gnu.version
节的内容打印出来,它的位置位于 14008 + 9x64 = 14584 byte
可以看出,它位于 0x0520
处,大小为0x30
,分析方法同上,就不一一分析了
.gnu.version_r
节的版本需求描述符的结构体定义如下:
struct Elf64_Vernaux {
Elf64_Word vna_hash; // 与该描述符关联的符号名哈希值
Elf64_Half vna_flags; // 描述符类型标志
Elf64_Half vna_other; // 其他描述符信息
Elf64_Word vna_name; // 与该描述符关联的符号名在字符串表中的偏移量
Elf64_Word vna_next; // 指向下一个版本需求描述符的偏移量
};
.gnu.version_r
节中某个 DSO
文件的版本需求条目的结构体定义:
struct Elf64_Verneed {
Elf64_Half vn_version; // 版本号
Elf64_Half vn_cnt; // 该 DSO 文件需求的版本数
Elf64_Word vn_file; // 与该需求条目关联的 DSO 文件名在字符串表中的偏移量
Elf64_Word vn_aux; // 指向版本需求描述符(Elf32_Vernaux)数组的偏移量
Elf64_Word vn_next; // 指向下一个版本需求条目的偏移量
};
.gnu.version_r
节和 .gnu.version
节一样,都需要编译器和链接器的支持来进行使用和管理。
2.2.12 [10] .rela.dyn 节:存储动态重定位表
.rela.dyn
这个节主要是用来存储动态重定位表的,它是基于 ELF 所采用的重定位机制实现的。
在编译时,由于符号的地址是不确定的,所以需要进行重定位处理,因此会生成一张重定位表来记录符号的位置以及需要做的重定位操作。
在动态链接时,这张表会被加载到内存中,并最终被用来对符号表中的符号进行重定位。
.rela.dyn
节中的重定位表项也称为重定位条目(relocation entry
),它的结构体定义如下:
struct Elf64_Rela {
Elf64_Addr r_offset; // 该条目所对应的符号地址,即需要被重定位的位置
Elf64_Word r_info; // 指定要进行重定位的符号以及重定位类型
Elf64_Sword r_addend; // 需要被加上或减去的常数值,用来计算出新的符号值
};
r_offset
是需要被重定位的位置,
r_info
指定了要进行重定位的符号以及重定位类型,
r_addend
则是需要被加上或减去的常数值,用来计算出新的符号值
我们把.rela.dyn
节的内容打印出来,它的位置位于 14008 + 10x64 = 14648 byte
可以看出,它位于 0x0550
处,大小为0xc0
,分析方法同上,就不一一分析了
2.2.13 [11] .rela.plt 节:存储 PLT(Procedure Linkage Table)的动态重定位表
.rela.plt
这个节主要是用来存储 PLT
(Procedure Linkage Table
)的动态重定位表,它同样是基于 ELF
所采用的重定位机制实现的。
在动态链接时,编译器为了实现延迟绑定(lazy binding
)以及减少重定位表的大小,会在 .plt
节中生成一张 PLT
表来进行优化。
该表中每个条目负责调度并执行某个特定的函数,
当这个函数第一次被调用时,PLT
中的条目会将控制权转移到 GOT
(Global Offset Table
)条目,并更新 GOT
条目中的地址。
在此之后,对该函数的所有调用都会直接跳转到 GOT
条目,从而实现延迟绑定的功能。
而 .rela.plt
节则记录了需要进行动态重定位的 PLT
条目的信息,这些信息包括 PLT
条目的偏移量、需要进行重定位的符号表索引以及重定位类型等。
具体而言,.rela.plt
节中的重定位表项的结构体定义如下:
struct Elf64_Rela {
Elf64_Addr r_offset; // 该条目所对应的符号地址,即需要被重定位的位置
Elf64_Word r_info; // 指定要进行重定位的符号以及重定位类型
Elf64_Sword r_addend; // 需要被加上或减去的常数值,用来计算出新的符号值
};
我们把.rela.plt
节的内容打印出来,它的位置位于 14008 + 11x64 = 14712 byte
可以看出,它位于 0x0610
处,大小为0x18
,分析方法同上,就不一一分析了
2.2.14 [12] .init 节:存储了在程序启动时需要执行的初始化代码
. init
节存储了在程序启动时需要执行的初始化代码,
这个节通常包含了一些需要在 main
函数之前运行的预处理代码,包括初始化全局变量、申请动态内存等。
在 ELF
文件被加载到内存中运行时,操作系统会自动在第一次执行 main
函数之前执行 .init
节中的代码,以保证程序能够正确运行。
因此,程序员可以利用 .init
节来进行各种初始化操作,从而提高程序的可靠性和安全性。
.init
节中存储的初始代码的结构体定义如下:
typedef void (*Elf64_Init)(void);
struct Elf64_Init_Array {
Elf64_Init *init_array; // 指向一组指针数组,每个指针指向一个函数入口地址
Elf64_Word init_array_sz; // init_array 数组长度
};
其中,ELF32_INIT
定义了函数指针类型,它指向一个没有参数和返回值的函数;
Elf32_Init_Array
结构体则包含了两个字段:
init_array
:一个指向函数指针数组的指针,每个指针都指向一个实现了.init
功能的函数入口地址;init_array_sz
:init_array
数组的长度,即该数组中指针的个数。
当 ELF
文件被加载到内存中并准备执行时,操作系统会找到 .init
节,然后遍历其中的 init_array
数组,按顺序依次执行其中存储的函数。这些函数就是用来实现在 main
函数之前应该执行的一系列初始化操作的。
.init
节只有在动态链接模式下才会被用到,在静态链接模式下是不需要使用 .init
节来实现初始化的。
我们把.init
节的内容打印出来,它的位置位于 14008 + 12x64 = 14776 byte
可以看出,它位于 0x1000 (4096)
处,大小为0x1b (27)
,
CielleeX:~/work/hello$ hexdump -C -s 4096 -n 27 hello
00001000 f3 0f 1e fa 48 83 ec 08 48 8b 05 d9 2f 00 00 48 |....H...H.../..H|
00001010 85 c0 74 02 ff d0 48 83 c4 08 c3 |..t...H....|
0000101b
将 .init 节的内容转化为如下汇编代码:
00001000: f3 0f 1e fa endbr64
00001004: 48 83 ec 08 sub rsp, 0x8
00001008: 48 8b 05 d9 2f 00 00 mov rax, QWORD PTR [rip+0x2fd9] # 0x200088
0000100f: 48 85 c0 test rax, rax
00001012: 74 02 je 0x16
00001014: ff d0 call rax
00001016: 48 83 c4 08 add rsp, 0x8
0000101a: c3 ret
解释如下:
-
f3 0f 1e fa
:
指令 endbr64 表示“将一个前缀字节用于指示下一个指针或调用目标地址是不是位于与代码段不同的内存区域。
“ 这条指令已经在特斯拉架构中被广泛使用,它是一种新的前缀指令,用于支持指向函数指针的内联 asm,
在新的特斯拉 CPU 中具有显著的性能优势。 -
48 83 ec 08
:
指令sub rsp, 0x8
将栈指针向下移动8
个字节,为后续代码的局部变量分配空间。 -
48 8b 05 d9 2f 00 00
:
指令mov rax, QWORD PTR [rip+0x2fd9]
将RIP
寄存器中指向下一条指令的地址加上偏移量0x2fd9
所指向的内存地址的值(即一个地址值)赋给RAX
寄存器,该地址值对应的是.got.plt
节中保存的一个函数的地址。 -
48 85 c0
:
指令test rax, rax
对RAX
执行逻辑与操作,并将结果设置到 CPU 标志寄存器中,这里的目的是检查 RAX 是否为 0。 -
74 02
:指令je 0x16
在RAX
等于0
时跳转到0x16
处。 -
ff d0
:指令call rax
调用RAX
寄存器中保存的函数。 -
48 83 c4 08
:指令add rsp, 0x8
将栈指针移回原来的位置,释放为局部变量分配的空间。 -
c3
:指令ret
返回主调函数。
总体来说,这段数据是用于初始化程序的状态或执行一些必要的操作。
其中调用的函数地址是在 .got.plt
节中进行了重定位的,这样可以避免在 ELF
文件中直接暴露函数地址,增加程序的安全性。
2.2.15 [13] .plt 节: 用于实现动态链接的延迟绑定机制(Lazy Binding)
. plt
节是 ELF
文件中的一个节,它主要用于实现动态链接的延迟绑定机制(Lazy Binding)。
具体而言,当一个程序调用一个动态链接库中的函数时,系统并不会立即将程序中的函数调用指向到动态链接库中真正的函数代码上,
而是先将这个调用指向到 .plt
节中的一个占位符函数中,
然后等到第一次真正调用该函数时再进行动态重定位,即将占位符函数指向到真正的函数代码上。
. plt
节的结构体定义如下:
typedef Elf32_Addr (*Elf32_Lookup)(Elf32_Sym *symtab, const char *strtab, Elf32_Addr rel_addr);
struct Elf64_Plt_Entry {
Elf64_insn insn1; // 两条机器码指令,实现了对 GOT 表项的加载和跳转
Elf64_insn insn2;
Elf64_Addr plt_addr; // PLT 表中的偏移地址,用于在 LD_PRELOAD 中进行函数劫持
Elf64_Addr got_addr; // 对应的 GOT 中的地址,用于实现延迟绑定功能
Elf64_Addr sym_addr; // 相应的符号表中的地址,用于后续的动态重定位
Elf64_Word sym_index; // 符号表中的索引值,表示该函数的符号表入口
Elf64_Addr jmp_addr; // 实际跳转到的地址,与 got_addr 相同,用于重定位
};
其中,Elf32_Lookup
定义了函数指针类型,它指向一个实现了符号查找功能的函数;
而 Elf32_Plt_Entry
结构体则包括了以下字段:
insn1
: PLT 表中对应的第一条机器指令,用于加载并跳转到 GOT 表中的相应项;insn2
: PLT 表中对应的第二条机器指令,用于实现 lazy binding 机制中的动态重定位;plt_addr
: PLT 表项在 .plt 节中的偏移地址;got_addr
: 对应的 GOT 表项的地址,该地址最初指向一个占位符函数(也称为延迟绑定函数)的地址;sym_addr
: 相应函数在符号表中的地址;sym_index
: 相应函数在符号表中的索引值;jmp_addr
: 最终跳转到的函数地址,通常会与 got_addr 相同,在动态重定位的时候将 got_addr 指向真正的函数入口地址即可。
2.2.16 [14] .plt.got 节:用于实现动态链接库中全局变量的延迟绑定
.plt.got
节是一个特殊的节,它用于实现动态链接库中全局变量的延迟绑定。
当程序中引用一个动态链接库中的全局变量时,
系统并不会立即将这个引用指向动态链接库中真正的变量地址,而是先将这个引用指向 .plt.got
节中的一个占位符变量地址,
然后等到第一次真正使用该全局变量时再进行动态重定位,即将占位符变量的地址指向真正的变量地址上。
. plt.got
节的结构体定义如下:
struct Elf64_Plt_Got_Entry {
Elf64_Addr jmp_slot; // 对应的 PLT 表项中的 jmp_addr 地址
Elf64_Addr value; // 对应的全局变量地址,在首次引用时需要进行动态重定位
};
其中,Elf64_Plt_Got_Entry
结构体包括了以下两个字段:
jmp_slot
: 对应的PLT
表项中的jmp_addr
地址;value
: 对应的全局变量地址,初始时指向一个占位符变量地址,在首次引用时需要进行动态重定位。
在程序首次访问动态链接库中的全局变量时,系统会对 .plt.got
节中对应的占位符变量进行动态重定位,将其指向真正的全局变量地址上。由于在动态链接库的生命周期内,一个全局变量只需要进行一次动态重定位即可,所以 .plt.got
节中的占位符变量地址是不需要像 . plt
表那样按需生成的。
2.2.17 [15] .plt.sec 节:用于实现对 PLT 表项中跳转到 GOT 表项地址的保护
.plt.sec
节是一个可选的节,它用于实现对 PLT
表项中跳转到 GOT
表项地址的保护。
由于在动态链接库加载时,攻击者可以通过篡改全局变量或符号表来修改 GOT
表项中的地址,从而实现代码劫持等攻击。
为了避免这种攻击,一种常见的方法是使用 .plt.sec
节将 PLT
表项中的跳转指令保护起来,使得攻击者无法篡改其中的地址。
. plt.sec
节的结构体定义如下:
struct Elf64_Plt_Sec_Entry {
Elf64_Addr insn1; // 原始的第一条机器码指令
Elf64_Addr insn2; // 原始的第二条机器码指令
Elf64_Addr insn3; // JMP 指令
Elf64_Word pad; // 对齐用的填充字节,大小根据机器架构而异
};
其中,Elf64_Plt_Sec_Entry 结构体包括了以下四个字段:
- insn1: PLT 表项中原始的第一条机器码指令;
- insn2: PLT 表项中原始的第二条机器码指令;
- insn3: JMP 指令,用于跳转到对应的 GOT 表项地址;
- pad: 对齐用的填充字节,大小根据机器架构而异。
在正常的动态链接操作中,PLT 表项和 GOT 表项是由静态链接器 (ld) 自动生成并填充的,
因此只要静态链接器的实现足够安全,就不必特地使用 .plt.sec 节来保护 PLT 表项。
但是为了防止动态链接库被攻击者篡改或替换,.plt.sec 节可以作为一种附加的安全措施,能够提高动态链接库的安全性。
2.2.18 [16] .text 节:存放程序代码的二进制指令
在 ELF 文件中,.text
节是一个必须存在的节,它用于存放程序代码的二进制指令。
具体而言,编译器会将源码编译成二进制指令,并将这些指令存储到 .text
节中。
在程序执行时,操作系统会从 .text
节中读取指令并依次执行,从而实现程序功能。
struct Elf64_Text_Entry {
unsigned char *data; // 指向 .text 节中的数据指针
Elf64_Word size; // .text 节中的数据大小,以字节为单位
Elf64_Addr addr; // .text 节的虚拟地址
};
其中,Elf64_Text_Entry 结构体包括了以下三个字段:
data
: 指向 .text 节中的数据指针;size
: .text 节中的数据大小,以字节为单位;addr
: .text 节的虚拟地址。
.text
节是只读的,因为它包含了程序的二进制代码,如果允许对其进行写入操作可能会破坏程序的内部逻辑。
2.2.19 [17] .fini 节:存储了程序结束时需要执行的代码
.fini
是一个 ELF
文件中的特殊节(section
),它存储了程序结束时需要执行的代码。
当程序正常退出时,动态加载器会在 .fini
节中查找指令,并执行这些指令,以执行一些清理工作和释放资源等操作。
.fini
节与.init
节的作用相反,.init
节在程序初始化时执行,.fini
节则在程序结束时执行。
2.2.20 [18] .rodata 节:存储只读数据,如常量
.rodata
是一个 ELF
文件中的节(section
),它存储了只读数据,包括字符串常量、全局常量等。
.rodata
节通常位于代码段和数据段之间,也可以放在一个单独的只读数据段中。
.rodata
节是一个只包含只读数据的段,在内存中只能读取该段的内容,无法修改。
.rodata
节的类型为 SHT_PROGBITS
,标志(flags
)中包含了 SHF_ALLOC
和 SHF_WRITE
标志,表示该节在内存中是可读写的。
2.2.21 [19] .eh_frame_hdr 节: 存储了 .eh_frame节中的调试信息的压缩版本,可以使调试信息的解析更加高效
.eh_frame_hdr
存储了 .eh_frame
节中的调试信息的压缩版本,可以使调试信息的解析更加高效。
.eh_frame
节存储了程序在执行过程中用到的信息,包括函数的返回地址、局部变量地址、异常处理等,用于程序的调试和分析。
在 ELF
文件中,.eh_frame_hdr
节位于 .eh_frame
节之前,并且与 .eh_frame
节相同的对齐方式。
.eh_frame_hdr
节包含了一张表格,用于描述 .eh_frame
节中的数据结构和压缩方式,以便更快速地解析出.eh_frame
节中的信息。
2.2.22 [20] .eh_frame 节:存储了异常处理框架信息,用于程序的调试和分析
.eh_frame
是一个 ELF
文件中的节(section
),它存储了异常处理框架(Exception Handling Frame
)信息,用于帮助程序在运行时定位函数调用链和栈帧信息,并实现 C++
异常处理机制。
.eh_frame
节通常位于代码段和数据段之间,也可以放在一个单独的只读数据段中。
.eh_frame
节是一个只包含只读数据的段,在内存中只能读取该段的内容,无法修改。
.eh_frame
节的类型为 SHT_PROGBITS
,标志(flags
)中包含了 SHF_ALLOC
和 SHF_WRITE
标志,表示该节在内存中是可读写的。
2.2.23 [21] .init_array 节:存储了在程序开始执行时需要运行的一系列初始化函数
.init_array
存储了在程序开始执行时需要运行的一系列初始化函数(init
函数)。
这些函数通常由编译器自动生成,在程序开始执行时按照数组顺序调用,用于进行一些初始化工作,如设置全局变量、打开日志文件等。
.init_array
节是一个只包含可执行代码的段,在内存中可以被读取和执行,但不允许写入操作。
.init_array
节中存储了一系列需要在程序开始执行时调用的初始化函数的指针,这些函数由编译器自动生成,并按照数组顺序存储在 .init_array
中,程序执行时会按顺序调用这些函数。
.init_array
节通常与其他初始化相关的节(如 .init、.ctors
等)配合使用,实现程序开始前的各种初始化工作。
2.2.24 [22] .fini_array 节:存储了在程序退出时需要运行的一系列清理函数
.fini_array
是一个 ELF 文件中的节,它存储了在程序退出时需要运行的一系列清理函数(fini
函数)。
这些函数通常由编译器自动生成,在程序退出时按照数组顺序调用,用于进行一些清理工作,如关闭日志文件、释放内存等。
.fini_array
节是一个只包含可执行代码的段,在内存中可以被读取和执行,但不允许写入操作。
2.2.25 [23] .dynamic 节:存储了动态链接时需要用到的信息,用于实现程序启动时的动态链接机制
.dynamic
存储了动态链接时需要用到的信息,用于实现程序启动时的动态链接机制(Dynamic Linking
)。
动态链接是指在程序运行时将依赖的库文件加载到内存中,并进行符号解析和重定位,这样可以减小程序的体积,提高可维护性。
.dynamic
节是一个只包含只读数据的段,在内存中只能读取该段的内容,无法修改。
2.2.26 [24] .got 节:存储了全局变量或函数的地址
.got
是一个 ELF
文件中的节,全称为 Global Offset Table
,它存储了全局变量或函数的地址。
在程序编译时,如果有对全局变量或函数的引用,编译器会把它们的地址写入 .got
节中。
当程序执行到这些代码段时,由于全局变量或函数的地址还没有确定,所以使用的是 .got
中的地址,而不是实际地址。
在程序执行时,动态链接器会将 .got
节中的地址替换为实际地址,从而实现全局变量或函数的动态链接。
.got
节是一个只包含地址的段,在内存中可以被读取和写入操作。
2.2.27 [25] .data 节:存储了程序在运行时需要用到的初始化的全局变量和静态变量的数据
.data
存储了程序在运行时需要用到的初始化的全局变量和静态变量的数据。
这些数据在程序启动时被复制到进程的数据段中,并在整个程序的执行过程中保持不变。
.data
节是一个包含已初始化数据的段,在内存中可以被读取、修改和执行。
我们把.data
节的内容打印出来,它的位置位于 14008 + 25x64 = 15608 byte
可以看出,它位于 0x3000 (12288)
处,大小为0x10 (16)
, 内容如下:
2.2.28 [26] .bss 节:存储了程序中未初始化的全局变量或静态变量的空间
.bss
全称为 Block Started by Symbol
,它存储了程序中未初始化的全局变量或静态变量的空间。
在程序编译时,如果有未初始化的全局变量或静态变量,编译器会把它们的大小信息记录在 .bss
节中。
当程序加载到内存时,操作系统会分配一段对应大小的空间,并把该空间清零,然后更新 .bss
节的地址信息。
因为未初始化的全局变量或静态变量默认值都是 0,所以这些空间不需要在文件中占用存储空间。
.bss
节是一个只包含空间大小信息的段,在内存中不能被读取和写入操作。
2.2.29 [27] .comment 节:包含了编译器和链接器的一些注释信息
.comment
包含了编译器和链接器的一些注释信息。
在程序编译或链接时,如果使用了 -g
选项,编译器和链接器会在 ELF
文件中添加一个 .comment
节,其中包含了一些注释信息。
这些注释信息主要用于调试和识别文件的来源。
.comment
节是一个只包含注释信息的段,其内容格式由编译器和链接器自己决定,通常为 ASCII
码文本。
2.2.30 [28] .symtab 节:存储程序中的符号表信息,包括全局变量、函数名和其他符号等
.symtab
用于存储程序中的符号表信息,包括全局变量、函数名和其他符号等。
符号表中的每个符号都有一个对应的符号表项,该表项是一个结构体,包含符号的名称、类型、大小等信息。
在链接时,链接器会把所有被引用的符号都加入到符号表中,并建立符号表和代码中的符号之间的映射关系。
在 ELF 文件中,.symtab
数据段的头部是一个 ELF
符号表头部(Elf32_Sym
或 Elf64_Sym
),用于描述符号表中每个符号表项的大小、数量和存储位置等信息。
每个符号表项本身也是一个结构体,其定义如下:
typedef struct {
Elf32_Word st_name; /* Symbol name (string table index) */
Elf32_Addr st_value; /* Symbol value */
Elf32_Word st_size; /* Symbol size */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf32_Section st_shndx; /* Section index */
} Elf32_Sym;
typedef struct {
Elf64_Word st_name; /* Symbol name (string table index) */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf64_Section st_shndx; /* Section index */
Elf64_Addr st_value; /* Symbol value */
Elf64_Xword st_size; /* Symbol size */
} Elf64_Sym;
这些字段的含义如下:
st_name
: 符号名字符串在字符串表中的偏移量。通过该偏移量可以在字符串表中查找到符号名。st_value
: 符号在内存中的地址或值。对于函数,通常是函数入口地址;对于变量,通常是变量在内存中的地址或初始值。st_size
: 符号的大小或长度(以字节为单位)。st_info
: 符号类型和绑定信息,编码方式如下:
BITS TYPE DESCRIPTION
0-3 Type Symbol type (ET_NONE, ET_REL, ET_EXEC, etc)
4-7 Bind Symbol binding (STB_LOCAL, STB_GLOBAL, etc)
总之,.symtab
数据段中存储了程序中所有的符号及其相关信息,提供了在链接期间进行符号解析和重定位的基础。
2.2.31 [29] .strtab 节: 存储程序中的字符串表信息
.strtab
是 ELF
格式可执行文件中的一种数据段,用于存储程序中的字符串表信息。
在 ELF
文件中,各个标记、符号等都需要使用字符串来表示,通过将这些字符串集中存储在 .strtab
数据段中,可以减小可执行文件的大小。
.strtab
数据段中的字符串是以 null
结尾的 C 字符串(即以'\0'
字符结尾的字符串),每个字符串以一个偏移量(offset
)来进行索引。
因此,在 ELF 文件解析过程中,需要通过访问该数据段来获取相关字符串信息,比如符号表中的符号名称等。
字符串表中的每个字符串则是按照顺序存放的。
总而言之,.strtab
数据段是一个常用的数据段,用于存储程序中的字符串信息,提供了在 ELF 文件解析过程中获取字符串信息的基础。
2.3 Program Headers:解析13个segment段 及 对应的虚拟地址和物理地址
详见: 《【Linux0.11代码分析】10 之 ELF可执行程序03 - Program Headers解析》
描述了各个 segment 段的虚拟地址 和 物理地址,及其对应的权限 RW
Program Headers:
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 0x00000000000001e5 0x00000000000001e5 R E 0x1000
LOAD 0x0000000000002000 0x0000000000002000 0x0000000000002000 0x000000000000012c 0x000000000000012c 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 0x0000000000002024 0x0000000000002024 0x0000000000002024 0x000000000000003c 0x000000000000003c R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x0000000000002db8 0x0000000000003db8 0x0000000000003db8 0x0000000000000248 0x0000000000000248 R 0x1
2.4 Program Headers:解析13个segment 段和31个section节的映射关系
详见: 《【Linux0.11代码分析】10 之 ELF可执行程序03 - Program Headers解析》
可以看出,.init_array .fini_array .dynamic .got .data .bss
这个section
节是保存在第6个 segment
段中
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