内存问题检测工具

内存问题检测工具

C/C++ 语言,程序员可以管理内存,十分便利,但同时灵活的内存访问也带来了很多很多的问题。尤其是那些偶现的,运行成本较高的场景,往往让我们的开发工作效率大大减慢。

一种办法是静态检查,但问题是很多测试场景跑不到,并且误报较多。另外一种就是动态检查工具,本文就是介绍几种 Linux 下运行时的内存检查工具。

一、内存问题

  • memory overrun:写内存越界
  • double free:同一块内存释放两次
  • use after free:内存释放后使用
  • wild free:释放的内存指针是非法的
  • access uninitialized memory:访问未初始化内存
  • read invalid memory:读取非法内存,本质上也是内存越界
  • memory leak:内存泄漏
  • use after return:caller 访问一个指针,但该指针指向 callee 的栈内内存
  • stack overflow:栈溢出

针对如上的内存问题,主要有如下几种方法:

  1. 为了检查内存非法使用,需要 hook 内存分配和操作函数。hook 的方法很多,可以使用重写 malloc/free,(Glibc 中的 malloc/free 等函数都是 weak symbol),使用 LD_PRELOAD。另外,通过 hook strcpy、memmove 等 C 库函数可以检查他们是否引起 buffer overflow
  2. 为了检查内存的非法访问,需要对程序的内存进行记账(bookkeeping),然后截获每次访问操作并检查是否合法。记账的方法主要思想是用 shadow memory 来验证某块内存的合法性。比如运行时,将程序运行在虚拟机中;比如编译时,在编译的时候就在访问内存指令处加入检查操作;另外,还可以通过在分配内存前后加 guard page(设为不可访问的),这样可以利用硬件(MMU)来触发 SIGSEGV,从而提高速度。
  3. 为了检查找的问题,一般在 stack 上设置魔数,即在函数调用时在栈上写魔数或者随机值,然后再函数返回时检查是否被改写。也可以通过 mprotect 在 stack 的顶端设置 guard page,这样栈溢出会导致 SIGSEGV 而不至于破坏数据。

如上的方法有些强于功能,有些胜于性能,有些需要重新编译可执行文件,有些使用比较方便,总之各有优势。

如下是功能的对比图

Tool / Function memory overrun double free use after free wild free access uninited read invalid memory memory leak use after return stack overflow
Memory checking tools in Glibc yes yes yes yes(if use memcpy,strcpy,etc)
TCMalloc / jemalloc(Gperftools) yes
Valgrind yes yes yes yes yes yes yes yes yes
Address Sanitizer(ASAN) yes yes yes yes (Memory Sanitizer) yes yes yes yes
Memwatch yes yes yes
Dr.Memory yes yes yes yes yes yes yes yes
Electrict Fence yes yes yes yes
Dmalloc yes yes yes yes yes

二、工具介绍

1. Memory checking tools in Glibc

设置 MALLOC_CHECK_ = 3 环境变量可以设置内存检测行为

1
2
3
MALLOC_CHECK_ = 0  // 和没设置一样,将忽略这些错误  
MALLOC_CHECK_ = 1 // 将打印一个错误告警
MALLOC_CHECK_ = 2 // 程序将收到 SIGABRT 信号退出

设置 LIBC_FATAL_STDERR_ = 1 可以将这些信息输出到 stderr。

mcheck:mcheck 是 Glibc 中的堆内存一致性检查机制,使用的时候需要加上头文件,并且在需要检查的地方加上:

1
2
3
4
5
6
7
#include <mcheck>

// 需要检查的地方加上如下代码
if (mcheck(NULL) != 0) {
fprintf(stderr, "mcheck() failed\n");
exit(EXIT_FAILURE);
}

编译时加上 -lmcheck 然后运行即可。

_FORTIFY_SOURCE:提供轻量化的 buffer overflow 检测。设置后会调用 Glibc 中的 _chk 后缀的函数,做一些运行时检查。主要检查各种字符串缓冲区溢出和内存操作。比如memmove, memcpy, memset, strcpy, strcat, vsprintf等。注意一些平台上编译时要加-O1或以上优化。这样就可以检查出因为那些内存操作函数导致的缓冲溢出问题:

1
g++ -Wall -g -O2 -D_FORTIFY_SOURCE=2 problem.cpp -o bug

mtrace:可以用于检查 malloc/free 是否正确配对。用时用mtrace()和muntrace()表示开始和结束内存分配trace(如果检测到结束结尾的话可以不用muntrace())。但这是简单地记录没有free对应的malloc,可能会有一些false alarm。

1
2
3
4
#include <mcheck.h>
mtrace();
// …
muntrace();

编译:g++ -Wall -g xxx.cpp -o main ,通过环境变量设置输出的日志文件:export MALLOC_TRACE=output.log

最后用 mtrace 命令将输出文件变得可读:mtrace ./main $MALLOC_TRACE

就可以得到那些地方的内存申请没有 free 掉

2. Gperftools

TC-Malloc 和 jemalloc 都是优秀的内存管理库,性能上也比 Glibc 的 ptmalloc 好。也提供了相关内存泄漏的检测工具。

编译对应的三方库,libjemalloc.so 或者 libtcmalloc.so 。以 jemalloc 来举例。修改环境变量,

1
2
3
export MALLOC_CONF="prof:true,lg_prof_interval:26"
运行可执行文件
LD_PRELOAD=xxx/libjemalloc.so ./xxx

一段时间后,运行目录下会产生 .heap 文件,我们取最开始和最后面的两个 heap 文件比较即可

1
jeprof --pdf xxx_exe --base=xxx.start.heap  xxx.end.heap  > xxx.pdf

即有可能分析出来内存泄漏的位置

3. Valgrind

其原理是让程序跑在一个虚拟机上,因此速度会慢几十倍。好在现实中很多程序是IO bound的,所以很多时候没有慢到忍无可忍的地步。好处是它不需要重新编译目标程序。它会通过hash表记录每个heap block,同时通过shadow memory记录这些内存区域的信息。这样就可以在每次访存时检查其合法性。

运行时可以根据需求配置参数:

1
valgrind --tool=memcheck --error-limit=no --track-origins=yes  --trace-children=yes --track-fds=yes ./xxx

详情见:https://valgrind.org/docs/manual/mc-manual.html

4. Address Sanitizer(ASAN)

在 LLVM 和 Gcc 编译器中集成。ASAN 在编译可执行文件时在访问内存操作中插入额外指令,同时通过 shadow memory 来记录和检测内存的有效性。性能视场景的不同而不同,官方称消耗在 2倍左右,我实际在项目中使用时消耗不止2倍

1
2
3
// 编译可执行文件时
g++ -Wall -g xxx.cpp -o main -fsanitize=address -fno-omit-frame-pointer
// 运行即可检测出问题

检测一些特定问题需要加上专门的选项

1
2
要检查访问指向已被释放的栈空间:ASAN_OPTIONS=delect_stack_use_after_return=1
检测 memory leak 需要加上:ASAN_OPTIONS=delect_leaks=1

5. Memwatch

Memwatch是一个轻量级的内存问题检测工具。主要用于检测内存分配释放相关问题及内存越界访问问题。通过C preprocessor,Memwatch替换所有 ANSI C的内存分配 函数,从而记录分配行为。注意它不保证是线程安全的。效率上,大块分配不受影响,小块分配会受影响,因此它没法使用原分配函数中的memory pool。最坏情况下会有3-5x的slowdown。它可以比较方便地模拟内存受限情况。对于未初始化内存访问,和已释放内存访问,Memwatch会poison相应内存(分配出来写0xFE,释放内存写0xFD)从而在出错时方便调试。

下载地址:https://sourceforge.net/projects/memwatch/files/memwatch/

需要带上 memwatch.c 一起编译,如下是源码自带的 test.c 的例子

1
gcc -o test -DMEMWATCH -DMEMWATCH_STDIO test.c memwatch.c

编译完后,运行程序,如果出现问题,错误信息会出现在 memwatch.log 中

6. Electric Fence

Electric Fence主要用于追踪buffer overflow的读和写。它利用硬件来抓住越界访问的指令。其原理是为每一次内存申请额外申请一个page或一组page,然后把这些buffer范围外的page设为不可读写。这样,如果程序访问这些区域,由于页表中这个额外page的权限是不可读写,会产生段错误。那些被free()释放的内存也会被设为不可访问,因此访问也会产生段错误。因为读写权限以页为单位,所以如果多的页放在申请内存区域后,可防止overflow。如果要防止underflow,就得用环境变量EF_PROTECT_BELOW在区域前加保护页。因为Electric Fence至少需要丙个页来满足内存分配申请,因此内存使用会非常大,好处是它利用了硬件来捕获非法访问,因此速度快。也算是空间换时间吧。
目前支持Window, Linux平台,语言支持C/C++。限制包括无法检测使用未初始化内存,memory leak等。同时它不是线程安全的。

具体见:http://linux.die.net/man/3/efence

7. Dmalloc

官方网站:https://dmalloc.com/releases/

dmalloc通过在分配区域增加padding magic number的做法来检测非法访问,因此它能够检测到问题但不能检测出哪条指令出的错。Dmalloc只能检测越界写,但不能检测越界读。另外,Dmalloc只检测堆上用malloc系函数(而不是sbrk()或mmap())分配的内存,而无法对栈内存和静态内存进行检测。 本质上它也是通过hook malloc(), realloc(), calloc(),free()等内存管理函数,还有strcat(), strcpy()等内存操作函数,来检测内存问题。它支持x86, ARM平台,语言上支持C/C++,并且支持多线程。

8. Dr. Memory

重量级内存监测工具之一,用于检测如未初始化内存访问,越界访问,已释放内存访问,double free,memory leak以及Windows上的handle leak, GDI API usage error等。它支持Windows, Linux和Mac操作系统, IA-32和AMD64平台,和其它基于binary instrumentation的工具一样,它不需要改目标程序的binary。有个缺点是目前只针对x86上的32位程序。貌似目前正在往ARM上port。其优点是对程序的正常执行影响小,和Valgrind相比,性能更好。官网为http://www.drmemory.org/。Dr. Memory基于DynamioRIO Binary Translator。原始代码不会直接运行,而是会经过translation后生成code cache,这些code cache会调用shared instrumentation来做内存检测。

可以在官网查看相关教程:https://drmemory.org/

9. Stack protection

对于栈内存,GCC本身提供了一些检错机制。加上-fstack-protector后,GCC会多加指令来检查buffer/stack overflow。原理是为函数加guard variable。在函数进入时初始化,函数退出时检查。相关的flag有-fstack-protector-strong -fstack-protector -fstack-protector-all等。

1
g++ -Wall -O2 -U_FORTIFY_SOURCE -fstack-protector-all problem.cpp -o bug

对于线程的栈可以参考:pthread_attr_setguardsize()

三、总结

大体来说,遇到诡异的内存问题,先可以试下Glibc和GCC里自带的检测机制,因为enable起来方便。如果检测不出来,那如果toolchain版本较新且有编译环境,可以先尝试ASan,因为其功能强大,且效率高。接下来,如果程序是I/O bound或slowdown可以接受,可以用Valgrind和Dr.Memory。它们功能强大且无需重新编译,但速度较慢,且后者不支持64位程序和ARM平台。然后可以根据实际情况和具体需要考虑Memwatch,Dmalloc和Electric Fence等工具。


参考: