7、数组与指针安全:数组越界访问、指针算术错误、多维数组的安全使用

数组和指针,是C语言的灵魂,也是无数bug的温床。我做了十几年安全审计,见过太多系统崩溃、数据泄露,甚至远程代码执行,根源就是数组越界或者指针乱飞。说白了,这两个东西用好了是利器,用不好就是自残。

今天咱们就好好聊聊,怎么安全地驾驭它们。

7.1 数组越界访问:那个“差一”的错误

数组越界,C语言世界里最常见的“杀手”。为什么?因为C语言不检查边界。你写 arr[10],它不会告诉你“嘿,数组只有10个元素,下标最大是9”。它只会默默地访问那块内存,然后……一切皆有可能。

我个人习惯,每次写循环遍历数组,都会下意识地检查一下边界条件。你想想看,下面这段代码,问题出在哪?

#include <stdio.h>

int main() {
    int arr[10];
    for (int i = 0; i <= 10; i++) {  // 注意这里!
        arr[i] = i * 2;
    }
    return 0;
}

嗯,i <= 10 就是罪魁祸首。当 i 等于10时,访问的是 arr[10],而数组最后一个有效元素是 arr[9]。这就是经典的“差一错误”(Off-by-one error)。

我在项目中遇到过,一个同事写的网络协议解析模块,就是因为这个“差一”,导致解析一个特殊构造的数据包时,程序直接崩溃。后来查了三天才定位到问题。

警告: 数组越界不仅仅是崩溃。它可能被利用来执行任意代码。攻击者可以通过越界读写,篡改相邻内存中的函数指针或返回地址,从而劫持程序控制流。

7.2 指针算术错误:一步踏错,万丈深渊

指针算术,说白了就是通过移动指针来访问不同的内存单元。但这里面的坑,比数组还多。

先看一个基础但容易犯错的地方:

#include <stdio.h>

int main() {
    int arr[] = {10, 20, 30, 40, 50};
    int *p = arr;

    // 正确:访问第三个元素
    printf("%d\n", *(p + 2));  // 输出 30

    // 错误:试图访问越界位置
    printf("%d\n", *(p + 10)); // 未定义行为!

    return 0;
}

为什么会这样?因为 p + 10 这个地址,根本不在数组的管辖范围内。指针算术的偏移量,是以“指针指向的类型大小”为单位的。对于 int*p + 1 实际上移动了 sizeof(int) 个字节(通常是4字节)。

我曾经犯过一个低级错误:

// 错误示例:试图用指针遍历结构体数组
struct Student {
    int id;
    char name[32];
    float score;
};

struct Student class[50];
struct Student *ptr = class;

// 我想访问第5个学生的id
int *id_ptr = (int*)((char*)ptr + 5 * sizeof(struct Student)); // 危险!

这种“手动计算偏移”的做法,极其危险。一旦结构体有内存对齐,或者我算错了偏移量,结果就是访问到错误的数据,甚至越界。

技巧: 永远不要手动计算指针偏移来访问数组元素。直接用下标 ptr[5],编译器会帮你算好。既安全,又清晰。

7.3 多维数组的安全使用:别被“维”绕晕

多维数组,本质上还是一维数组,只是编译器帮我们做了“行优先”的映射。理解这一点,很多问题就迎刃而解了。

比如一个 int matrix[3][4],在内存中是连续存放的:

matrix[0][0], matrix[0][1], ..., matrix[0][3],
matrix[1][0], matrix[1][1], ..., matrix[1][3],
matrix[2][0], matrix[2][1], ..., matrix[2][3]

访问 matrix[i][j] 时,编译器实际计算的是 *( *(matrix + i) + j )。这里最容易出错的,就是指针类型不匹配。

看这个例子:

#include <stdio.h>

void print_matrix_wrong(int **mat, int rows, int cols) {
    // 错误!二维数组名不是 int**
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%d ", mat[i][j]); // 编译警告或崩溃
        }
    }
}

void print_matrix_correct(int rows, int cols, int mat[rows][cols]) {
    // 正确:使用变长数组参数
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%d ", mat[i][j]);
        }
        printf("\n");
    }
}

int main() {
    int matrix[3][4] = {{1,2,3,4}, {5,6,7,8}, {9,10,11,12}};
    // print_matrix_wrong(matrix, 3, 4); // 不要这样调用!
    print_matrix_correct(3, 4, matrix);
    return 0;
}

我个人建议,传递多维数组时,要么用变长数组(VLA)参数,要么用一维数组加手动索引计算。千万别用 int**,那是给指针数组用的,不是给二维数组用的。

下面这张图,帮你理清多维数组在内存中的布局和访问方式:

多维数组内存布局与安全访问 一维数组:int arr[5] arr[0] arr[1] arr[2] arr[3] arr[4] arr[5] 越界! 二维数组:int mat[3][4] [0][0] [0][1] [0][2] [0][3] [1][0] [1][1] [1][2] [1][3] [2][0] [2][1] [2][2] [2][3] mat[0][4] 越界! 指针算术:int *p = arr; p + 0 → &arr[0] p + 1 → &arr[1] (偏移 sizeof(int) 字节) p + n → &arr[n] (n 必须在 [0, 4] 范围内)

7.4 避坑指南与最佳实践

说了这么多问题,咱们总结一下,到底该怎么安全地使用数组和指针。

核心原则: 永远假设用户输入是恶意的,永远检查边界。

场景 安全做法 常见错误
数组遍历 使用 for (i = 0; i < size; i++) 使用 i <= sizei < size-1
指针偏移 用下标 p[i] 代替 *(p + i) 手动计算字节偏移
多维数组传参 使用变长数组参数或一维数组+手动索引 使用 int**
动态分配数组 分配后立即检查 NULL,并记录大小 忘记检查 malloc 返回值
我曾经 接手过一个遗留的嵌入式项目,里面有一个函数,用指针遍历一个缓冲区,但缓冲区大小是通过一个全局变量传递的。结果某个模块更新了缓冲区大小,忘了更新那个全局变量。于是,一个精心构造的网络包,就能让系统缓冲区溢出,直接重启。从那以后,我坚持“大小与指针同行”的原则——要么用结构体把指针和大小绑在一起,要么用 sizeof 操作符。

最后,送你一个“安全三问”:

  1. 这个指针指向的内存,真的属于我吗?(所有权问题)
  2. 我要访问的位置,在合法范围内吗?(边界问题)
  3. 如果用户故意输入一个很大的值,我的代码会崩溃吗?(鲁棒性问题)

每次写数组或指针操作前,问自己一遍。养成习惯,能帮你避开90%的坑。


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