1. 从操作系统角度看看可执行文件的加载
1.1进程建立过程
1. 创建一个独立的虚拟地址空间
- 操作系统通过 execve() 系统调用为新进程创建一个干净的虚拟地址空间。
- 清理旧地址空间中的内容(如代码段、数据段、堆栈等)。
- 分配基本的地址空间结构,包括 用户态内存区域 和 内核态内存区域。
- 创建虚拟地址与物理地址的映射(可做)
2. 读取可执行文件头, 建立 虚拟空间与可执行文件的映射关系
- 检查是否为有效的 ELF 文件(ELF magic number 等)。
根据 ELF 文件头解析 Program Header 表,获取各段的信息:
- .text(代码段):映射为只读且可执行。
- .data(已初始化数据段):映射为可读写。
- .bss(未初始化数据段):分配物理内存。
- .rodata(只读数据段):映射为只读。
- 用系统调用(如 mmap)将文件的各个段映射到进程的虚拟地址空间。
3. 将CPU指令寄存器设置为可执行文件文件头
- ELF 文件头中指定了程序的入口地址(Entry Point)。
- 设置好初始堆栈内容(环境变量、命令行参数等)。
- 切换到用户态,开始执行从入口地址开始的代码(通常是 _start 函数)。
1.2上述的几个概念描述
1. 节(Section)和段(Segment)的区别
| 属性 | 节 (Section) | 段 (Segment) |
|---|---|---|
| 面向对象 | 编译器、链接器 | 操作系统 |
| 层次 | 文件层次 | 文件到内存的映射层次 |
| 数量 | 通常较多 | 通常较少 |
| 冗余信息 | 包含调试和符号信息,较为冗余 | 仅保留运行时加载必要信息 |
| 是否映射到内存 | 只有部分节被映射 | 定义了所有需要映射的内容 |
为什么设计了 节 还要设计 段?
- 节:服务于编译器和链接器,用于组织代码和数据,包含所有的程序信息,包括调试符号表、元数据等。
- 段:服务于操作系统和运行时,告诉操作系统如何加载程序到内存,包括哪些内容需要加载、加载到哪里,以及加载后的权限。
2. 为什么有了节表(Section Header 表,SH 表) 还需要 程序头表(Program Header 表,PH 表)
PH 表的作用:从文件到内存的桥梁
ELF 文件中有两个主要表:
- 节表(Section Header 表,SH 表):描述文件中各个节的细节,如 .text(代码)、.data(初始化数据)、.bss(未初始化数据)等。主要用于编译、链接和调试。
- 程序头表(Program Header 表,PH 表):描述段(Segment)的信息,指导操作系统如何将 ELF 文件加载到内存中运行。
区别在于:
- SH 表是文件视角,面向编译器和链接器。
- PH 表是运行时视角,面向操作系统和动态链接器。
没有 PH 表,操作系统不知道如何加载 ELF 文件
如果没有 PH 表,操作系统根本无法确定:
- 文件中的哪些部分需要加载。
- 加载后它们该映射到内存中的什么地方。
3. 节(Section)和段(Segment)映射合并规则
- 节(Section) 是编译器生成的逻辑单元,用于表示文件中的数据和指令内容。 主要存于节表。
- 段(Segment) 是操作系统加载可执行文件时使用的内存单元。 主要存于Program Header 表。
目标文件的节信息
| 节名 | 属性 | 大小 | 描述 |
|---|---|---|---|
.text | 可执行 + 只读 | 1000 | 存储程序指令 |
.data | 可写 + 初始化 | 200 | 存储已初始化的全局变量 |
.bss | 可写 + 未初始化 | 300 | 存储未初始化的全局变量 |
.rodata | 只读 | 150 | 存储只读的全局数据(常量) |
.debug | 调试信息(不加载内存) | 500 | 存储调试符号表 |
合并后生成的段
| 段名 | 包含节 | 属性 | 大小 | 描述 |
|---|---|---|---|---|
| 代码段 | .text | 可执行 + 只读 | 1000 | 加载到内存运行指令 |
| 数据段 | .data + .rodata | 可写 + 初始化 | 350 | 加载到内存运行全局变量 |
| BSS 段 | .bss | 可写 + 未初始化 | 300 | 加载到内存,文件中不占空间 |
| 调试段 | .debug | 调试信息(不加载内存) | 500 | 仅供调试使用 |
代码举例
1
2
3
4
5
# 查看节头表(Section Header Table, SHT) 查节
readelf -S <binary_file>
# 查看程序头表(Program Header Table, PHT) 查段
readelf -l <binary_file>
4. 段与VMA
VMA 是操作系统为进程维护的虚拟内存区域,通常由内核内存管理子系统(如 Linux 的 mm_struct)管理。 每个进程有多个 VMA,每个 VMA 表示一个连续的虚拟地址区间,通常与段或文件映射相关。
1
2
3
4
5
6
程序段 (Segments): 虚拟内存区域 (VMAs):
[代码段] .text -------> VMA1: [0x00400000 - 0x0040FFFF] R-X
[数据段] .data -------> VMA2: [0x00600000 - 0x00600FFF] RW-
[BSS 段] .bss -------> VMA3: [0x00601000 - 0x00601FFF] RW-
[堆段] -------> VMA4: [0x00700000 - 0x0080FFFF] RW-
[栈段] -------> VMA5: [0xFFFFE000 - 0xFFFFFFFF] RW-
- 段是程序逻辑上的划分,VMA 是操作系统对内存管理的实现。
每个段通常会对应一个或多个 VMA,但 VMA 可能比段更细粒度,也更灵活。(例如,一个 .text 段的结尾部分包含了跳转表(需要写权限),系统会为其创建一个新的 VMA。)
1 2
VMA1: [0x00400000 - 0x0040FFFF] R-X (普通代码) VMA2: [0x00410000 - 0x00410FFF] RW- (跳转表)
- 多个段也可以合并成一个VMA(节省内存), 如将
.text.init合并成为一个Segment, 装载的时候可以看为一个整体VMA. - 两者在权限、大小和位置上是一致的,但 VMA 是段映射到操作系统内存模型的结果。
2. 进程的虚拟空间分布
2.1 再聊 EFL 文件链接视图与执行视图
1
2
3
4
5
6
7
8
9
10
11
// 编译 静态链接 gcc -static SectionMapping.c -o SectionMapping.elf
#include <stdlib.h>
#include <unistd.h>
int main() {
while(1) {
sleep(1000);
}
return 0;
}
1. 链接视图 Section Header
节表 和 efl 中的对应节
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
xm@xm:~/xx/complier$ readelf -S SectionMapping.elf
There are 32 section headers, starting at offset 0xd4618:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .note.gnu.propert NOTE 0000000000400270 00000270
0000000000000020 0000000000000000 A 0 0 8
[ 2] .note.gnu.build-i NOTE 0000000000400290 00000290
0000000000000024 0000000000000000 A 0 0 4
[ 3] .note.ABI-tag NOTE 00000000004002b4 000002b4
0000000000000020 0000000000000000 A 0 0 4
[ 4] .rela.plt RELA 00000000004002d8 000002d8
0000000000000240 0000000000000018 AI 0 20 8
[ 5] .init PROGBITS 0000000000401000 00001000
000000000000001b 0000000000000000 AX 0 0 4
[ 6] .plt PROGBITS 0000000000401020 00001020
0000000000000180 0000000000000000 AX 0 0 16
[ 7] .text PROGBITS 00000000004011a0 000011a0
0000000000091b50 0000000000000000 AX 0 0 16
[ 8] __libc_freeres_fn PROGBITS 0000000000492cf0 00092cf0
0000000000001ca0 0000000000000000 AX 0 0 16
[ 9] .fini PROGBITS 0000000000494990 00094990
000000000000000d 0000000000000000 AX 0 0 4
[10] .rodata PROGBITS 0000000000495000 00095000
000000000001bfec 0000000000000000 A 0 0 32
[11] .stapsdt.base PROGBITS 00000000004b0fec 000b0fec
0000000000000001 0000000000000000 A 0 0 1
[12] .eh_frame PROGBITS 00000000004b0ff0 000b0ff0
000000000000a63c 0000000000000000 A 0 0 8
[13] .gcc_except_table PROGBITS 00000000004bb62c 000bb62c
00000000000000b1 0000000000000000 A 0 0 1
[14] .tdata PROGBITS 00000000004bd0c0 000bc0c0
0000000000000020 0000000000000000 WAT 0 0 8
[15] .tbss NOBITS 00000000004bd0e0 000bc0e0
0000000000000040 0000000000000000 WAT 0 0 8
[16] .init_array INIT_ARRAY 00000000004bd0e0 000bc0e0
0000000000000010 0000000000000008 WA 0 0 8
[17] .fini_array FINI_ARRAY 00000000004bd0f0 000bc0f0
0000000000000010 0000000000000008 WA 0 0 8
[18] .data.rel.ro PROGBITS 00000000004bd100 000bc100
0000000000002df4 0000000000000000 WA 0 0 32
[19] .got PROGBITS 00000000004bfef8 000beef8
00000000000000f0 0000000000000000 WA 0 0 8
[20] .got.plt PROGBITS 00000000004c0000 000bf000
00000000000000d8 0000000000000008 WA 0 0 8
[21] .data PROGBITS 00000000004c00e0 000bf0e0
0000000000001a50 0000000000000000 WA 0 0 32
[22] __libc_subfreeres PROGBITS 00000000004c1b30 000c0b30
0000000000000048 0000000000000000 WA 0 0 8
[23] __libc_IO_vtables PROGBITS 00000000004c1b80 000c0b80
00000000000006a8 0000000000000000 WA 0 0 32
[24] __libc_atexit PROGBITS 00000000004c2228 000c1228
0000000000000008 0000000000000000 WA 0 0 8
[25] .bss NOBITS 00000000004c2240 000c1230
0000000000001718 0000000000000000 WA 0 0 32
[26] __libc_freeres_pt NOBITS 00000000004c3958 000c1230
0000000000000028 0000000000000000 WA 0 0 8
[27] .comment PROGBITS 0000000000000000 000c1230
000000000000002b 0000000000000001 MS 0 0 1
[28] .note.stapsdt NOTE 0000000000000000 000c125c
00000000000013e8 0000000000000000 0 0 4
[29] .symtab SYMTAB 0000000000000000 000c2648
000000000000afb0 0000000000000018 30 735 8
[30] .strtab STRTAB 0000000000000000 000cd5f8
0000000000006ec5 0000000000000000 0 0 1
[31] .shstrtab STRTAB 0000000000000000 000d44bd
0000000000000157 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
2. 装载&运行视图 Program Header
Segment 表, 一个段就是一个VMA, 只用关注LOAD 段。
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
xm@xm:~/xx/complier$ readelf -l SectionMapping.elf
Elf file type is EXEC (Executable file)
Entry point 0x401b90
There are 10 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x0000000000000518 0x0000000000000518 R 0x1000
LOAD 0x0000000000001000 0x0000000000401000 0x0000000000401000
0x000000000009399d 0x000000000009399d R E 0x1000
LOAD 0x0000000000095000 0x0000000000495000 0x0000000000495000
0x00000000000266dd 0x00000000000266dd R 0x1000
LOAD 0x00000000000bc0c0 0x00000000004bd0c0 0x00000000004bd0c0
0x0000000000005170 0x00000000000068c0 RW 0x1000
NOTE 0x0000000000000270 0x0000000000400270 0x0000000000400270
0x0000000000000020 0x0000000000000020 R 0x8
NOTE 0x0000000000000290 0x0000000000400290 0x0000000000400290
0x0000000000000044 0x0000000000000044 R 0x4
TLS 0x00000000000bc0c0 0x00000000004bd0c0 0x00000000004bd0c0
0x0000000000000020 0x0000000000000060 R 0x8
GNU_PROPERTY 0x0000000000000270 0x0000000000400270 0x0000000000400270
0x0000000000000020 0x0000000000000020 R 0x8
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x00000000000bc0c0 0x00000000004bd0c0 0x00000000004bd0c0
0x0000000000002f40 0x0000000000002f40 R 0x1
Section to Segment mapping:
Segment Sections...
00 .note.gnu.property .note.gnu.build-id .note.ABI-tag .rela.plt
01 .init .plt .text __libc_freeres_fn .fini
02 .rodata .stapsdt.base .eh_frame .gcc_except_table
03 .tdata .init_array .fini_array .data.rel.ro .got .got.plt .data __libc_subfreeres __libc_IO_vtables __libc_atexit .bss __libc_freeres_ptrs
04 .note.gnu.property
05 .note.gnu.build-id .note.ABI-tag
06 .tdata .tbss
07 .note.gnu.property
08
09 .tdata .init_array .fini_array .data.rel.ro .got
总的来说, Section 和 Segment 就是从 链接 和 执行 视图来看ELF文件。
2.2 聊聊堆 和 栈
程序的内存映射区域,以及 ELF 文件中段和 VMA 的关系
1
2
3
4
5
6
7
8
9
10
11
12
13
14
xm@xm:~/xx/complier$ ./SectionMapping.elf &
[1] 35142
xm@xm:~/xx/complier$ cat /proc/35142/maps
00400000-00401000 r--p 00000000 08:05 6946957 /home/xm/xx/complier/SectionMapping.elf
00401000-00495000 r-xp 00001000 08:05 6946957 /home/xm/xx/complier/SectionMapping.elf
00495000-004bc000 r--p 00095000 08:05 6946957 /home/xm/xx/complier/SectionMapping.elf
004bd000-004c0000 r--p 000bc000 08:05 6946957 /home/xm/xx/complier/SectionMapping.elf
004c0000-004c3000 rw-p 000bf000 08:05 6946957 /home/xm/xx/complier/SectionMapping.elf
004c3000-004c4000 rw-p 00000000 00:00 0
050aa000-050cd000 rw-p 00000000 00:00 0 [heap]
7ffedc961000-7ffedc982000 rw-p 00000000 00:00 0 [stack]
7ffedc99d000-7ffedc9a1000 r--p 00000000 00:00 0 [vvar]
7ffedc9a1000-7ffedc9a3000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]
1. ELF 文件映射
1
2
3
4
5
00400000-00401000 r--p 00000000 08:05 6946957 /home/xm/xx/complier/SectionMapping.elf
00401000-00495000 r-xp 00001000 08:05 6946957 /home/xm/xx/complier/SectionMapping.elf
00495000-004bc000 r--p 00095000 08:05 6946957 /home/xm/xx/complier/SectionMapping.elf
004bd000-004c0000 r--p 000bc000 08:05 6946957 /home/xm/xx/complier/SectionMapping.elf
004c0000-004c3000 rw-p 000bf000 08:05 6946957 /home/xm/xx/complier/SectionMapping.elf
- 每一行代表一个 VMA(虚拟内存区域),通常映射到 ELF 文件中的一个或多个段。
- 权限标志:
- r–p: 只读,不可执行。
- r-xp: 只读 + 可执行。
- rw-p: 可读 + 可写,不可执行。
对应ELF文件段
- .text 段(代码段):通常映射到 r-xp 区域,对应程序的指令。
- .rodata 段(只读数据段):映射到 r–p 区域,对应只读的全局数据。
- .data 段(已初始化数据段):映射到 rw-p 区域,对应可写的全局变量。
- .bss 段(未初始化数据段):没有实际文件内容,仅分配内存(rw-p 部分的 0 填充区域)。
2. 堆
1
050aa000-050cd000 rw-p 00000000 00:00 0 [heap]
3. 栈
1
7ffedc961000-7ffedc982000 rw-p 00000000 00:00 0 [stack]
4. vvar 和 vdso
- vvar(Virtual Variable):虚拟变量区域,提供高效访问某些内核数据。
- vdso(Virtual Dynamically-linked Shared Object):用于加速系统调用(如 gettimeofday),通过用户空间直接访问。
3. 32 位中, 4G的虚拟空间, 为什么不可以都给进程使用 ? 要保留1G 内核空间
32 位系统通常采用 3GB 用户空间 + 1GB 内核空间 的分配策略:
- 3GB 用户空间:为用户进程分配虚拟地址。
- 1GB 内核空间:为内核专用,所有进程共享这一部分空间。
内核区的虚拟地址在实际访问时,会映射到物理内存中的某些区域,具体如何映射取决于内核的内存管理机制和硬件支持。
内核虚拟地址与物理内存的映射
- 内核的虚拟地址空间(通常是 0xC0000000 到 0xFFFFFFFF,约 1GB 空间)主要映射的是物理内存的高地址部分。
- 这个映射是固定的,由内核在启动时通过页表设置,直接将内核虚拟地址空间映射到物理内存。
32 位系统下,假设物理内存为 4GB:
- 物理地址 0x00000000 到 0xBFFFFFFF 映射到用户空间(3GB)。
- 物理地址 0xC0000000 到 0xFFFFFFFF 映射到内核空间(1GB)。
内核空间的虚拟地址会直接对应到 物理地址的高地址部分。