现代 C++ 中的虚拟内存分布

现代 C++ 中的虚拟内存分布

十二月 02, 2025 次阅读

在现代 C++ 开发中,理解虚拟内存的地址分布对于优化程序性能和调试内存相关问题至关重要。本文将深入探讨虚拟内存的基本概念、地址分布以及各个区域的用途和特点。

首先我放一张典型的 Linux 64 位进程虚拟内存布局图,方便大家理解:

virtual_memery

什么是虚拟内存?

虚拟内存是一种内存管理技术,它使得每个进程都拥有一个独立的地址空间。操作系统通过将虚拟地址映射到物理内存地址,实现了内存的抽象化。这种机制允许程序使用比实际物理内存更大的内存空间,并且提高了内存的利用效率,有关进程地址空间的介绍,我在之前的文章中讲过,详细可见:进程地址空间

虚拟内存的地址分布

现代操作系统(如 Linux)在每个进程中都采用 虚拟内存(Virtual Memory) 的方式来组织地址空间。虚拟地址空间逻辑上是连续的,但实际映射到物理内存、磁盘或文件时会经过内核调度与分页管理。下面从地址高低顺序出发,对典型的 Linux 64 位进程地址空间结构逐一说明。

1. 栈(Stack)

栈位于虚拟内存的高地址区域,并且 向下增长
主要用于存放:

  • 函数的局部变量
  • 函数参数
  • 返回地址
  • 保存寄存器值
  • 临时结构体、数组等

栈由内核根据需要动态调整大小(如启用了 Guard Page 防止溢出),但其最大空间受 ulimit -s 限制。由于栈的生命周期严格与函数调用相关,因此其地址位置通常最高且变化最为频繁。
这里的栈又可称之为线程栈,因为每个线程都有独立的栈空间。主线程创建的栈通常较大(如 8MB),而子线程的栈大小可以通过 pthread_attr_setstacksize 设置。同样,主线程创建的子线程的栈空间也位于高地址区域,且向下增长。这里可以回顾到,子线程的栈是独立于主线程的栈的,这也是主线程和子线程之间数据隔离的一部分,除了这个之外,子线程还会有自己的寄存器状态等。

特点:

  • 地址最高
  • 向低地址增长
  • 自动分配/回收,速度极快
  • 容量有限:典型默认 8MB(Linux)
  • 生命周期随函数调用结束

2. 内存空洞(Gap / Unused Area)

在栈与堆之间通常存在一大段空闲区域。这是为了使 堆向上增长栈向下增长 时拥有足够空间,不会立即相遇造成冲突。

系统通常在地址空间中保留足够的未映射区域,用于:

  • 堆扩展
  • 栈扩展
  • mmap 区域分配(及其罕见,通常不会占用这部分空间,一般不需要考虑)

这部分不是一个真正的“段”,但在内存布局图中通常被标注为“空闲区域”。


3. 堆(Heap)

堆位于虚拟内存的中间区域,从低地址向高地址增长
主要由 mallocnew 等动态分配函数使用。

底层通过:

  • brk() / sbrk() 扩大传统堆空间
  • 或通过 mmap() 分配更大的对象(如 glibc 分配 >128KB)

特点:

  • 向高地址增长
  • 生命周期由程序控制(new/delete,malloc/free)
  • 地址变化不频繁,但受分配策略影响会有碎片
  • 大对象通常直接 mmap,绕过常规堆,也就是说,堆空间并不一定全部来自 brk/sbrk 分配的区域,有些大对象操作系统会直接通过 mmap 分配内存,这部分内存地址可能会分布在堆区域之外。通常情况下,glibc 会将小对象(小于 128KB)分配在堆区域,而大对象则通过 mmap 分配。

4. 内存映射区(Memory Mapping Area)

内存映射区域(mmap 区域)。通常位于栈和堆之间,这里需要注意,内存空洞部分我也提到了mmap区域的分配,这是因为现代 64 位操作系统堆共享内存的存放不再仅限于堆和栈之间的空洞区域,mmap 区域可以分布在空洞区域内,也可以分布在堆和栈的其他位置,但是需要注意,通常情况下 mmap 是不会分配空间在内存空洞区的,你可以直接理解成共享内存只会分配在内存映射区。

这里存放:

  • 共享库(.so
  • 加载的文件映射(例如 mmap 打开文件)
  • 匿名内存映射(如你的 mmap(NULL, 4096,...)
  • JIT 或栈保护页等系统映射区域

特点:

  • 动态分布,可根据 mmap 调用自动扩展
  • 与文件系统关联密切,可将文件直接映射到内存
  • 是分配大型对象与共享库的主要区域

5. BSS 段(未初始化数据段)

存放所有 未初始化或初始化为 0 的全局变量与静态变量

例如:

int uninit_global;
static int uninit_static;
char buffer[1024];

这些变量在程序启动时由内核置为 0,不占用可执行文件大小。

特点:

  • 位于内存映射区下方
  • 内容全为 0,实际不占文件大小
  • 程序加载时自动清零

6. 数据段(Data Segment,初始化数据)

存放 已初始化的全局变量、静态变量以及部分常量数据

例如:

int global_var = 100;
static int static_var = 50;
char str[] = "Hello World";
double pi = 3.14159;

这部分在可执行文件中占有实际空间。

特点:

  • 处于 BSS 之下
  • 在程序加载时从 ELF 文件读入
  • 包含可写数据

7. 代码段(Text Segment)

位于最低地址区域,是程序可执行指令的存放位置。

包含:

  • 所有函数代码(如 add()printMessage()
  • 常量字符串文字区域(有时也被划分到只读数据段 .rodata
  • 只读数据(const)

操作系统通常将其设为 只读 + 可执行,以防止修改和注入攻击。

特点:

  • 最低地址
  • 只读(可执行)
  • 包含指令与常量数据

下面我用一份代码来更清楚的呈现虚拟内存的分布情况:

#include <iostream>
#include <sys/mman.h>


/*
    代码段
*/
// 所有函数代码都存储在这里
int add(int a, int b) {
    return a + b;
}

void printMessage() {
    printf("Hello");
}


/*
    初始化数据段
*/
int global_var = 100;           // 全局变量(已初始化)
static int static_var = 50;     // 静态变量(已初始化)
const int const_var = 200;      // 常量(通常放在只读区域)

char str[] = "Hello World";     // 字符串常量
double pi = 3.14159;            // 双精度浮点数

/*
    未初始化数据段
*/
int uninit_global;              // 未初始化的全局变量(BSS)
static int uninit_static;       // 未初始化的静态变量(BSS)

char buffer[1024];              // 未初始化的数组(BSS)
long big_array[10000];          // 大数组(BSS)


/*
    堆
*/
void heapExample() {
    // 动态分配内存(堆)
    int* heap_var = new int(42);            // 单个整数
    char* heap_str = new char[50];          // 字符数组
    double* heap_array = new double[100];   // 双精度浮点数组
    std::cout << "Heap variable address: " << (void*)heap_var << std::endl;
    std::cout << "Heap string address: " << (void*)heap_str << std::endl;
    std::cout << "Heap array address: " << (void*)heap_array << std::endl;

    // 使用完毕后释放内存
    delete heap_var;
    delete[] heap_str;
    delete[] heap_array;
}


/*
    内存映射段
*/
void* create_mmap() {
    // 创建内存映射区域
    void *addr = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
                     MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    return addr;
}

/*
    栈
*/
void function() {
    int local_var = 10;         // 局部变量(栈)
    char local_str[100];        // 局部数组(栈)
    double temp = 3.14;         // 局部浮点数(栈)
    std::cout << "Local variable address: " << (void*)&local_var << std::endl;
    std::cout << "Local array address: " << (void*)local_str << std::endl;
    std::cout << "Local double address: " << (void*)&temp << std::endl;
    // 函数调用时参数压栈
    add(1, 2);
    
    // 结构体变量(栈)
    struct Point {
        int x;
        int y;
    } p = {1, 2};
}

int main() {
    // 代码段
    std::cout << "Code Segment:" << std::endl;
    std::cout << "The addr of function printMessage: " << (void*)printMessage << std::endl;
    std::cout << "The addr of function add: " << (void*)add << std::endl;
    std::cout << "-------------------------" << std::endl;
    // 初始化数据段
    std::cout << "Initialized Data Segment:" << std::endl;
    std::cout << "The addr of global_var: " << (void*)&global_var << std::endl;
    std::cout << "The addr of static_var: " << (void*)&static_var << std::endl;
    std::cout << "The addr of const_var: " << (void*)&const_var << std::endl;
    std::cout << "The addr of str: " << (void*)str << std::endl;
    std::cout << "The addr of pi: " << (void*)&pi << std::endl;
    std::cout << "-------------------------" << std::endl;
    // 未初始化数据段
    std::cout << "Uninitialized Data Segment (BSS):" << std::endl;
    std::cout << "The addr of uninit_global: " << (void*)&uninit_global << std::endl;
    std::cout << "The addr of uninit_static: " << (void*)&uninit_static << std::endl;
    std::cout << "The addr of buffer: " << (void*)buffer << std::endl;
    std::cout << "The addr of big_array: " << (void*)big_array << std::endl;
    std::cout << "-------------------------" << std::endl;
    // 堆
    std::cout << "Heap Segment:" << std::endl;
    heapExample();
    std::cout << "-------------------------" << std::endl;
    // 内存映射段
    std::cout << "Memory Mapped Segment:" << std::endl;
    void* mmap_addr = create_mmap();
    std::cout << "The addr of mmaped region: " << mmap_addr << std::endl;
    // 释放内存映射区域
    munmap(mmap_addr, 4096);
    // 栈
    std::cout << "-------------------------" << std::endl;
    std::cout << "Stack Segment:" << std::endl;
    function();
    return 0;
}

通过运行上述代码,你可以观察到各个变量和函数在虚拟内存中的地址分布情况,从而更好地理解现代 C++ 程序的内存布局。运行结果如下:

Code Segment:
The addr of function printMessage: 0x5603e4da31fd
The addr of function add: 0x5603e4da31e9
-------------------------
Initialized Data Segment:
The addr of global_var: 0x5603e4da6070
The addr of static_var: 0x5603e4da6074
The addr of const_var: 0x5603e4da4010
The addr of str: 0x5603e4da6078
The addr of pi: 0x5603e4da6088
-------------------------
Uninitialized Data Segment (BSS):
The addr of uninit_global: 0x5603e4da61e0
The addr of uninit_static: 0x5603e4db9e84
The addr of buffer: 0x5603e4da6200
The addr of big_array: 0x5603e4da6600
-------------------------
Heap Segment:
Heap variable address: 0x5604196082c0
Heap string address: 0x5604196082e0
Heap array address: 0x560419608320
-------------------------
Memory Mapped Segment:
The addr of mmaped region: 0x7f5540982000
-------------------------
Stack Segment:
Local variable address: 0x7ffc0b10e94c
Local array address: 0x7ffc0b10e8e0
Local double address: 0x7ffc0b10e8d8

栈与共享库之间的特殊内核映射:VDSO、VVAR 与 Stack Guard

在典型的 64 位 Linux 进程虚拟地址空间中,用户态代码能看到的“最上方”(接近高地址)是栈(stack)。而在 栈与共享库(mmap 区)之间,系统还会自动插入若干 特殊的只读或不可执行的内核辅助映射,主要包括:

  • VDSO(Virtual Dynamic Shared Object)
  • VVAR
  • Stack Guard(栈保护页)

这些区域不是程序编译产物,而是 由内核在进程创建时自动加入,作用是提高安全性和效率。

下文对这三者逐一展开说明。


1. VDSO(Virtual Dynamic Shared Object)

VDSO 是 Linux 为用户态提供特定内核功能而引入的一段共享库。
本质上,VDSO 是内核伪造的一段只读空间,被映射进 mmap 区域附近,带有 ELF 格式但绝不是磁盘文件。

作用

VDSO 的主要目的是让用户态可以 快速调用某些系统服务,而不用真正进入内核态 (即不执行 syscall 或中断指令)。

常通过 VDSO 优化的系统调用包括:

  • gettimeofday()
  • clock_gettime()
  • time()

这些调用非常频繁,传统 syscall 会带来用户态 ↔ 内核态切换,而 VDSO 允许直接在用户态执行内核提前准备好的数据和逻辑,提高性能。

特点

  • 内核动态生成,不在磁盘上存在。
  • 带有 ELF 结构,因此在调试器里看起来像一个“.so”。
  • 位于:
高地址
  [ stack ]
  [ VDSO ]
  [ VVAR ]
  [ stack guard ]
  ↓
  [ mmap 区域 ]

2. VVAR(VDSO Variables Area)

VVAR 是 VDSO 的数据区,用来存放内核向用户态共享的只读变量,例如:

  • 内核 monotonic clock 基准值
  • 实时时钟偏移量
  • 与时钟相关的校准数据

VDSO 的函数通过访问 VVAR 中的数据来返回时间结果(如 clock_gettime())。

特点

  • 只读映射,用户态无法修改。
  • 必须与 VDSO 配套出现。
  • 大小较小,一般只有几页。

可以理解为:

VDSO 是代码段,VVAR 是它的数据段。


3. Stack Guard(栈保护页 / Guard Page)

为了防止栈溢出直接覆盖下方的映射区域,内核会在栈底部(朝向低地址方向)插入一块 不可访问的保护页,通常称为 Guard Page。

作用

  • 一旦程序的栈指针向下越过正常栈空间进入这个区域,访问会立即触发 段错误(Segmentation Fault)
  • 防止无意或恶意的堆栈碰撞攻击。

特点

  • 通常大小为 1 页(4KB)。
  • 具有 PROT_NONE 权限,其访问会触发错误。
  • 位置如下:
[ stack 顶部(高地址) ]
        ↓增长方向
[ stack 内容 ]
[ guard page (PROT_NONE) ]
-------------------------------------
[ mmap 区域 / shared libraries ]

mmap 区域作为一个 灵活可扩展的映射池,共享库、文件映射等均从此处分配,而栈是从顶部向下扩展。
为了避免冲突并加入安全 & 性能优化层,Linux 在两者之间插入 VDSO/VVAR/guard page 等内核辅助页。

它们不属于传统意义上的堆、栈、数据段,而是内核附加的系统级支持。