第2章:内存布局基础:栈区、堆区、全局区、代码段详解

指针越界,说白了就是访问了不该访问的内存地址。要搞懂这个问题,你得先知道程序在内存里是怎么“住”的。我刚开始学C语言时,觉得内存就是个黑盒子,直到有一次调试一个诡异的崩溃,才逼着我把它彻底摸清楚。

一个C/C++程序跑起来后,内存会分成几个区域。每个区域有各自的用途、生命周期和访问规则。你想想看,如果连这些区域都分不清,指针越界几乎是必然的。

2.1 代码段(Text Segment)

代码段存的是你的程序指令。也就是那些编译后的机器码。这块区域是只读的,谁也别想改它。

为什么是只读的?因为指令本身不允许被修改。如果有人试图通过指针去写代码段,操作系统会直接给你一个段错误(Segmentation Fault)。

关键点:代码段是只读的,任何写入操作都会导致程序崩溃。

我在项目中遇到过一种情况:有人不小心把函数指针赋值错了,然后试图通过它修改代码段的内容。结果程序秒崩,查了半天才发现是往只读区域写了数据。

2.2 栈区(Stack)

栈区是函数调用时用的。局部变量、函数参数、返回地址都放在这里。栈的特点是“先进后出”,就像叠盘子一样,后放上去的先拿走。

栈的大小是固定的。Windows下默认1MB,Linux下默认8MB。别觉得大,递归调用深一点就爆了。

避坑指南:我曾经在项目中写了一个递归函数,递归深度大概5000层。结果每次跑到一半就崩溃。查了半天,原来是栈溢出了。后来改成迭代实现,问题解决。

栈上的变量生命周期很明确:函数执行完,变量就自动销毁。所以,千万别返回局部变量的地址。你想想看,函数都结束了,那块内存已经被回收了,你拿着它的地址去访问,不就是越界吗?

// 错误示例:返回局部变量的地址
int* bad_function() {
    int local_var = 42;
    return &local_var;  // 危险!函数结束后local_var就不存在了
}

// 正确做法:用堆分配或传参
int* good_function() {
    int* p = (int*)malloc(sizeof(int));
    *p = 42;
    return p;  // 堆上的内存,手动管理
}

2.3 堆区(Heap)

堆区是动态分配的内存。你用malloc、calloc、realloc、new申请的内存都在这里。堆的大小理论上只受限于物理内存和操作系统限制。

堆的特点是灵活,但管理起来麻烦。申请了就要释放,否则内存泄漏。释放了就不能再用,否则野指针。

个人经验:我习惯在malloc之后立即检查返回值是否为NULL。很多新手觉得这步多余,但嵌入式环境下内存分配失败是常有的事。不检查,直接访问NULL指针,就是越界。

堆上的指针越界,常见于两种情况:

  • 写入的数据超过了申请的大小(缓冲区溢出)
  • 释放后继续使用(Use-After-Free)
// 堆缓冲区溢出示例
char* buffer = (char*)malloc(10);  // 只申请了10个字节
for (int i = 0; i < 20; i++) {
    buffer[i] = 'A';  // 越界!写到第10个字节之后了
}
free(buffer);

2.4 全局区(静态区)

全局变量和静态变量放在这里。它们在整个程序运行期间都存在。全局区又分两块:已初始化的全局变量和未初始化的全局变量(BSS段)。

全局区的变量生命周期最长,从程序启动到结束。但这也意味着,如果你不小心,全局指针可能指向已经释放的堆内存。

注意:全局区的变量默认初始化为0。而栈上的局部变量是随机值。这个区别,我见过不少人踩坑。

int global_var = 100;       // 已初始化全局区
static int static_var;      // BSS段,默认0

void func() {
    static int count = 0;   // 静态局部变量,也在全局区
    count++;
}

2.5 内存布局全景图

下面这张图,是我自己总结的内存布局结构。你把它记住,以后排查指针越界问题会轻松很多。

程序内存布局 高地址 栈区(Stack) 局部变量、函数参数、返回地址 向下增长(高地址→低地址) 堆区(Heap) 动态分配(malloc/new) 向上增长(低地址→高地址) 全局区(静态区) 代码段(Text) 低地址

这张图里,栈和堆是相向而长的。栈从高地址往低地址走,堆从低地址往高地址走。它们中间是空闲区域。如果栈和堆撞上了,程序就崩了。

2.6 各区域对比

区域 存储内容 生命周期 访问权限 常见越界问题
代码段 程序指令 程序运行期间 只读 试图写入代码段
栈区 局部变量、参数 函数执行期间 读写 栈溢出、返回局部变量地址
堆区 动态分配的内存 从malloc到free 读写 缓冲区溢出、Use-After-Free
全局区 全局/静态变量 程序运行期间 读写 全局指针指向已释放内存

2.7 实战建议

搞清楚了内存布局,排查指针越界就有方向了。我个人总结了几条经验:

  • 栈上的指针越界:检查数组下标是否越界,递归深度是否过大
  • 堆上的指针越界:检查malloc的大小是否足够,free之后是否置NULL
  • 全局区的指针越界:检查全局指针是否指向了已经释放的堆内存
  • 代码段的指针越界:检查函数指针是否被错误赋值

一个小技巧:我习惯在调试时打印指针的地址,然后对照/proc/pid/maps(Linux下)查看它属于哪个区域。这样能快速定位越界发生在哪个区。

嗯,内存布局这块就讲到这里。你把这些区域的特点和生命周期记牢了,后面讲指针越界的具体案例时,就能快速定位问题根源。

核心一句话:指针越界的本质,就是访问了不属于你的内存区域。搞懂内存布局,你就知道哪些区域能碰,哪些不能碰。