3. 指针越界场景:数组越界、字符串越界、结构体指针越界

指针越界,说白了就是你的指针跑到了不该去的内存区域。我做了十几年C/C++开发,可以负责任地告诉你——这是内存问题里最隐蔽、最坑人的一类。很多时候程序跑起来一切正常,偏偏在客户现场或者凌晨三点崩溃。嗯,今天我们就来把这几个典型场景掰开揉碎了讲清楚。

3.1 数组越界:最常见的“定时炸弹”

数组越界,我个人习惯叫它“沉默的杀手”。为什么?因为编译器不会报错,运行时也不一定立刻崩溃。它就像一颗定时炸弹,不知道什么时候会炸。

核心问题:C/C++不检查数组边界。你访问了arr[10],但数组只有5个元素,系统不会拦你。它只会默默地让你读写别人的内存。

来看一个我项目中真实遇到过的例子:

#include <stdio.h>

void process_data() {
    int arr[5] = {1, 2, 3, 4, 5};
    int secret = 0xDEADBEEF;
    
    // 越界写入
    for (int i = 0; i <= 5; i++) {  // 注意:i <= 5,多了一次
        arr[i] = i * 10;
    }
    
    printf("secret = 0x%X\n", secret);
}

int main() {
    process_data();
    return 0;
}

你猜结果是什么?在我的机器上,secret的值被改成了0x32(也就是50)。因为arr[5]恰好覆盖了secret变量的内存位置。这就是典型的栈上变量被越界破坏。

我曾经踩过的坑:有一次做嵌入式通信协议栈,一个数组越界导致校验和计算总是出错。查了整整两天,最后发现是循环边界多写了1。从那以后,我写循环都会反复确认边界条件。

3.2 字符串越界:C风格字符串的“阿喀琉斯之踵”

字符串越界,本质上还是数组越界。但为什么单独拿出来说?因为字符串操作函数太容易出问题了。你想想看,strcpy、sprintf、strcat这些函数,哪个不是等着你犯错?

为什么会这样?因为C风格字符串以'\0'结尾,但很多程序员会忘记给这个终止符留位置。

#include <stdio.h>
#include <string.h>

void string_demo() {
    char buffer[10];
    
    // 危险操作:源字符串长度超过目标缓冲区
    char *long_str = "Hello, this is a very long string!";
    
    // 这行代码会写爆buffer
    strcpy(buffer, long_str);
    
    printf("buffer: %s\n", buffer);
}

这段代码一跑,buffer后面的内存就被污染了。更可怕的是,如果攻击者精心构造输入数据,这就是缓冲区溢出漏洞的入口。我在做安全审计时见过太多这样的案例。

我的建议:永远使用带长度限制的版本——strncpy、snprintf、strncat。虽然麻烦一点,但能救你于水火之中。

来看一个安全版本:

#include <stdio.h>
#include <string.h>

void safe_string_demo() {
    char buffer[10];
    char *long_str = "Hello, this is a very long string!";
    
    // 安全操作:指定最大复制长度
    strncpy(buffer, long_str, sizeof(buffer) - 1);
    buffer[sizeof(buffer) - 1] = '\0';  // 手动保证终止
    
    printf("buffer: %s\n", buffer);
}

3.3 结构体指针越界:隐蔽的“内存错位”

结构体指针越界,我个人觉得是最难排查的一类。为什么?因为结构体内部有内存对齐,有填充字节。你看着指针偏移量算对了,实际上可能已经跑偏了。

举个例子:

#include <stdio.h>
#include <stddef.h>

#pragma pack(push, 1)  // 取消对齐,方便演示
typedef struct {
    char type;      // 1字节
    int value;      // 4字节
    short flag;     // 2字节
} __attribute__((packed)) MyStruct;
#pragma pack(pop)

void struct_overflow() {
    MyStruct arr[3];
    MyStruct *p = arr;
    
    // 越界访问:访问了arr[3]
    p += 3;
    p->value = 0x12345678;  // 写到了arr之后的内存
    
    printf("arr[2].value = %d\n", arr[2].value);
}

这段代码里,p += 3直接跳到了数组末尾之后。更坑的是,如果结构体有对齐要求,sizeof(MyStruct)可能不是7,而是8甚至12。你算偏移量的时候,一个不小心就算错了。

关键点:结构体指针的加减运算,是以结构体大小为单位的。p + 1不是跳过1个字节,而是跳过sizeof(MyStruct)个字节。这个细节,新手特别容易搞混。

3.4 三种越界的对比与总结

我把这三种场景整理成一个表格,方便你对照:

越界类型 典型表现 排查难度 我的经验
数组越界 循环边界错误、索引计算失误 中等 用assert检查索引范围
字符串越界 strcpy/sprintf等不检查长度 较低 一律用带n的版本
结构体指针越界 指针偏移计算错误、对齐问题 较高 用offsetof宏验证偏移量

3.5 避坑指南:我总结的几条铁律

  • 写循环时,边界条件用“小于”而不是“小于等于”——这是数组越界最常见的来源。
  • 字符串操作,永远预留终止符的位置——sizeof(buffer) - 1是安全上限。
  • 结构体指针偏移,用offsetof宏验证——别相信自己的手工计算。
  • 开启编译器的边界检查选项——比如GCC的-fsanitize=address,能帮你抓到很多越界。

我曾经犯过的错:有一次写网络协议解析,结构体里有个位域,我手动算了偏移量。结果不同编译器下布局不一样,程序在Windows上跑得好好的,到了Linux上就崩。从那以后,我再也不敢手动算偏移了。

3.6 知识体系结构图

下面这张图,帮你理清这三种越界场景的关系:

指针越界 数组越界 字符串越界 结构体指针越界 循环边界错误 索引计算失误 strcpy不检查长度 忘记终止符 指针偏移计算错误 内存对齐问题 共同特征:访问了不属于自己的内存区域 后果:数据损坏、程序崩溃、安全漏洞

嗯,这三种越界场景,说白了都是同一个问题——你的指针跑出了它该待的内存范围。区别只在于表现形式不同。我个人觉得,理解了数组越界,其他两种也就迎刃而解了。字符串越界是数组越界的特例,结构体指针越界是数组越界的变种。

最后分享一个实用技巧:在调试阶段,可以用memset把分配的内存全部填充成0xCC或0xFD。这样一旦越界访问,数据看起来会非常奇怪,能帮你快速定位问题。我一直在用这个方法,屡试不爽。

可可精品资料库