C 语言程序中与存储器相关的常见错误(即指针相关)

来源:互联网 发布:python 3.x运算符 编辑:程序博客网 时间:2024/04/29 17:45
对于书写 C 语言程序的程序员而言, 与存储器有关的错误是非常让人郁闷无语的, 因为不管是在空间还是时间上, 错误和异常通常是在距离错误根源有一段时间或空间距离上才表现出来. 给查找真正的错误源带来很大困难. 将错误的数据写到错误的位置, 程序可能在最终失败前运行了好几个小时甚至几天几月(嵌入式系统),  但是程序的错误已经离真正的源头很远了, 呈现的错误也好像是"刻舟求剑"的结果, 只有刀痕没有剑.

下面就来摘录一些常见的错误:


1. 间接引用坏指针(Dereferencing Bad Pointers)


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

间接引用坏指针的一个常见实例就是经典的 scanf 错误. 假设我们想要使用 scanf 从 stdin 读一个整数到一个变量. 正确的方法是传递给 scanf 一个格式串和变量的地址:

[cpp] view plaincopy
  1. scanf("%d", &val); // 正确的方法  

然而, 对于初学 C 语言的人而言, 很容易传递 val 的内容, 而不是他的地址:

[cpp] view plaincopy
  1. scanf("%d", val);  // 错误的方式  
  2. // GCC 一般会给出警告:格式 ‘%d’ expects argument of type ‘int *’, but argument 2 has type ‘int’ [-Wformat]|  

在这种情况下, scanf 将把变量的内容解释为一个地址, 并试图将一个字写到这个位置. 在最好的情况下, 程序立即以异常中止. 在最糟糕的情况下, val 的内容将对应于虚拟存储器的某个合法的读/写区域, 于是我们就覆盖了存储器, 这通常会在相当长的时间后才会造成灾难性且令人困惑的后果.


2. 读未初始化的存储器(Reading Uninitialized Memory)


虽然 bss 存储器位置(未初始化的全局变量)总是被加载器初始化为零(链接器一般会分配有 BSS 段), 但是对于堆存储器(heap memory)却不是这样的. 一个常见的错误就是假设堆存储器被初始化为零:

[cpp] view plaincopy
  1. /* Return y = Ax */  
  2. int *matvec(int **A, int *x, int n)  
  3. {  
  4.     int i, j;  
  5.   
  6.     int *y = (int *)malloc(n * sizeof(int));  
  7.     for (i = 0; i < n; i++)  
  8.         for (j = 0; j < n; j++)  
  9.             y[i] += A[i][j] * x[j];  
  10.     return y;  
  11. }  
在这个例子中, 程序员不正确的假设向量(vector) y 被初始化为零. 正确的实现方式是显示的将 y[i] 设置为零, 或者是使用calloc函数.


3. 允许栈缓冲区溢出(Allowing Stack Buffer Overflows)


如果一个程序不检查输入串的大小就写入栈中的目标缓冲区, 那么这个程序就会有缓冲区溢出错误(buffer overflow bug). 例如, 下面的函数就有缓冲区溢出错误, 因为 gets 函数拷贝一个任意长度的串到缓冲区. 为了纠正这些错误, 我们必须使用 fgets 函数, 这个函数限制了输入串的大小:

[cpp] view plaincopy
  1. void bufoverflow()  
  2. {  
  3.     char buf[64];  
  4.   
  5.     gets(buf); /* Here is the stack buffer overflow bug */  
  6.   
  7.     return;  
  8. }  


4. 假设指针和他们指向的对象是相同大小的(Assuming that Pointers and the Objects They Point to Are the
Same Size)


一个常见的错误是假设指向对象的指针和他们所指向的对象是相同大小的:

[cpp] view plaincopy
  1. /* Create an nxm array */  
  2. int **makeArray1(int n, int m)  
  3. {  
  4.     int i;  
  5.     int **A = (int **)malloc(n * sizeof(int)); // 这个......  
  6.   
  7.     for (i = 0; i < n; i++)  
  8.         A[i] = (int *)malloc(m * sizeof(int));  
  9.     return A;  
  10. }  

这里的目的是创建一个由 n 个指针组成的数组, 每个指针都指向一个包含 m 个 int 的数组. 然而, 因为程序员在第五行将 sizeof(int *) 写成了 sizeof(int), 代码实际上创建一个 int 的数组.

这段代码只有在 int 和指向 int 的指针大小相同的机器上运行良好. 但是, 如果我们在像 core i7 这样的机器上运行这段代码, 其中指针大于 int, 那么第七行和第八行的循环将写到超过 A 数组结尾的地方. 因为这些字中的一个很可能是已分配块的边界标记脚部, 所以我们可能不会发现这个错误, 直到我们在这个程序的后面很久释放这个块时, 此时, 分配器中的合并代码会戏剧性的失败, 而没有任何明显的原因. 这是"在远处起作用(action at distance)" 的一个隐秘示例, 这个"action  at distance" 是与存储器有关(memory-related)的编程错误的典型情况.


5. 造成错位错误(Making Off-by-One Errors)


错位(off-by-one)错误是另一个很常见的覆盖错误来源:

[cpp] view plaincopy
  1. /* Create an nxm array */  
  2. int **makeArray2(int n, int m)  
  3. {  
  4.     int i;  
  5.     int **A = (int **)malloc(n * sizeof(int *));  
  6.   
  7.     for (i = 0; i <= n; i++)  
  8.         A[i] = (int *)malloc(m * sizeof(int));  
  9.     return A;  
  10. }  

这里我们创建一个 n 个元素的指针数组, 但是随后在第 7 行和第 8 行试图初始化这个数组的 n + 1 个元素, 在这个过程中覆盖了数组 A 后面的某个存储器位置.


6. 引用指针, 而不是他所指向的对象(Referencing a Pointer Instead of the Object It Points to)


如果不注意 C 操作符的优先级和结合性, 我们就会错误的操作指针, 而不是他所指向的对象. 比如, 下面的这个函数, 其目的是删除一个有 *size 项的二叉堆里的第一项, 然后对剩下的 *size - 1 项重新建堆:

[cpp] view plaincopy
  1. int *binheapDelete(int **binheap, int *size)  
  2. {  
  3.     int *packet = binheap[0];  
  4.   
  5.     binheap[0] = binheap[*size - 1];  
  6.     *size--;     /* This should be (*size)-- */  
  7.     heapify(binheap, *size, 0);  
  8.     return(packet);  
  9. }  

在第 6 行, 目的是减少 size 指针指向的整数的值. 然而, 因为一元运算符 -- 和 * 的优先级相同, 从右向左结合, 所以 第 6 行中的代码实际减少的是指针自己的值, 而不是他所指向的整数的值. 如果幸运的话, 程序会立即失败; 但是更有可能发生的是, 当程序执行过程后很久才产生出一个不正确的结果时, 我们只有一头雾水. 这里的原则是, 当你对优先级和结合性有疑问的时候, 就应该使用括号. 比如, 第 6 行, 我们可以使用表达式, 清晰的表明我们的意图.


7. 误解指针运算(Misunderstanding Pointer Arithmetic)


另一种常见的错误是忘了指针的算术操作是以他们指向的对象的大小为单位来进行的, 而这种大小单位并不一定是字节. 例如, 下面函数的目的是扫描一个 int 数组, 并返回一个指针, 指向 val 的首次出现:

[cpp] view plaincopy
  1. int *search(int *p, int val)  
  2. {  
  3.     while (*p && *p != val)  
  4.         p += sizeof(int); /* Should be p++ */  
  5.     return p;  
  6. }  

然而, 因为每次循环时, 第 4 行都把指针加了 4(一个 int 的字节数), 函数就不正确的扫描数组中每 4 个整数.


8. 引用不存在的变量(Referencing Nonexistent Variables)


没有太多经验的 C 程序员不理解栈的规则, 有时会引用不再合法的本地变量, 如下列所示:

[cpp] view plaincopy
  1. int *stackref(void)  
  2. {  
  3.     int  val;  
  4.   
  5.     return &val;  
  6. }  

这个函数返回一个指针(如 p), 指向栈里的一个局部变量, 然后弹出它的栈帧. 尽管 p 仍然指向一个合法的存储器地址, 但是它已经不再指向一个合法的变量了. 当以后在程序中调用其他函数时, 存储器将重用他们的栈帧. 再后来, 如果程序分配某个值给 *p, 那么它可能实际上正在修改另一个函数的栈帧中的一个条目, 从而潜在的带来灾难性的后果. 要是没有发现就悲剧了.


9. 引用空闲堆块中的数据(Referencing Data in Free Heap Blocks)


一个相似的错误就是引用已经被释放了的堆块中的数据. 例如, 考虑下面的例子, 这个实例在第 6 行分配了一个整数数组 x, 在第 10 行中先释放了块 x, 然后在第 14 行中又引用了它:

[cpp] view plaincopy
  1. int *heapref(int n, int m)  
  2. {  
  3.     int i;  
  4.     int *x, *y;  
  5.   
  6.     x = (int *)malloc(n * sizeof(int));  
  7.   
  8.     /* ... */          /* Other calls to malloc and free go here */  
  9.   
  10.     free(x);  
  11.   
  12.     y = (int *)malloc(m * sizeof(int));  
  13.     for (i = 0; i < m; i++)  
  14.         y[i] = x[i]++; /* Oops! x[i] is a word in a free block */  
  15.   
  16.     return y;  
  17. }  

取决于在第 6 行和第 10 行发生的 malloc 和 free 的调用模式, 当程序在第 14 行引用 x[i] 时, 数组 x 可能是某个其他已分配堆块的一部分了, 因此其内容被重写了. 和其他许多与存储器有关的错误一样, 这个错误只会在程序执行的后面, 当我们注意到 y 中的值被破坏了时才会显现出来.


10. 引起存储器泄露(Introducing Memory Leaks)


存储器泄露是缓慢, 隐性的杀手(slow, silent killers), 当程序员不小心忘记释放已分配块, 而在堆里创建了垃圾时, 会发生这种问题. 例如, 下面的函数分配了一个堆块 x, 然后不释放他就返回:

[cpp] view plaincopy
  1. void leak(int n)  
  2. {  
  3.     int *x = (int *)malloc(n * sizeof(int));  
  4.   
  5.     return/* x is garbage at this point */  
  6. }  

如果经常调用 leak, 那么渐渐地, 堆里就会充满了垃圾, 在最糟糕的情况下, 只会占用整个虚拟地址空间. 对于像守护进行和服务器这样的程序来说, 存储器泄露是特别严重的, 根据定义, 这些程序是不会终止的.
0 0
原创粉丝点击