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 知识体系结构图
下面这张图,帮你理清这三种越界场景的关系:
嗯,这三种越界场景,说白了都是同一个问题——你的指针跑出了它该待的内存范围。区别只在于表现形式不同。我个人觉得,理解了数组越界,其他两种也就迎刃而解了。字符串越界是数组越界的特例,结构体指针越界是数组越界的变种。
最后分享一个实用技巧:在调试阶段,可以用memset把分配的内存全部填充成0xCC或0xFD。这样一旦越界访问,数据看起来会非常奇怪,能帮你快速定位问题。我一直在用这个方法,屡试不爽。
可可精品资料库