02-内存布局详解¶
重要性:⭐⭐⭐⭐⭐ 实用度:⭐⭐⭐⭐⭐ 学习时间:3天 必须掌握:是
为什么学这一章?¶
内存是程序运行的核心资源。理解内存布局能帮助你: - 理解变量存储在哪里(栈、堆、数据段) - 避免内存相关的bug(段错误、内存泄漏) - 优化程序的内存使用 - 理解虚拟内存的工作原理
学完这一章,你将能够: - ✅ 画出进程的完整内存布局图 - ✅ 解释不同存储区域的特点和用途 - ✅ 理解虚拟地址到物理地址的映射 - ✅ 使用工具观察程序的内存使用情况
📖 核心概念¶
1. 进程的虚拟地址空间¶
每个进程都有自己独立的虚拟地址空间,从0开始到一个最大值(64位系统上是2^64):
┌─────────────────────────────────────────────────────────────────┐
│ 64位Linux进程的虚拟地址空间 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 高地址 0xFFFFFFFFFFFFFFFF │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 内核空间(128TB) │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ 内核代码和数据 │ │ │
│ │ │ 物理内存映射 │ │ │
│ │ │ 内核堆栈 │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ │ 用户程序无法直接访问,通过系统调用进入 │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ (空洞 - 未使用) │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 栈(Stack) │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ ↓ 向下增长 │ │ │
│ │ │ 局部变量 │ │ │
│ │ │ 函数参数 │ │ │
│ │ │ 返回地址 │ │ │
│ │ │ 寄存器保存 │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ (空洞 - 动态扩展) │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 内存映射区域(mmap) │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ 动态链接库(.so文件) │ │ │
│ │ │ 文件映射 │ │ │
│ │ │ 匿名映射 │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ (空洞 - 动态扩展) │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 堆(Heap) │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ ↑ 向上增长 │ │ │
│ │ │ malloc/new分配的内存 │ │ │
│ │ │ 动态数据结构 │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ BSS段 │ │
│ │ 未初始化的全局变量和静态变量(自动初始化为0) │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 数据段(Data) │ │
│ │ 已初始化的全局变量和静态变量 │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 代码段(Text) │ │
│ │ 程序的机器指令(只读) │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ 低地址 0x0000000000000000 │
│ │
└─────────────────────────────────────────────────────────────────┘
用户空间:128TB(0x0000_0000_0000_0000 ~ 0x0000_7FFF_FFFF_FFFF)
内核空间:128TB(0xFFFF_8000_0000_0000 ~ 0xFFFF_FFFF_FFFF_FFFF)
2. 各内存区域详解¶
2.1 代码段(Text Segment)¶
存储内容: - 程序的机器指令(编译后的代码) - 只读常量(如字符串字面量,某些编译器放在.rodata)
特点: - 只读:防止程序意外修改自己的代码 - 共享:多个进程可以共享同一份代码(如运行多个bash实例) - 固定大小:程序运行时不会改变
示例:
// code_segment.cpp
#include <iostream>
void function() { // 函数代码存储在代码段
std::cout << "Hello" << std::endl;
}
int main() {
// &function 获取函数地址,位于代码段
std::cout << "Function address: " << (void*)function << std::endl;
return 0;
}
2.2 数据段(Data Segment)¶
存储内容: - 已初始化的全局变量 - 已初始化的静态变量
特点: - 可读可写 - 程序启动时从可执行文件加载初始值 - 生命周期:整个程序运行期间
示例:
// data_segment.cpp
#include <iostream>
int global_var = 42; // 数据段
static int static_var = 100; // 数据段
void func() {
static int local_static = 200; // 数据段(不是栈!)
}
int main() {
std::cout << "&global_var: " << &global_var << std::endl;
std::cout << "&static_var: " << &static_var << std::endl;
return 0;
}
2.3 BSS段(Block Started by Symbol)¶
存储内容: - 未初始化的全局变量 - 未初始化的静态变量
特点: - 不占用磁盘空间(在ELF文件中只记录大小) - 程序加载时由操作系统初始化为0 - 可读可写
示例:
// bss_segment.cpp
#include <iostream>
int global_uninit; // BSS段
static int static_uninit; // BSS段
void func() {
static int local_static_uninit; // BSS段
}
int main() {
// 这些变量自动初始化为0
std::cout << "global_uninit: " << global_uninit << std::endl; // 输出0
return 0;
}
2.4 栈(Stack)¶
存储内容: - 局部变量 - 函数参数 - 返回地址 - 保存的寄存器
特点: - 自动管理:函数调用时分配,返回时释放 - 向下增长:从高地址向低地址增长 - 大小有限:通常8MB(可调整) - 速度快:CPU有专门的栈指针寄存器(RSP/ESP)
栈帧结构:
┌─────────────────────────────────────┐ ← 高地址
│ 调用者的栈帧 │
├─────────────────────────────────────┤
│ 参数 n │
│ 参数 n-1 │
│ ... │
│ 参数 1 │
├─────────────────────────────────────┤
│ 返回地址 ← 调用时自动压入 │
├─────────────────────────────────────┤ ← 当前栈帧开始(RBP)
│ 保存的旧的RBP ← 建立栈帧时压入 │
├─────────────────────────────────────┤
│ 局部变量 1 │
│ 局部变量 2 │
│ ... │
│ 局部变量 n │
├─────────────────────────────────────┤
│ (临时空间/寄存器保存) │
├─────────────────────────────────────┤ ← 栈顶(RSP)
│ 未使用空间 │
└─────────────────────────────────────┘ ← 低地址
示例:
// stack_example.cpp
#include <iostream>
void func(int a, int b) {
int local = a + b; // 局部变量在栈上
std::cout << "&local: " << &local << std::endl;
}
int main() {
int x = 10; // 局部变量在栈上
func(1, 2);
return 0;
}
栈溢出(Stack Overflow):
// 导致栈溢出的代码
void infinite_recursion() {
char buffer[1024]; // 每次调用占用1KB栈空间
infinite_recursion(); // 无限递归
}
// 解决方法:
// 1. 避免无限递归
// 2. 减少大数组的使用(改用堆分配)
// 3. 增加栈大小(ulimit -s)
2.5 堆(Heap)¶
存储内容: - 动态分配的内存(malloc/new) - 动态数据结构(链表、树等)
特点: - 手动管理:程序员负责分配和释放 - 向上增长:从低地址向高地址增长 - 大小灵活:受限于系统的虚拟内存 - 速度较慢:需要系统调用和内存管理
内存分配过程:
┌─────────────────────────────────────────────────────────────┐
│ 堆内存分配过程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 初始状态 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 堆起始 │ │
│ │ ┌─────────┐ │ │
│ │ │ 未分配 │ ← 程序中断(brk) │ │
│ │ └─────────┘ │ │
│ │ 堆结束 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ malloc(100) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 堆起始 │ │
│ │ ┌─────────┬────────────────────────┐ │ │
│ │ │ 已分配 │ 未分配 │ ← brk上移 │ │
│ │ │ 100B │ │ │ │
│ │ └─────────┴────────────────────────┘ │ │
│ │ 堆结束 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ malloc(200) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 堆起始 │ │
│ │ ┌─────────┬─────────┬───────────────┐ │ │
│ │ │ 已分配 │ 已分配 │ 未分配 │ ← brk上移 │ │
│ │ │ 100B │ 200B │ │ │ │
│ │ └─────────┴─────────┴───────────────┘ │ │
│ │ 堆结束 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ free(第一个100B) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 堆起始 │ │
│ │ ┌─────────┬─────────┬───────────────┐ │ │
│ │ │ 空闲 │ 已分配 │ 未分配 │ │ │
│ │ │ 100B │ 200B │ │ │ │
│ │ └─────────┴─────────┴───────────────┘ │ │
│ │ 堆结束 │ │
│ └─────────────────────────────────────────────────────┘ │
│ 注意:brk不会下降,空闲内存由malloc库管理 │
│ │
└─────────────────────────────────────────────────────────────┘
示例:
// heap_example.cpp
#include <iostream>
int main() {
// 栈分配
int stack_var = 10;
// 堆分配
int* heap_var = new int(20);
std::cout << "Stack variable address: " << &stack_var << std::endl;
std::cout << "Heap variable address: " << heap_var << std::endl;
delete heap_var; // 必须手动释放
return 0;
}
内存泄漏:
// 内存泄漏示例
void memory_leak() {
int* ptr = new int[1000];
// 忘记 delete[] ptr
} // ptr离开作用域,但内存未释放,造成泄漏
// 解决方法:
// 1. 配对使用 new/delete
// 2. 使用智能指针(unique_ptr, shared_ptr)
// 3. 使用RAII模式
3. 内存对齐¶
什么是内存对齐?
数据在内存中的存储地址必须是其大小的整数倍: - 4字节int:地址必须是4的倍数 - 8字节double:地址必须是8的倍数
为什么需要对齐?
┌─────────────────────────────────────────────────────────────┐
│ 内存对齐的原因 │
├─────────────────────────────────────────────────────────────┤
│ │
│ CPU读取内存的方式 │
│ ├── CPU一次读取4字节或8字节(32位/64位) │
│ ├── 内存按字(word)编址 │
│ └── 不对齐的数据可能需要多次读取 │
│ │
│ 示例:读取4字节int │
│ │
│ 对齐的情况(地址0x1000): │
│ ┌─────────┬─────────┬─────────┬─────────┐ │
│ │ Byte0 │ Byte1 │ Byte2 │ Byte3 │ ← 一次读取完成 │
│ └─────────┴─────────┴─────────┴─────────┘ │
│ 地址:0x1000 │
│ │
│ 不对齐的情况(地址0x1001): │
│ ┌─────────┬─────────┬─────────┬─────────┬─────────┐ │
│ │ Byte0 │ Byte1 │ Byte2 │ Byte3 │ Byte4 │ │
│ └─────────┴─────────┴─────────┴─────────┴─────────┘ │
│ ↑_________↑ ↑_________↑ │
│ 第一次读取 第二次读取 │
│ 需要两次内存访问! │
│ │
└─────────────────────────────────────────────────────────────┘
结构体内存对齐示例:
// alignment.cpp
#include <iostream>
struct Aligned { // struct结构体:自定义复合数据类型
char a; // 1字节
// 3字节填充
int b; // 4字节,地址必须是4的倍数
char c; // 1字节
// 3字节填充
}; // 总大小:12字节(不是6字节!)
struct Packed {
char a;
char c;
// 2字节填充
int b;
}; // 总大小:8字节
#pragma pack(push, 1) // 禁用对齐
struct Unaligned {
char a;
int b;
char c;
}; // 总大小:6字节(但访问速度慢)
#pragma pack(pop)
int main() {
std::cout << "sizeof(Aligned): " << sizeof(Aligned) << std::endl; // 12
std::cout << "sizeof(Packed): " << sizeof(Packed) << std::endl; // 8
std::cout << "sizeof(Unaligned): " << sizeof(Unaligned) << std::endl; // 6
return 0;
}
4. 虚拟内存与物理内存¶
为什么需要虚拟内存?¶
┌─────────────────────────────────────────────────────────────┐
│ 虚拟内存的优势 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 隔离性 │
│ ├── 每个进程有独立的地址空间 │
│ ├── 进程A无法直接访问进程B的内存 │
│ └── 防止程序错误影响其他进程 │
│ │
│ 2. 连续性 │
│ ├── 程序看到的内存是连续的 │
│ ├── 实际物理内存可以不连续 │
│ └── 简化内存管理 │
│ │
│ 3. 扩展性 │
│ ├── 可以使用比物理内存更大的地址空间 │
│ ├── 通过换页(swapping)使用磁盘扩展内存 │
│ └── 多个程序可以同时运行 │
│ │
│ 4. 共享性 │
│ ├── 多个进程可以共享同一块物理内存 │
│ ├── 共享库只需要加载一次 │
│ └── 节省物理内存 │
│ │
└─────────────────────────────────────────────────────────────┘
地址转换过程¶
┌─────────────────────────────────────────────────────────────────────┐
│ 虚拟地址到物理地址的转换 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ CPU │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 程序使用虚拟地址 │ │
│ │ int* ptr = 0x7fff_1234_5678; │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ MMU(内存管理单元) │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 1. 拆分虚拟地址 │ │
│ │ 虚拟地址 = 页号 + 页内偏移 │ │
│ │ 0x7fff_1234_5678 = 0x7fff_1234_5 (页号) + 0x678 (偏移) │ │
│ │ │ │
│ │ 2. 查页表 │ │
│ │ 页表[0x7fff_1234_5] = 物理页框号 0x0000_0000_3 │ │
│ │ │ │
│ │ 3. 组合物理地址 │ │
│ │ 物理地址 = 物理页框号 + 页内偏移 │ │
│ │ 0x0000_0000_3_678 = 0x0000_0000_3678 │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ 物理内存 │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 实际访问物理地址 0x0000_0000_3678 │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
🧪 动手实验¶
实验1:观察变量的存储位置¶
目的:理解不同变量的存储区域
步骤:
-
创建测试程序
C++// memory_regions.cpp #include <iostream> #include <cstdlib> int global_init = 42; // 数据段 int global_uninit; // BSS段 static int static_init = 100; // 数据段 static int static_uninit; // BSS段 const int const_global = 200; // 只读数据段 void func() { int local = 10; // 栈 static int local_static = 20; // 数据段 static int local_static_uninit; // BSS段 int* heap_var = new int(30); // 堆 std::cout << "=== 变量地址 ===" << std::endl; std::cout << "global_init: " << &global_init << std::endl; std::cout << "global_uninit: " << &global_uninit << std::endl; std::cout << "static_init: " << &static_init << std::endl; std::cout << "static_uninit: " << &static_uninit << std::endl; std::cout << "const_global: " << &const_global << std::endl; std::cout << "local: " << &local << std::endl; std::cout << "local_static: " << &local_static << std::endl; std::cout << "local_static_uninit: " << &local_static_uninit << std::endl; std::cout << "heap_var: " << heap_var << std::endl; delete heap_var; } int main() { func(); std::cout << "\n查看 /proc/" << getpid() << "/maps" << std::endl; std::cout << "按回车退出..." << std::endl; std::cin.get(); return 0; } -
编译运行
-
在另一个终端查看内存映射
实验2:观察栈的增长¶
目的:理解栈向下增长
步骤:
-
创建测试程序
-
观察输出,注意地址变化
- 每次递归调用,局部变量的地址应该减小(向下增长)
实验3:观察堆的增长¶
目的:理解堆向上增长
步骤:
-
创建测试程序
-
观察输出,注意地址变化
- 每次分配,地址应该增大(向上增长)
实验4:使用valgrind检测内存问题¶
目的:学习使用工具检测内存错误
步骤:
-
安装valgrind(Linux)
-
创建有内存问题的程序
C++// memory_problems.cpp #include <iostream> // 引入头文件 void memory_leak() { int* ptr = new int[100]; // 分配但未释放 } void use_after_free() { int* ptr = new int(42); // 指针:存储变量的内存地址 delete ptr; // *ptr = 10; // 错误:使用已释放的内存 } void buffer_overflow() { int* ptr = new int[10]; ptr[10] = 100; // 错误:越界访问 delete[] ptr; } int main() { memory_leak(); // use_after_free(); // buffer_overflow(); return 0; } -
使用valgrind检测
💡 核心要点总结¶
内存区域对比¶
| 区域 | 存储内容 | 生命周期 | 管理方式 | 增长方向 |
|---|---|---|---|---|
| 代码段 | 机器指令 | 程序运行期 | 自动 | 固定 |
| 数据段 | 已初始化全局/静态变量 | 程序运行期 | 自动 | 固定 |
| BSS段 | 未初始化全局/静态变量 | 程序运行期 | 自动 | 固定 |
| 栈 | 局部变量、函数参数 | 函数执行期 | 自动 | 向下 |
| 堆 | 动态分配内存 | 程序员控制 | 手动 | 向上 |
内存布局全景图¶
高地址
┌─────────────────┐
│ 内核空间 │
├─────────────────┤
│ 栈 │ ← 向下增长
│ ↓ │
│ │
├─────────────────┤
│ 内存映射 │
├─────────────────┤
│ │
│ ↑ │
│ 堆 │ ← 向上增长
├─────────────────┤
│ BSS段 │
├─────────────────┤
│ 数据段 │
├─────────────────┤
│ 代码段 │
└─────────────────┘
低地址
❓ 常见问题¶
Q1:为什么局部变量不初始化会是随机值,而全局变量自动为0?
A: - 局部变量在栈上分配,栈帧复用之前的内存,所以是"垃圾值" - 全局变量在BSS段,操作系统加载时会清零
Q2:栈和堆哪个更快?
A:栈更快。因为: - 栈分配只是移动栈指针(一条指令) - 堆分配需要复杂的内存管理算法 - 栈在CPU缓存中命中率更高
Q3:如何查看程序的内存使用情况?
A:
# Linux
ps aux | grep <程序名>
cat /proc/<PID>/status
cat /proc/<PID>/maps
# 使用top/htop
# 使用valgrind --tool=massif
Q4:什么是内存碎片?
A: - 外部碎片:空闲内存分散,无法满足大块分配请求 - 内部碎片:分配的内存比实际需要的多,造成浪费 - 堆内存管理器通过各种算法(如伙伴系统)减少碎片
📚 扩展阅读¶
- 《深入理解计算机系统》 - 第9章:虚拟内存
- 《程序员的自我修养》 - 第6章:可执行文件的装载与进程
- Linux手册:man 2 brk, man 2 mmap, man 3 malloc
🎯 下一步¶
继续学习程序运行原理的后续内容,深入了解函数调用、系统调用和动态链接等核心机制。