阅读:0

内存越界的可能情况分析,C语言内存越界详解

内存越界是软件系统主要错误之一,其后果往往不可预料且非常严重。更麻烦的是,它出现的时机是随机的,表现出来的症状是随机的,而且造成的后果也是随机的,这会使程序员很难找出这些 Bug 的现象和本质之间的联系,从而给 Bug 的定位带来极大的困难。一般情况下,内存越界访问可分如下两种:
  • 读越界,即读了不属于自己的数据。如果所读的内存地址是无效的,程序立刻崩溃;如果所读内存地址是有效的,在读的时候不会马上出现问题,但由于读到的数据是随机的,因此它会造成不可预料的后果。
  • 写越界,又称为缓冲区溢出,所写入的数据对别的程序来说是随机的,它也会造成不可预料的后果。

避免数组越界

数组越界错误主要包括数组下标取值越界指向数组的指针的指向范围越界

数组下标取值越界主要是指访问数组时,下标的取值不在已定义好的数组的取值范围,而访问的是无法获取的内存地址。例如 int a[10],此数组 a 的下标取值范围是 [0,9]。若取值不在这个范围,就出现越界错误。

指向数组的指针的指向范围越界表示当定义的指针 p 若指向了数组的首地址时(即 p=a),若对其不断进行操作 p++,则最后会导致指针 p 指向大于该数组范围的上界,从而使程序访问了数组以外的存储单元,造成数组越界。

如下面的示例代码所示:
#define MAX_BUF_SIZE 10
int main(void)
{
    int i = 0;
    int a[MAX_BUF_SIZE] = { 0 };
    for (i = 0; i <= MAX_BUF_SIZE; i++)
    {
        a[i] = i;
        printf("%d", a[i]);
    }
    return 0;
}
上面的示例代码就是一个典型的数组下标取值越界。其中,因为 MAX_BUF_SIZE 被定义为 10,所以 int a[MAX_BUF_SIZE] 定义了 10 个元素大小的数组。由于 C 语言中数组的索引是从 0 开始的,所以只能访问 a[0] 到 a[9]。当“i=10”时,访问 a[10] 就造成越界错误。因此,应该修改成如下形式:
#define MAX_BUF_SIZE 10
int main(void)
{
    int i = 0;
    int a[MAX_BUF_SIZE] = { 0 };
    for (i = 0; i < MAX_BUF_SIZE; i++)
    {
        a[i] = i;
        printf("%d", a[i]);
    }
    return 0;
}
除此之外,在设置缓冲区大小时,要考虑各种应用场合,特别是考虑到函数参数的边界条件,按最大的可能分配空间,能够利用程序计算的,尽量自动计算。

避免 sprintf、vsprintf、strcpy、strcat 与 gets 越界

前面已经阐述过,C 语言提供的字符串库函数 sprintf、vsprintf、strcpy、strcat 与 gets 等非常危险,很容易导致内存越界,应该尽量使用安全的字符串库函数 snprintf、strncpy、strncat 与 fgets 来替换它们。

如下面的示例代码所示:
char buf[250];
sprintf(buf, "*** File:%s Line : %d ****", __FILE__, __LINE__);
其中,“__FILE__”在预编译时,被编译时的目录名和源文件名代替,但目录和文件名的长度可变,很可能超出 250 字节,从而导致内存越界。因此,应该使用 snprintf 来替换 sprintf 函数,指定缓冲区的大小,确保内存不会越界。如下面的示例代码所示:
snprintf(buf, MAX_BUF_SIZE - 1, "*** File:%s Line : %d ****", __FILE__, __LINE__);
buf[MAX_BUF_SIZE - 1] = '\0';

避免memcpy与memset函数长度越界

对于 memcpy 与 memset 函数,在使用的时候一定要确保长度不要越界。如下面的示例代码所示:
char a[80];
char b[100];
/*a的长度小于b的长度,发生越界*/
memcpy(a,b,sizeof(b));
很明显,b 的长度是 100,而 a 长度是 80,执行语句“memcpy(a,b,sizeof(b))”时,由于 a 的长度小于 b 的长度,所以将导致程序内存越界。因此,必须确保 a 的长度大于 b 的长度,又或者是 a 和 b 的长度保持一致。由于是字符串拷贝,因此还可以改用 strncpy 函数。如下面的示例代码所示:
char a[MAX_BUF_SIZE];
char b[MAX_BUF_SIZE];
strncpy(a, b, MAX_BUF_SIZE);
a[MAX_BUF_SIZE - 1] = '\0';

避免忽略字符串最后的'\0'字符而导致的越界

在 C 语言中,字符串是一个以'\0'字符结尾的字符数组。但是,当使用 strlen 库函数来获取字符串的长度时,其长度值并不包含'\0'字符。这就导致我们经常因为不小心而忽略了字符串最后的'\0'字符。

如下面的示例代码所示:
int main(void)
{
    char * str = "abcdefghijk";
    char c[20];
    memcpy(c, str, strlen(str));
    printf("%s:%d\n", c, strlen(c));
    return 0;
}
在上面的代码中,“strlen(str)”获取的是 str 真实的字符串长度,不包括最后的'\0'字符。因此,当执行“memcpy(c,str,strlen(str))”语句时,并没有将 str 整个字符串复制到 c 中。再加上这里的字符数组 c 并没有进行初始化,所以最后执行“printf("%s:%d\n",c,strlen(c))”语句时,其运行结果将出乎意料,如图 1 所示。


图 1

但如果在“strlen(str)”获取的str真实的字符串长度后再加上 1(即包括最后的 '\0' 字符),情况就不一样了,如下面的示例代码所示:
int main(void)
{
    char * str = "abcdefghijk";
    char c[20];
    memcpy(c, str, strlen(str)+1);
    printf("%s:%d\n", c, strlen(c));
    return 0;
}
运行结果为:
abcdefghijk:11

除此之外,在使用 strncpy 等安全函数时,当复制字符串到达指定的长度时,不会在目标字符串结尾添加 '\0' 字符,必须手工进行添加 '\0' 字符。当然,可以在申请内存时,将最后一个字节置为 '\0' 字符;也可以在调用 strncpy 函数后,紧接着赋 '\0' 字符。