最近读了一下《深入理解计算机操作系统》第 9 章,虚拟存储器。在 9.11 书上总结了 C 中常见的与存储器有关的错误。书上下面这一句话说的很有道理(我看的是中文版,觉得引用原版会更加原汁原味),就是指出现某次内存操作错误,如果当时就立刻表现出来,那会很幸运,不幸的是有时会在错误操作之后一段时间后才显现出来。所以有时候发现 coredump 文件堆栈中某次很奇怪的调用居然导致崩溃了,查不出来原因,有可能是前文环境中某次指针操作错误造成的。
所以就把书上这节内容整篇笔记来提醒自己。
Memory-related bugs are among the most frightening because they often manifest themselves at a distance, in both time and space, from the source of the bug. Write the wrong data to the wrong location, and your program can run for hours before it finally fails in some distant part of the program.
■ Dereferencing Bad Pointers
scanf("%d", var); // => scanf("%d", &var)
解引用时,使用了错误的指针。 C 是静态类型的语言,需要类型匹配,但是遇见变长函数参数时,无法检测类型,传递了错误类型而未被编译器查出,从而导致了错误。
■ Reading Uninitialized Memory
int *y = (int*)malloc(sizeof(int));
printf("value:%d\n", *y);
使用未初始化内存,错误的认为 malloc 分配的存储空间会被初始化为 0 。可以自己测试,使用 malloc 直接分配 500MB 空间,然后调用 getchar 等待,此时在任务管理器中查看进程内存并没有 500MB ,但是在 malloc 后使用 memset 设置为 0 后再在任务管理器中查看进程内存空间,发现就有了 500 多 MB 。因为后者访问了 malloc 返回的虚拟地址空间,从而导致把此地址映射到真正的物理地址空间中,进而进程的内存占用增大了。
■ Allowing Stack Buffer Overflows
char buf[64];
gets(buf);
代码很直观,不检测缓冲区空间大小,在使用时导致栈缓冲区溢出。
■ Assuming that Pointers and the Objects They Point to Are the Same Size
int **parr = (int**)malloc(10 * sizeof(int)); // 分配 10 个指针变量,每个指针指向 5 个 int
for (int i = 0; i < 10; i++)
parr[i] = (int*)malloc(5 * sizeof(int)); // 为指针变量赋值
在 32 位系统上 int 类型和指针类型都是 32 位,这段代码在 32 位系统上能正常运行但是移植到 64 位系统上就会出问题,因为 64 位系统上指针类型是 64 位,但是分配空间时却为这个 64 位的指针分配的是 32 位的大小。所以在 64 位系统上 for 循环中的赋值语句会超出实际可用的空间。若超出的部分覆盖了分配器的边界标记脚部(分配器的实现)或者覆盖了另一个 malloc 返回的空间中的值,所以可能不会立刻发现这个错误,等到调用 free 时或者另一个地址访问它自己的空间时,便会奇怪的出错。这就是因内存错误而导致的 action at a distance 的例子。
■ Making Off-by-One Errors
int n = 10, m = 5;
int **parr = (int**)malloc(n * sizeof(int*));
for (int i = 0; i <= n; i++)
parr[i] = (int*)malloc(m * sizeof(int));
遍历数组时,使用了越界的索引。i 等 0 开始,那么最大值应该是 n - 1 而不是 n 。
■ Referencing a Pointer Instead of the Object It Points to
int m = 100;
int *p = &m;
*p--; // => (*p)--;
程序本意是想指针 p 指向的变量减一,但是不清楚 * 和 -- 的优先级相同,从而从右向左结合导致先执行 -- ,程序错误变成了使指针 p 移动了前一个单位,并访问这个地址处的 int 值。
■ Misunderstanding Pointer Arithmetic
int arr[] = {1, 2, 3, 4};
int *p = arr;
int *next = p + sizeof(int); // => int *next = p + 1;
另一种常见的错误是指针的算术操作是以它们指向的对象的大小为单位进行的。比如 int *p; p = p + 1;
代码中 p + 1
表示 p 移动 1 * sizeof(int)
4 个字节,而不是一个字节。
■ Referencing Nonexistent Variables
int* func() {
int var = 100;
return &var;
}
另一种常见的错误是函数返回了指向局部变量的指针,函数返回时局部变量已经无效了,尽管返回的指针是一个合法的地址,但是它已经不再指向一个合法的变量了。以后在程序中调用其它函数时,这段栈空间会被重新利用,再后来如果赋值给这个返回的无效指针,那么它可能正在修改另一个函数的栈,从而带来灾难性的后果。
■ Referencing Data in Free Heap Blocks
int n = 10;
int *p = (int*)malloc(n * sizeof(int));
free(p);
int *arr = (int*)malloc(n * sizeof(int));
for (int i = 0; i < n; i++)
arr[i] = p[i];
一个相似的错误是引用了已经被释放的堆块中的数据。
■ Introducing Memory Leaks
void leak(size_t n) {
int *arr = (int*)malloc(n * sizeof(int));
return; // arr is garbage now
}
最后当然是万恶的内存泄漏了。
这里我是用内存( memory )代替了书中的虚拟存储器( virtual memory ),我们习惯上把 memory 翻译成内存,书上翻译的是存储器。
其实写这么多,主要是想提醒自己在实际项目中查找内存相关的问题时,除了要盯住出问题的地方,还要注意上下文中是否其他地方的错误指针操作留下的隐患。