指针越界排查与修复实战 - 第3节

指针越界排查与修复实战 · 第一章

数组越界:从原理到实战

大家好,我是老李。今天咱们聊聊数组越界——这个C/C++程序员几乎都踩过的坑。

说实话,我入行头三年,至少有一半的崩溃bug跟数组越界有关。你想想看,一个不小心多写了一个元素,程序可能当场崩,也可能悄悄跑几个月才出问题。嗯,后者更可怕。

数组越界的本质原理

数组越界,说白了就是访问了不属于你的内存。

C/C++的数组不会做边界检查。这是语言设计时的选择——为了性能。但代价就是,你得自己管好下标。

来看一个最基础的例子:

int arr[5] = {1, 2, 3, 4, 5};
arr[5] = 100;  // 越界!下标0-4才是合法的

为什么arr[5]能编译通过?因为编译器只负责分配内存,不负责运行时检查。arr[5]实际上访问的是紧挨着数组末尾的那4个字节。至于那4个字节里存的是什么——可能是另一个变量,可能是函数返回地址,也可能是堆管理信息。

核心要点:数组名本质上是一个指针常量,指向连续内存块的首地址。下标操作 arr[i] 等价于 *(arr + i)。编译器不会验证 i 是否在合法范围内。

栈上数组越界的典型场景

栈上的数组越界,我遇到过太多次了。最典型的就是循环边界写错。

场景一:循环边界错误

void process_data() {
    int scores[10] = {0};
    // 错误:循环到10,但有效下标是0-9
    for (int i = 0; i <= 10; i++) {
        scores[i] = i * 10;  // i=10时越界
    }
}

我曾经在项目里排查过一个诡异bug:程序运行一段时间后,某个局部变量的值莫名其妙被改了。查了两天才发现,是前面一个循环多跑了一次,把栈上相邻的变量给覆盖了。

避坑指南:我建议你养成习惯——循环条件一律用 < 而不是 <=。数组长度为N,下标范围就是[0, N-1]。这个习惯能帮你避免90%的越界问题。

场景二:字符串操作越界

void copy_name() {
    char name[8];
    strcpy(name, "Alexander");  // 源字符串长度10,目标缓冲区只有8
}

strcpy不检查目标缓冲区大小。它只管把源字符串逐字节复制,直到遇到'\0'。如果源字符串比目标缓冲区长,就会一路写下去,把栈上后面的内容全覆盖掉。

我记得有一次,一个同事的代码里用了strcpy,结果函数返回时直接跳到了错误地址——因为返回地址被覆盖了。嗯,这就是经典的栈溢出攻击原理。

个人建议:能用strncpy就别用strcpy。虽然strncpy也有坑(不自动加'\0'),但至少给了你一道防线。C11标准里还有strcpy_s,更安全。

堆上数组越界的典型场景

堆上的越界比栈上更隐蔽。因为堆内存是动态分配的,越界后不一定马上崩溃。

场景一:分配大小计算错误

int* arr = (int*)malloc(5 * sizeof(int));  // 分配了5个int的空间
for (int i = 0; i < 10; i++) {
    arr[i] = i;  // i从5开始就越界了
}

这种错误很常见。你分配了5个元素的空间,但实际用了10个。堆管理器不会立刻报错,因为堆内存通常是按页管理的。你可能写到了相邻的空闲块里,也可能写到了别的已分配块里。

我遇到过最头疼的一个bug:程序在客户现场跑了三个月才崩溃。最后定位到是堆越界,把另一个对象的虚函数表指针给覆盖了。调用虚函数时跳到了非法地址,直接段错误。

场景二:忘记预留终止符

char* buf = (char*)malloc(strlen(input));  // 错误!没有为'\0'预留空间
strcpy(buf, input);  // 如果input长度等于分配大小,就会越界写一个字节

这个坑我踩过不止一次。strlen返回的是字符串长度,不包括结尾的'\0'。所以分配空间时一定要 +1。

经验之谈:堆越界最难排查的地方在于——错误发生的位置和崩溃的位置往往不在同一个地方。你写越界的时候程序还在跑,等到崩溃时,调用栈已经跟越界点毫无关系了。

知识体系总览

下面这张图是我整理的数组越界知识体系,帮你快速建立全局认知:

数组越界知识体系 数组越界 栈上数组越界 堆上数组越界 循环边界错误 for(i=0; i<=N; i++) 字符串操作越界 strcpy/gets 无边界检查 分配大小计算错误 malloc(5) 却用了10个 忘记预留终止符 strlen 未 +1 后果:内存污染 → 数据错乱 → 程序崩溃 → 安全漏洞 排查方法:AddressSanitizer / Valgrind / 代码审查

如何系统排查数组越界

排查数组越界,我有一套自己的流程:

  1. 复现问题——先搞清楚什么条件下会触发崩溃
  2. 启用内存检测工具——AddressSanitizer 是我的首选,编译时加 -fsanitize=address 就行
  3. 检查循环边界——所有 for/while 循环,确认下标范围
  4. 检查字符串操作——strcpy、sprintf、memcpy 这些,确认目标缓冲区大小
  5. 检查动态内存分配——malloc/calloc 的大小是否足够,realloc 后指针是否更新

调试小技巧:如果你怀疑堆越界,可以在分配时多分配一些空间,然后在边界处填充特殊标记(比如0xDEADBEEF)。程序崩溃后检查这些标记是否被覆盖,就能快速定位越界位置。

好了,这一章我们聊了数组越界的原理和典型场景。栈上的越界往往跟循环和字符串操作有关,堆上的越界则多出在分配大小计算上。下一章我会讲指针运算越界——那又是另一番风景了。

可可精品资料库