Linux 进程地址空间划分详解
一、进程虚拟地址空间结构概览
Linux 下每个进程拥有独立的虚拟地址空间,通常 64 位进程的用户空间地址范围是从 0x0000000000000000
到 0x00007fffffffffff
(约128TB),但操作系统对不同区域有明确划分,常见布局如下(假设 x86_64 Linux):
+-------------------------------+ 0x7fffffffffff (最高地址)
| 用户栈(stack) | -- 从高地址向低地址生长
+-------------------------------+
| mmap 区域 | -- 动态映射区域,靠近栈
+-------------------------------+
| 堆(heap) | -- 动态分配内存(malloc)
+-------------------------------+
| 数据段(data) | -- 静态变量,初始化数据
+-------------------------------+
| 代码段(text/code) | -- 程序指令(只读)
+-------------------------------+ 0x0000000000400000 (通常程序起始地址)
| 内核空间(kernel) | -- 进程不可访问
+-------------------------------+ 0x0000000000000000 (最低地址)
二、各个区域详细说明
1. 代码段(Text Segment)
-
地址位置:通常从较低地址开始,固定位置,程序的入口地址。
-
内容:存放程序机器码指令。
-
权限:只读且可执行,防止代码被意外修改。
-
特点:
- 由编译器和链接器生成的
.text
段组成。 - 通常是只读且共享的,比如多个相同程序的进程可以共享代码段。
- 由编译器和链接器生成的
-
大小:程序的指令大小,静态固定。
2. 数据段(Data Segment)
-
地址位置:紧接代码段之后。
-
内容:存放全局变量和静态变量。
-
分为两个子区域:
- 已初始化数据段(.data) :初始化的全局变量,比如
int x = 5;
。 - 未初始化数据段(BSS) :未初始化的全局变量,运行时被置为0,比如
static int y;
。
- 已初始化数据段(.data) :初始化的全局变量,比如
-
权限:可读写。
3. 堆(Heap)
-
地址位置:紧挨数据段之后,向高地址扩展。
-
用途 :程序运行时动态分配内存区域,
malloc
、new
等调用的内存都会从堆中分配。 -
特点:
- 大小不固定,运行时可以增长或缩小(通过
brk
/sbrk
系统调用调整堆尾)。 - 堆的起始地址是固定的,但堆顶会动态变化。
- 堆的内存管理通常由用户空间的分配器(如 ptmalloc)控制。
- 大小不固定,运行时可以增长或缩小(通过
-
示意:
| Data Segment | Heap ↑ grows upwards → |
4. 栈(Stack)
-
地址位置:在虚拟地址的高端,通常从高地址向低地址生长。
-
用途:函数调用时保存局部变量、返回地址、函数参数等。
-
特点:
- 栈空间自动管理,函数进入/退出时自动分配和回收。
- 栈大小有限,超过会触发"栈溢出"。
- 线程独立,每个线程有自己的栈空间。
-
权限:可读写,通常支持"栈保护"防止溢出攻击。
5. mmap 区域
-
位置:位于栈和堆之间,靠近栈的较高地址区域。
-
用途:
- 用于内存映射文件或设备(如通过
mmap
系统调用)。 - 加载共享库(so 文件)映射区域。
- 共享内存、匿名映射等。
- 用于内存映射文件或设备(如通过
-
特点:
- 动态分配,大小不固定。
- 可能向高地址或者低地址增长,灵活管理。
- 权限灵活,依映射文件属性决定(可读、写、执行)。
三、内存区域对应的管理机制和系统调用
区域 | 常见系统调用 / 机制 | 说明 |
---|---|---|
代码段 | execve() |
进程启动时,加载 ELF 文件的代码段 |
数据段 | execve() |
加载 ELF 文件的 .data 和 .bss |
堆 | brk() , sbrk() |
调整堆的大小 |
mmap 区 | mmap() , munmap() , mprotect() |
内存映射操作,用于文件、设备、匿名映射 |
栈 | 由内核自动管理,大小可通过 ulimit -s 设定 |
栈溢出检查,栈保护机制 |
四、动态链接库的映射
- 共享库(.so)会被动态映射进进程的 mmap 区域。
- 这些库的代码和数据分别映射为只读可执行和可读写段。
- 共享库映射允许多个进程共享同一份代码节省内存。
五、示意图
0x7fffffffffff ← 用户栈高地址(stack grows down)
+--------------------+
| 栈区域 |
+--------------------+
| mmap 区(动态映射) |
+--------------------+
| |
| |
| 堆 ↑ |
+--------------------+
| 数据段 |
+--------------------+
| 代码段 |
+--------------------+
0x0000000000000000
六、内存保护和安全机制
-
段保护:代码段通常是只读可执行,防止程序自修改代码。
-
地址空间布局随机化(ASLR):
- 堆、栈、mmap 区都会随机化起始地址,增加攻击难度。
-
栈保护:
- 栈溢出检测(如
stack canary
) - 非执行栈(NX bit)
- 栈溢出检测(如
七、总结
区域 | 作用 | 访问权限 | 生长方向 | 分配方式 |
---|---|---|---|---|
代码段 | 程序机器指令 | 只读+可执行 | 固定不变 | 静态编译生成 |
数据段 | 静态/全局变量 | 读写 | 固定不变 | 静态编译生成 |
堆 | 动态内存分配 | 读写 | 向高地址增长 | brk/sbrk 或 mmap |
栈 | 函数调用及局部变量 | 读写 | 向低地址增长 | 内核自动管理 |
mmap | 内存映射文件/库 | 灵活 | 动态调整 | mmap 系统调用 |
八、扩展
=> 是否bss段都会默认初始化0?
大多数现代操作系统和主流编译器(如 GCC、Clang、MSVC)都会遵循 C/C++ 语言规范 和 目标文件格式(如 ELF、PE)规范 ,将 未初始化的全局/静态变量 (包括局部静态变量)放入 .bss
段,并在程序加载时由 操作系统自动置零。
- 语言标准的要求(C/C++)
根据 C/C++ 语言标准:
所有在编译期分配的、未显式初始化的 全局变量、静态变量、未初始化的局部静态变量,其初始值默认为 0(或 nullptr)。
cpp
int a; // 全局变量,默认初始化为 0
static int b; // 静态变量,默认初始化为 0
无论使用哪个符合标准的编译器(GCC、Clang、MSVC),其值在程序运行时都会是 0 ,保证行为一致性。
- 系统层面(操作系统负责清零
.bss
段)
以 ELF 格式为例(Linux、Unix):
.bss
段在目标文件中不占据实际空间(节省磁盘空间),只是记录了需要多大的未初始化内存。- 程序启动时,操作系统的程序加载器(loader)自动分配
.bss
段空间,并清零。
在 Windows 中的 PE 格式也类似:.bss
由 .data
段的未初始化部分表示,系统加载时清零。
例外:手动分配内存或禁用初始化的情况
若使用:
cpp
int* p = (int*)malloc(sizeof(int)); // malloc 不清零!
int* q = new int; // new 不一定清零(除非 new int())
这种情况,变量不会自动初始化为 0 。此时就和 .bss
无关,由程序员负责初始化。
验证方式
可以通过反汇编或使用 readelf -S
查看 .bss
是否存在、是否包含某变量:
bash
readelf -S your_program | grep bss
也可以写个例子:
cpp
#include <stdio.h>
static int a;
int b;
int main() {
printf("a = %d, b = %d\n", a, b); // 必定输出:0 0
return 0;
}
总结
条件 | 是否自动初始化为 0 | 存储段 | 谁负责清零 |
---|---|---|---|
未初始化全局变量 | 是 | .bss |
操作系统 loader |
未初始化静态变量 | 是 | .bss |
操作系统 loader |
局部变量(非 static) | 否(未定义行为) | 栈 | 不清零,由程序员负责 |
动态分配(malloc/new) | 否(除非手动清零) | 堆 | 不清零,由程序员负责 |
所以答案是:
是,所有标准编译器和操作系统都会对
.bss
段中的变量自动置零,但仅限于编译期分配的未初始化变量。