内存问题检测工具
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:栈溢出
针对如上的内存问题,主要有如下几种方法:
- 为了检查内存非法使用,需要 hook 内存分配和操作函数。hook 的方法很多,可以使用重写 malloc/free,(Glibc 中的 malloc/free 等函数都是 weak symbol),使用 LD_PRELOAD。另外,通过 hook strcpy、memmove 等 C 库函数可以检查他们是否引起
buffer overflow
- 为了检查内存的非法访问,需要对程序的内存进行记账(bookkeeping),然后截获每次访问操作并检查是否合法。记账的方法主要思想是用 shadow memory 来验证某块内存的合法性。比如运行时,将程序运行在虚拟机中;比如编译时,在编译的时候就在访问内存指令处加入检查操作;另外,还可以通过在分配内存前后加 guard page(设为不可访问的),这样可以利用硬件(MMU)来触发 SIGSEGV,从而提高速度。
- 为了检查找的问题,一般在 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 | MALLOC_CHECK_ = 0 // 和没设置一样,将忽略这些错误 |
设置 LIBC_FATAL_STDERR_ = 1
可以将这些信息输出到 stderr。
mcheck:mcheck 是 Glibc 中的堆内存一致性检查机制,使用的时候需要加上头文件,并且在需要检查的地方加上:
1 | #include <mcheck> |
编译时加上 -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 | #include <mcheck.h> |
编译: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 | export MALLOC_CONF="prof:true,lg_prof_interval:26" |
一段时间后,运行目录下会产生 .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 | // 编译可执行文件时 |
检测一些特定问题需要加上专门的选项
1 | 要检查访问指向已被释放的栈空间:ASAN_OPTIONS=delect_stack_use_after_return=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等工具。
参考: