跳转至

02-内存布局详解

重要性:⭐⭐⭐⭐⭐ 实用度:⭐⭐⭐⭐⭐ 学习时间:3天 必须掌握:是


为什么学这一章?

内存是程序运行的核心资源。理解内存布局能帮助你: - 理解变量存储在哪里(栈、堆、数据段) - 避免内存相关的bug(段错误、内存泄漏) - 优化程序的内存使用 - 理解虚拟内存的工作原理

学完这一章,你将能够: - ✅ 画出进程的完整内存布局图 - ✅ 解释不同存储区域的特点和用途 - ✅ 理解虚拟地址到物理地址的映射 - ✅ 使用工具观察程序的内存使用情况


📖 核心概念

1. 进程的虚拟地址空间

每个进程都有自己独立的虚拟地址空间,从0开始到一个最大值(64位系统上是2^64):

Text Only
┌─────────────────────────────────────────────────────────────────┐
│                    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实例) - 固定大小:程序运行时不会改变

示例

C++
// 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)

存储内容: - 已初始化的全局变量 - 已初始化的静态变量

特点: - 可读可写 - 程序启动时从可执行文件加载初始值 - 生命周期:整个程序运行期间

示例

C++
// 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 - 可读可写

示例

C++
// 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)

栈帧结构

Text Only
┌─────────────────────────────────────┐ ← 高地址
│           调用者的栈帧               │
├─────────────────────────────────────┤
│  参数 n                             │
│  参数 n-1                           │
│  ...                                │
│  参数 1                             │
├─────────────────────────────────────┤
│  返回地址  ← 调用时自动压入          │
├─────────────────────────────────────┤ ← 当前栈帧开始(RBP)
│  保存的旧的RBP  ← 建立栈帧时压入      │
├─────────────────────────────────────┤
│  局部变量 1                         │
│  局部变量 2                         │
│  ...                                │
│  局部变量 n                         │
├─────────────────────────────────────┤
│  (临时空间/寄存器保存)              │
├─────────────────────────────────────┤ ← 栈顶(RSP)
│           未使用空间                 │
└─────────────────────────────────────┘ ← 低地址

示例

C++
// 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)

C++
// 导致栈溢出的代码
void infinite_recursion() {
    char buffer[1024];      // 每次调用占用1KB栈空间
    infinite_recursion();   // 无限递归
}

// 解决方法:
// 1. 避免无限递归
// 2. 减少大数组的使用(改用堆分配)
// 3. 增加栈大小(ulimit -s)


2.5 堆(Heap)

存储内容: - 动态分配的内存(malloc/new) - 动态数据结构(链表、树等)

特点: - 手动管理:程序员负责分配和释放 - 向上增长:从低地址向高地址增长 - 大小灵活:受限于系统的虚拟内存 - 速度较慢:需要系统调用和内存管理

内存分配过程

Text Only
┌─────────────────────────────────────────────────────────────┐
│                    堆内存分配过程                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  初始状态                                                    │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  堆起始                                              │   │
│  │  ┌─────────┐                                        │   │
│  │  │  未分配  │ ← 程序中断(brk)                       │   │
│  │  └─────────┘                                        │   │
│  │  堆结束                                              │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  malloc(100)                                                │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  堆起始                                              │   │
│  │  ┌─────────┬────────────────────────┐              │   │
│  │  │ 已分配  │        未分配           │ ← brk上移    │   │
│  │  │  100B   │                        │              │   │
│  │  └─────────┴────────────────────────┘              │   │
│  │  堆结束                                              │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  malloc(200)                                                │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  堆起始                                              │   │
│  │  ┌─────────┬─────────┬───────────────┐             │   │
│  │  │ 已分配  │ 已分配  │     未分配     │ ← brk上移   │   │
│  │  │  100B   │  200B   │               │             │   │
│  │  └─────────┴─────────┴───────────────┘             │   │
│  │  堆结束                                              │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  free(第一个100B)                                           │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  堆起始                                              │   │
│  │  ┌─────────┬─────────┬───────────────┐             │   │
│  │  │  空闲   │ 已分配  │     未分配     │             │   │
│  │  │  100B   │  200B   │               │             │   │
│  │  └─────────┴─────────┴───────────────┘             │   │
│  │  堆结束                                              │   │
│  └─────────────────────────────────────────────────────┘   │
│  注意:brk不会下降,空闲内存由malloc库管理                    │
│                                                             │
└─────────────────────────────────────────────────────────────┘

示例

C++
// 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;
}

内存泄漏

C++
// 内存泄漏示例
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的倍数

为什么需要对齐?

Text Only
┌─────────────────────────────────────────────────────────────┐
│                    内存对齐的原因                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  CPU读取内存的方式                                            │
│  ├── CPU一次读取4字节或8字节(32位/64位)                      │
│  ├── 内存按字(word)编址                                     │
│  └── 不对齐的数据可能需要多次读取                             │
│                                                             │
│  示例:读取4字节int                                           │
│                                                             │
│  对齐的情况(地址0x1000):                                     │
│  ┌─────────┬─────────┬─────────┬─────────┐                 │
│  │  Byte0  │  Byte1  │  Byte2  │  Byte3  │  ← 一次读取完成  │
│  └─────────┴─────────┴─────────┴─────────┘                 │
│  地址:0x1000                                              │
│                                                             │
│  不对齐的情况(地址0x1001):                                   │
│  ┌─────────┬─────────┬─────────┬─────────┬─────────┐       │
│  │  Byte0  │  Byte1  │  Byte2  │  Byte3  │  Byte4  │       │
│  └─────────┴─────────┴─────────┴─────────┴─────────┘       │
│       ↑_________↑           ↑_________↑                    │
│      第一次读取              第二次读取                       │
│  需要两次内存访问!                                          │
│                                                             │
└─────────────────────────────────────────────────────────────┘

结构体内存对齐示例

C++
// 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. 虚拟内存与物理内存

为什么需要虚拟内存?

Text Only
┌─────────────────────────────────────────────────────────────┐
│                    虚拟内存的优势                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. 隔离性                                                   │
│     ├── 每个进程有独立的地址空间                              │
│     ├── 进程A无法直接访问进程B的内存                          │
│     └── 防止程序错误影响其他进程                              │
│                                                             │
│  2. 连续性                                                   │
│     ├── 程序看到的内存是连续的                                │
│     ├── 实际物理内存可以不连续                                │
│     └── 简化内存管理                                          │
│                                                             │
│  3. 扩展性                                                   │
│     ├── 可以使用比物理内存更大的地址空间                       │
│     ├── 通过换页(swapping)使用磁盘扩展内存                   │
│     └── 多个程序可以同时运行                                  │
│                                                             │
│  4. 共享性                                                   │
│     ├── 多个进程可以共享同一块物理内存                        │
│     ├── 共享库只需要加载一次                                  │
│     └── 节省物理内存                                          │
│                                                             │
└─────────────────────────────────────────────────────────────┘

地址转换过程

Text Only
┌─────────────────────────────────────────────────────────────────────┐
│                    虚拟地址到物理地址的转换                           │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  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:观察变量的存储位置

目的:理解不同变量的存储区域

步骤

  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. 编译运行

    Bash
    g++ -o memory_regions memory_regions.cpp
    ./memory_regions
    

  3. 在另一个终端查看内存映射

    Bash
    cat /proc/<PID>/maps
    

实验2:观察栈的增长

目的:理解栈向下增长

步骤

  1. 创建测试程序

    C++
    // stack_growth.cpp
    #include <iostream>
    
    void recursive(int depth) {
        int local;
        std::cout << "Depth " << depth << ", local address: " << &local << std::endl;
    
        if (depth < 5) {
            recursive(depth + 1);
        }
    }
    
    int main() {
        recursive(0);
        return 0;
    }
    

  2. 观察输出,注意地址变化

  3. 每次递归调用,局部变量的地址应该减小(向下增长)

实验3:观察堆的增长

目的:理解堆向上增长

步骤

  1. 创建测试程序

    C++
    // heap_growth.cpp
    #include <iostream>
    #include <cstdlib>
    
    int main() {
        std::cout << "=== 堆内存分配 ===" << std::endl;
    
        for (int i = 0; i < 5; i++) {
            int* ptr = new int[i];
            std::cout << "Allocation " << i << ": " << ptr << std::endl;
        }
    
        return 0;
    }
    

  2. 观察输出,注意地址变化

  3. 每次分配,地址应该增大(向上增长)

实验4:使用valgrind检测内存问题

目的:学习使用工具检测内存错误

步骤

  1. 安装valgrind(Linux)

    Bash
    sudo apt-get install valgrind
    

  2. 创建有内存问题的程序

    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;
    }
    

  3. 使用valgrind检测

    Bash
    g++ -g -o memory_problems memory_problems.cpp
    valgrind --leak-check=full ./memory_problems
    


💡 核心要点总结

内存区域对比

区域 存储内容 生命周期 管理方式 增长方向
代码段 机器指令 程序运行期 自动 固定
数据段 已初始化全局/静态变量 程序运行期 自动 固定
BSS段 未初始化全局/静态变量 程序运行期 自动 固定
局部变量、函数参数 函数执行期 自动 向下
动态分配内存 程序员控制 手动 向上

内存布局全景图

Text Only
高地址
┌─────────────────┐
│    内核空间      │
├─────────────────┤
│    栈           │ ← 向下增长
│       ↓         │
│                 │
├─────────────────┤
│    内存映射      │
├─────────────────┤
│                 │
│       ↑         │
│    堆           │ ← 向上增长
├─────────────────┤
│    BSS段        │
├─────────────────┤
│    数据段       │
├─────────────────┤
│    代码段       │
└─────────────────┘
低地址

❓ 常见问题

Q1:为什么局部变量不初始化会是随机值,而全局变量自动为0?

A: - 局部变量在栈上分配,栈帧复用之前的内存,所以是"垃圾值" - 全局变量在BSS段,操作系统加载时会清零

Q2:栈和堆哪个更快?

A:栈更快。因为: - 栈分配只是移动栈指针(一条指令) - 堆分配需要复杂的内存管理算法 - 栈在CPU缓存中命中率更高

Q3:如何查看程序的内存使用情况?

A:

Bash
# Linux
ps aux | grep <程序名>
cat /proc/<PID>/status
cat /proc/<PID>/maps

# 使用top/htop
# 使用valgrind --tool=massif

Q4:什么是内存碎片?

A: - 外部碎片:空闲内存分散,无法满足大块分配请求 - 内部碎片:分配的内存比实际需要的多,造成浪费 - 堆内存管理器通过各种算法(如伙伴系统)减少碎片


📚 扩展阅读

  1. 《深入理解计算机系统》 - 第9章:虚拟内存
  2. 《程序员的自我修养》 - 第6章:可执行文件的装载与进程
  3. Linux手册:man 2 brk, man 2 mmap, man 3 malloc

🎯 下一步

继续学习程序运行原理的后续内容,深入了解函数调用、系统调用和动态链接等核心机制。