登 录
注 册
< 系统运维
Linux
计算机系统
系统工具
系统硬件组成
高速缓存
存储器及操作系统
Amdahl定理
信息表示和处理
内存有关错误
全球IP因特网
信号量同步线程
热门推荐>>>
中台架构
中台建设与架构
Hadoop
源码分析-NN启动(三)
HBase
HBased对接Hive
Linux
Nginx高可用
Python
数据导出工具
Flink
3分钟搭建Flink SQL测试环境
Kafka
Kafka对接Flume
深度学习
卷积神经网络
数据结构与算法
选择合适的算法
MySQL
数据备份恢复
Hive
Hive调优参数大全
其他框架
Azkaban Flow1.0与2.0
ClickHouse
表引擎-其他类型
技术成长
最好的职业建议
精选书单
技术成长书单—机器学习
技术资讯
数据在线:计算将成为公共服务
开发工具
IntelliJ IDEA 20年发展回顾(二)
系统工具
Mac命令行工具
虚拟化
内存虚拟化概述
云原生
云原生构建现代化应用
云服务
一文搞懂公有云、私有云...
Java
Spring Boot依赖注入与Runners
Go
Go函数与方法
SQL
SQL模板
安全常识
一文读懂SSO
当前位置:
首页
>>
计算机系统
>>
内存有关错误
内存有关错误
2020-07-05 13:53:37 星期日 阅读:2172
![](/static/images/article_images/1693753382.264676.jpeg) 对于C程序员来说,管理和使用虚拟内存可能是个困难的、容易出错的任务。与内存有关的错误属于那些最令人惊恐的错误,因为它们在时间和空间上,经常在距错误源一段距离之后才表现出来。将错误的数据写到错误的位置,你的程序可能在最终失败之前运行了好几个小时,且使程序中止的位置距离错误的位置已经很远了。 ####间接引用坏指针 在进程的虚拟地址空间中有较大的漏洞。没有映射到任何有意义的数据。如果我们试图间接引用一个指向这些漏洞的指针,那么操作系统就会以段异常中止程序。而且虚拟内存的某些区域是只读的。试图写这些区域将会以保护异常中止这个程序。 间接引用坏指针的一个常见示例是经典的scanf错误。假设我们想要使用scanf从stdin读一个整数到一个变量。正确的方法是传递给scanf一个格式串和变量的地址: ``` scanf("%d", &val) ``` 然而,对于C程序员初学者而言(对有经验的也是如此),很容易传递val的内容,而不是其他的地址: ``` scanf("%d", val) ``` 在这种情况下,scanf将把val的内容解释为一个地址,并试图将一个字写到这个位置。在最好的情况下,程序立即异常终止。在最糟糕的情况下,val的内容对应于虚拟内存的某个合法的读写区域,于是我们就覆盖了这块内存,这通常会在相当长的一段时间以后造成灾难性的、令人困惑的后果。 ####读未初始化的内存 虽然bss内存位置(诸如未初始化的全局C变量)总是被加载器初始化为零,但是对于堆内存却不是这样的。一个常见的错误就是假设堆内存被初始化为零。 ``` /* Return y = Ax */ int *matvec(int **A, int *x, int n) { int i, j; int *y = (int *)Malloc(n * sizeof(int)); for (i = 0; i < n; i++) for (j = 0; j < n; j++) y[i] += A[i][j] * x[j]; return y; } ``` 在这个示例中,程序员不正确地假设向量y被初始化为0,正确的实现方式是显式地将y[i] 设置为0,或者使用calloc。 ####允许栈缓冲区溢出 正如我们在3.10.3节中看到的,如果一个程序不检查输入串的大小就写入栈中的目标缓冲区,那么这个程序就会有缓冲区溢出错误(buffer overflow bug)。例如,下面的函数就有缓冲区溢出错误,因为gets函数复制一个任意长度的串到缓冲区,为了纠正这个错误,我们必须使用fgets函数,这个函数限制了输入串的大小: ``` void bufoverflow(): { char buf[64]; gets(buf); /* Here is the stack buffer overflow bug */ return; } ``` ####假设指针和它们指向的对象是相同大小的 一种常见的错误时假设指向对象的指针和它们所指向的对象是相同大小的: ``` /* Create an nxm array */ 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; } ``` 这里的目的是创建一个由n个指针组成的数组,每个指针都指向一个包含m个int的数组。然而,因为程序员在第5行将sizeof(int *) 写成了sizeof(int),代码实际上创建的是一个int的数组。 这段代码只有在int和指向int的指针大小相同的机器上运行良好。但是,如果我们像Core i7这样的机器上运行这段代码,其中指针大于int,那么第7行和第8行的循环将写到超出A数组结尾的地方。因为这些字中的一个很可能是已分配块的边界标记脚部,所以我们可能不会发现这个错误,直到在这个程序的后面很久释放这个块时,此时,分配器中的合并代码会戏剧性的失败,而且没有任何明显的原因。这是“在远处起作用(action as distance)”的一个阴险的示例,这类“在远处起作用”是与内存有关的编程错误的典型情况。 ####造成错位错误 错位(off-by-one)错误是另一种很常见的造成覆盖错误的来源: ``` /* Create an nxm array */ int **makeArray2(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; } ``` 这是前面一节中程序的另一个版本。这里我们在第5行创建了一个n个元素的指针数组,但是随后在第7行和第8行试图初始化这个数组的n +1 个元素,在这个过程中覆盖了A数组后面的某个内存位置。 ####引用指针、而不是它所指向的对象 如果不太注意C操作符的优先级和结合性,我们就会错误地操作指针,而不是指针所指向的对象。比如考虑下面的函数,其目的是删除一个有*size项的二叉堆里的第一项,然后对剩下的*size-1项重新建堆: ``` int *binheapDelete(int **binheap, int *size) { int *packet = binheap[0]; binheap[0] = binheap[*size - 1]; *size--; /* This should be (*size) -- */ heapify(binheap, *size, 0); return(packet); } ``` 在第6行,目的是减少size指针指向的整数的值。然而,因为一元运算符--和*的优先级相同,从右向左结合,所以第6行中的代码实际减少的是指针自己的值,而不是它所指向的整数的值。如果幸运的话,程序会立即失败;但是更有可能发生的是,当程序在执行过程后很久才产生出一个不正确的结果时,我们只有一头雾水。这里的原则是当你对优先级和结合性有疑问的时候,就使用括号。比如,在第6行,我们可以使用表达式(*size)--,清晰地表明我们的意图。 ####误解指针运算 另一种常见的错误时忘记了指针的算术操作是以它们指向的对象的大小为单位来进行的,而这种大小单位并不一定是字节。例如,下面函数的目的是扫描一个int的数组,并返回一个指针,指向val的首次出现: ``` int *search(int *p, int val) { while (*p && *p != val) p += sizeof(int); /* Should be p++ */ return p; } ``` 然而,因为每次循环时,第4行都把指针加了4(一个整数的字节数),函数就不正确地扫描数组中每个4个整数。 ####引用不存在的变量 没有太多经验的C程序员不理解栈的规则,有时会引用不再合法的本地变量,如下所示: ``` int *stackerf() { int val; return &val; } ``` 这个函数返回一个指针(比如说是p),指向栈里的一个局部变变量,然后弹出它的栈帧。尽管p仍然指向一个合法的内存地址,但是它已经不再指向一个合法的变量了。当以后在程序中调用其他函数时,内存将重用它们的栈帧。再后来,如果程序分配某个值给*p,那么它可能实际上正在修改另一个函数的栈帧中的一个条目,从而潜在地带来灾难性的、令人困惑的后果。 ####引用空闲堆块中的数据 一个相似的错误是引用已经被释放了的堆块中的数据。例如,考虑下面的示例,这个示例在第6行分配了一个整数数组x,在第10行中先释放了块x。然后再第14行中又引用了它: ``` int *heapref(int n, int m) { int i; int *x, *y; x = (int *)Malloc(n * sizeof(int)); // Other calls to malloc and free go here free(x); y = (int *)Malloc(m * sizeof(int)); for (i = 0; i < m; i++) y[i] = x[i]++; /* Oops! x[i] is a word in a free block */ return y; } ``` 取决于在第6行和第10行发生的malloc和free的调用模式,当程序在第14行引用x[i]时,数组x可能是某个其他已分配堆块的一部分了,因此内容被重写了。和其他许多与内存有关的错误一样,这个错误只会在程序执行的后面,当我们注意到y中的值被破坏了时才会显现出来。 ####引起内存泄漏 内存泄漏是缓慢、隐性的杀手,当程序员不小心忘记释放已分配块,而在堆里创建了垃圾时,会发生这种问题。例如,下面的函数分配了一个堆块x,然后不释放它就返回: ``` void leak(int n) { int *x = (int *)Malloc(n * sizeof(int)); return; /* x is grabage at this point */ } ``` 如果经常调用leak,那么渐渐地,堆里就会充满了垃圾,最糟糕的情况下,会占用整个虚拟地址空间。对于像守护进程和服务器这样的程序来说,内存泄漏是特别严重的,根据定义这些程序是不会终止的。