指针与安全编程:缓冲区溢出、指针安全规范、防御性编程、常见指针漏洞

指针,是C语言的灵魂,也是噩梦的根源。我见过太多项目,因为一个指针的越界访问,整个系统在客户现场崩溃。说实话,指针用好了,你是高手;用不好,你就是定时炸弹。今天这堂课,咱们就聊聊怎么把这颗炸弹的引信拆掉。

缓冲区溢出:最经典的指针杀手

缓冲区溢出,说白了就是写数据写过了头。你申请了10个字节的空间,结果往里塞了20个字节。多出来的10个字节,就把别人的地盘给占了。

我在一个嵌入式项目中遇到过这种事。一个串口通信的接收缓冲区,定义的是64字节。结果对方设备发了一包128字节的数据过来,直接就把返回地址给覆盖了。程序跑飞,系统死机。查了整整两天才找到原因。

⚠️ 缓冲区溢出的后果:
  • 覆盖相邻变量,导致数据异常
  • 覆盖函数返回地址,引发程序跑飞
  • 被恶意利用,执行攻击代码
  • 内存碎片化,系统逐渐不稳定
// 典型的缓冲区溢出代码
void dangerous_copy(char *src) {
    char buffer[16];
    strcpy(buffer, src);  // 如果src超过15个字符,就出事了
}

// 安全的写法
void safe_copy(char *src, size_t src_len) {
    char buffer[16];
    if (src_len >= sizeof(buffer)) {
        // 记录错误日志,拒绝拷贝
        return;
    }
    memcpy(buffer, src, src_len);
    buffer[src_len] = '\0';
}

指针安全规范:我的三条铁律

做嵌入式这些年,我给自己定了几条规矩。你想想看,这些规矩救过我多少次。

  1. 初始化必须到位:指针声明时,要么赋有效地址,要么赋NULL。别留野指针。
  2. 使用前必须检查:每次用指针前,先判断是不是NULL。别嫌麻烦。
  3. 释放后必须置空:free完指针,立刻赋NULL。防止悬空指针。

我个人习惯的做法:

// 声明时初始化
int *p = NULL;

// 使用前检查
if (p != NULL) {
    *p = 10;
}

// 释放后置空
free(p);
p = NULL;

防御性编程:把能想到的坑都填上

防御性编程,不是胆小,是成熟。我建议你在写指针代码时,默认所有输入都是恶意的,所有指针都可能为空,所有数组都可能越界。

场景 防御措施 示例
函数参数 检查传入指针是否有效 if (p == NULL) return -1;
数组访问 检查索引是否越界 if (idx < 0 || idx >= size) return;
内存分配 检查malloc返回值 if (p == NULL) exit(1);
字符串操作 使用带长度限制的函数 strncpy(dst, src, n)

💡 小技巧:写代码时,把断言(assert)用起来。调试阶段打开断言,发布阶段关掉。既能发现问题,又不影响性能。

常见指针漏洞:我踩过的坑

这些年,我见过的指针漏洞,说多了都是泪。挑几个典型的,你注意避坑。

1. 释放后使用(Use-After-Free)

我曾经写过一个网络协议栈,释放了一个数据包的内存后,又在另一个线程里访问了它。结果数据全乱了,查了三天才发现是野指针的问题。

// 错误示例
free(packet);
// ... 中间隔了很多代码
process_packet(packet);  // 这里packet已经无效了

// 正确做法
free(packet);
packet = NULL;
// 后面使用前检查
if (packet != NULL) {
    process_packet(packet);
}

2. 双重释放(Double Free)

嗯,这个坑我也掉过。两个不同的模块,都以为自己是内存的管理者,结果都调了free。堆管理器直接崩溃。

// 错误示例
void module_a_free(void *p) {
    free(p);
}

void module_b_free(void *p) {
    free(p);  // 第二次释放,崩溃!
}

// 正确做法:明确所有权,谁分配谁释放

3. 数组越界访问

这个太常见了。循环里少写了一个等号,或者多写了一个等号,数组就出去了。

// 错误示例
int arr[10];
for (int i = 0; i <= 10; i++) {  // 应该是 < 10
    arr[i] = i;  // 最后一次越界了
}

// 安全写法
for (int i = 0; i < sizeof(arr)/sizeof(arr[0]); i++) {
    arr[i] = i;
}

知识体系总览

下面这张图,是我整理的本章节核心逻辑。你把它存下来,写代码时对照着看。

指针安全编程 缓冲区溢出 数组越界 字符串溢出 堆栈破坏 指针安全规范 初始化置NULL 使用前检查 释放后置空 防御性编程 参数校验 边界检查 断言使用 常见漏洞 Use-After-Free Double Free 数组越界

说实话,指针安全没有捷径。就是靠规范、靠检查、靠习惯。我写了十几年C代码,现在每次用指针,还是会下意识地检查一下。这不是胆小,这是对代码负责。

最后送你一句话:指针就像手术刀,用好了救人,用不好伤人。每次写指针代码,就当自己在做一台精密手术。严谨一点,再严谨一点。


公众号:蓝海资料掘金营,微信deep3321