第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 内存布局全景图
下面这张图,是我自己总结的内存布局结构。你把它记住,以后排查指针越界问题会轻松很多。
这张图里,栈和堆是相向而长的。栈从高地址往低地址走,堆从低地址往高地址走。它们中间是空闲区域。如果栈和堆撞上了,程序就崩了。
2.6 各区域对比
| 区域 | 存储内容 | 生命周期 | 访问权限 | 常见越界问题 |
|---|---|---|---|---|
| 代码段 | 程序指令 | 程序运行期间 | 只读 | 试图写入代码段 |
| 栈区 | 局部变量、参数 | 函数执行期间 | 读写 | 栈溢出、返回局部变量地址 |
| 堆区 | 动态分配的内存 | 从malloc到free | 读写 | 缓冲区溢出、Use-After-Free |
| 全局区 | 全局/静态变量 | 程序运行期间 | 读写 | 全局指针指向已释放内存 |
2.7 实战建议
搞清楚了内存布局,排查指针越界就有方向了。我个人总结了几条经验:
- 栈上的指针越界:检查数组下标是否越界,递归深度是否过大
- 堆上的指针越界:检查malloc的大小是否足够,free之后是否置NULL
- 全局区的指针越界:检查全局指针是否指向了已经释放的堆内存
- 代码段的指针越界:检查函数指针是否被错误赋值
一个小技巧:我习惯在调试时打印指针的地址,然后对照/proc/pid/maps(Linux下)查看它属于哪个区域。这样能快速定位越界发生在哪个区。
嗯,内存布局这块就讲到这里。你把这些区域的特点和生命周期记牢了,后面讲指针越界的具体案例时,就能快速定位问题根源。
核心一句话:指针越界的本质,就是访问了不属于你的内存区域。搞懂内存布局,你就知道哪些区域能碰,哪些不能碰。