printf 对于内存布局的影响

起因

最近参加了一个存储的比赛,需要在 Linux 下使用 C/C++ 来完成,需要频繁的使用 new/malloc 来进行动态内存的申请,时不时会出现段错误,在 Debug 过程中,部分地方使用了 printf 来打印对应的参数,但却发现,有些地方加上了 printf 后不会再报段错误,经过一段时间的思考后,得到了如下的结果

发生错误的代码

这里所给的代码只是一个实例代码

考虑如下的代码

#include <stdio.h>
#include <stdlib.h>

#define BUFSIZE 67100

struct Disk {
  unsigned char *fp;
  char buf[BUFSIZE + 1];
  char filename[BUFSIZE + 1];
};

int main() {
  //printf("Test Start\n");
  struct Disk *d = (struct Disk *)malloc(sizeof(void *));
  d->fp = NULL;
  for (int i = 0; i < BUFSIZE + 1; ++i) {
    d->buf[i] = 0;
    if (i <= BUFSIZE) {
      d->filename[i] = '0';
    } else
      d->filename[i] = 0;
  }
  free(d);
  return 0;
}

注意到这里指针 d 肯定是越界了,但是我们来写一个测试的来看看,测试系统为 Debian,使用的编译器为 gcc

gcc example.c -o example.o -O0

可以看到是可以顺利执行的

之后将 printf 行注释掉,再次编译运行,结果如下

可以看到这次出发了 Linux 的段错误

printf 函数正常下不应该对程序产生影响,但是在这里,是否有 printf 却会让程序崩溃,这是否是 printf 的实现 bug 呢,显然不是,问题在于内存越界的问题,想要了解可以继续往下看

原因

在这里,首先需要了解 printf 和 OS 的一些基础。

printf 在首次运行的时候回去申请一块内存空间用于缓冲,这部分的缓冲区是在堆上的,也就是和我们动态内存分配 (new/malloc) 是在同一个内存区域中的,而 OS 对于堆内存的管理方案通常都是采用分页来管理,也就是每次对进程的内存分配请求的回应是以页为单位的,通常的页大小为 4KB,也就是可能我们只是请求了 2KB 的内存,但是对于 OS (如果这次分配触发了系统调用),那么返回的是一个新的 4K 的页,了解了这些之后可以开始对我们这里的 bug 进行处理了

struct Disk *d = (struct Disk *)malloc(sizeof(void *)); 这里显然就是导致错误的点,这里只对 d 申请了一个指针的大小,而 struct Disk 的大小是远远超过的,也就是说对于这个返回的内存区域,只有 d + 8 或者 d + 4 是合法的,但是这里,在最开始还能正常运行,是因为上述说到的原因,malloc 对于返回的是一个(或者多个)页的起始地址,而这个页都是被该进程合法占用的,所以在这里都能进行正常的写入,不会触发段错误,但加入了 printf 后,如上面所说,申请了一块缓冲区,这让返回的 d 的地址往后偏移,导致后续的访问跃出了该页,正常的程序,如果合法占用内存,那么该进程的页表中是有这个内存项的,但这里显然不是,所以再次访问的时候会触发缺页中断,让 OS 将对应的页调入,但这里访问的是非法的内存地址,从而触发段错误

所以 Debug 的时候别滥用 printf


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!