C程序中常见的与存储器有关的错误

在C语言程序中,与存储器有关的错误属于那些最令人惊恐的错误之一,因为它们在时间上和空间上经常是在距离错误一段距离之后才表现出来。将错误的数据写到错误的位置,你的程序可能在最终失败之前运行了好几个小时,且程序终止的位置离导致错误的位置已经很远了。

间接引用坏指针

再进程的虚拟地址空间中有较大的区域没有映射到任何有意义的数据,如果我们试图间接引用一个指向这些区域的指针,那么操作系统就会以段异常中止程序。而且虚拟存储器的某些区域是只读的,试图写这些区域会以保护异常中止这个程序。

间接引用换指针一个常见的(初学者常犯的)的示例就是 scanf 错误,写成如 scanf("%d", val);这种情况下,scanf把val的内容解释为一个地址,并试图将一个字写入到这个位置,在最好的情况下,程序以异常中止,如果val正好指向虚拟存储器的某个合法读/写区域,就会覆盖该区域而导致灾难性的后果。(我们知道块内局部变量并不会自动初始化,所以局部变量val可能指向任何值)。

读未初始化的存储器

虽然 bss 存储器位置(未初始化的全局C变量)总是被加载器初始化为零,但是对于堆存储器(局部变量)却并不是这样的。如下面的用法(只是一个示例):

int *y = (int *)malloc(n * sizeof(int));

这样定义的y指向一个int型数组,但是与使用 int[] 定义声明的变量不同,malloc分配的存储器区域并会自动初始化为0,而是该区域原来存储的变量的内容,不能直接对其进行数组相关的操作(取值/写值)。当然可以用 calloc 替代它来初始化。

允许栈缓冲区溢出

如果一个程序不检查输入串的大小就写入栈中的目标缓冲区,那么这个程序就会有缓冲区溢出错误。常用 gets(char *) 就存在这样的隐患,内核中都是用 fgets(char *, int size, FILE *stream) 替代gets。
对于一个健壮的程序一定要注意读取输入时是否会导致栈溢出,前段时间引起关注的ssh 心脏出血事故就是由于缓冲区溢出导致的。

假设指针和它们指向的对象是相同大写的

这个比较好理解,指针是由大小的,指针指向的变量也是有大小的,它们之间没有什么联系。虽然一般指针的存储空间和int类型变量的存储空间一样大,但是在有的处理器中int的指针大于int。所以在使用存储器空间分配的时候要注意指针和指针所指向的变量的大小是不同的。

造成错位错误

错位(off-by-one)是一种很常见的覆盖错误来源,看下面的示例:

int **makeArray(int n, int m) {
int i;
int **A = (int **)malloc(n * sizeof(int *)(;
for(i=0; i<=n; i++) {
A[i] = (int *)malloc(m * sizeof(int));
}
return A;
}

对于有一定经验的人一眼就能看出for循环的最后一次执行会试图初始化数组的第 n+1 个元素而覆盖A数组后面的某个存储器,这会导致不可预知的错误。

引用指针,而不是它指向的对象

如果不太注意C操作符的优先级和结合性,我们就会错误地操作指针,而不是指针所指向的对象。一个常见的操作如下:

// type of variable stirng is char *
char ch = *string++;

// type of size is int *
*size--;

上面的用法在C的字符串处理库函数中经常用到,这里主要要考虑dereference和一元操作符的优先级和结合性,它们的优先级相同,从右向左结合,所以这里的一元操作符的操作对象是指针自身而不是指针指向的对象。也就是ch存储的是 *string的值,然后 string 指针指向下一个元素。

在开发中应该使用括号避免这种难以理解,有可能产生无意识的错误的代码,使用 (*size)-- 这样返回的是 size指针所指向的int变量减一的值。

误·解指针运算

一种常见的错误是忘记了指针的算术操作是以它们指向的对象的大小为单位进行的。比如 int *size 操作 size++ 就指向了数组的下一个元素而不是要 size += sizeof(int); 这样就加到第5个元素(假设int为4字节)去了。

这对于比较熟练的开发人员可能不太会出现,新手则应该多加注意。

引用不存在的变量

没有经验的程序员可能不理解栈的规则,有时会引用不再合法的本地变量。比如下面的代码:

int *ref() {
int val;
val = 1;
// ... ; do something here

return &val; // return int *
}

这里返回的指针是栈里面的一个局部变量,尽管函数返回时它仍是一个合法的存储器地址,但是它已经不再指向一个合法的变量了,当以后的程序调用其它的函数时,存储器将重用它们的栈帧。如果后面的程序分配某个值给这个指针(的解引用),那么它可能实际上正在修改另一个函数的栈帧的一个条目。

引用空闲堆块中的数据

引用一个释放了的堆块中的数据显然会导致错误。这个其实比较简单了,free过的引用在再次分配空间及初始化之前不要再使用。

引起存储器泄漏

存储器泄漏是缓慢隐形的杀手,当程序员不小心忘记释放已经分配的块,而在堆里创建了垃圾时就会发生这种问题。例如malloc之后忘记free就返回了。

对于使用存储不大,且很快就返回(结束运行)的程序这也许不会有太大的问题,因为程序结束运行会回收这些存储,但是对于守护进程和服务器程序来说,它们需要长时间运行(根据定义它们不会终止),任何的内存泄露问题都会积少成多而把系统的资源消耗殆尽,对它们来说内存泄漏是特别严重的问题。