原文链接 AddressSanitizer VS Valgrind
前言
C/C++ 等底层语言在提供强大功能及性能的同时,其灵活的内存访问也带来了各种纠结的问题。如果 crash 的地方正是内存使用错误的地方,说明你人品好。如果 crash 的地方内存明显不是 consistent 的,或者内存管理信息都已被破坏,并且还是随机出现的,那就比较麻烦了。当然,祼看 code 打 log 是一个办法,但其效率不是太高,尤其是在运行成本高或重现概率低的情况下。另外,静态检查也是一类方法,有很多工具(lint, cppcheck, klockwork, splint, o, etc.)。但缺点是误报很多,不适合针对性问题。另外好点的一般还要钱。最后,就是动态检查工具。下面介绍几个 Linux 平台下主要的运行时内存检查工具。绝大多数都是开源免费且支持 x86 和 ARM 平台的。
首先,比较常见的内存问题有下面几种:
- 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 的方法可以是用 C-preprocessor,也可以是在链接库中直接定义(因为 Glibc 中的 malloc/free 等函数都是 weak symbol),或是用 LD_PRELOAD。另外,通过 hook strcpy(),memmove()等函数可以检测它们是否引起 buffer overflow。
-
(2)为了检查内存的非法访问,需要对程序的内存进行 bookkeeping,然后截获每次访存操作并检测是否合法。bookkeeping 的方法大同小异,主要思想是用 shadow memory 来验证某块内存的合法性。至于 instrumentation 的方法各种各样。有 run-time 的,比如通过把程序运行在虚拟机中或是通过 binary translator 来运行;或是 compile-time 的,在编译时就在访存指令时就加入检查操作。另外也可以通过在分配内存前后加设为不可访问的 guard page,这样可以利用硬件(MMU)来触发 SIGSEGV,从而提高速度。
-
(3)为了检测栈的问题,一般在 stack 上设置 canary,即在函数调用时在栈上写 magic number 或是随机值,然后在函数返回时检查是否被改写。另外可以通过 mprotect()在 stack 的顶端设置 guard page,这样栈溢出会导致 SIGSEGV 而不至于破坏数据。
两个典型的内存检测工具
工具 1: AddressSanitizer
AddressSanitizer 是 Google 用于检测内存各种 buffer overflow(Heap buffer overflow, Stack buffer overflow, Global buffer overflow)的一个非常有用的工具。该工具是一个 LLVM 的 Pass,现已集成至 llvm 中,要是用它可以通过-fsanitizer=address 选项使用它。AddressSanitizer 的源码位于/lib/Transforms/Instrumentation/AddressSanitizer.cpp 中,Runtime-library 的源码在 llvm 的另一个项目 compiler-rt 的/lib/asan 文件夹中。
AddressSanitizer 早期只能在 Clang 编译器下使用,后来加入到 GCC 中,GCC4.8 及以上版本可使用此编译选项。4.8 版本 GCC 对 AddressSanitizer 支持有限,功能不太完善,输出的错误信息也不够友好,使用不太方便,建议使用 4.9 及以上版本。
使用实例:
选取 Google 提供的一段小代码,来说明 AddressSanitizer 的用法。
编写源代码:
use-after-free.c
#include <stdlib.h>
int main() {
char *x = (char*)malloc(10 * sizeof(char*));
free(x);
return x[5];
}
编译源代码
clang -fsanitize=address -O1 -fno-omit-frame-pointer -g use-after-free.c
运行程序可得以下输出结果
=========================
==7332==ERROR: AddressSanitizer: heap-use-after-free on address 0x607000000095 at pc 0x00010532cef4 bp 0x7ffeea8d38d0 sp 0x7ffeea8d38c8
READ of size 1 at 0x607000000095 thread T0
# 0 0x10532cef3 in main use-after-free.c:5
#1 0x7fff7c0a13d4 in start (libdyld.dylib:x86_64+0x163d4)
0x607000000095 is located 5 bytes inside of 80-byte region [0x607000000090,0x6070000000e0)
freed by thread T0 here:
#0 0x10538c20d in wrap_free (libclang_rt.asan_osx_dynamic.dylib:x86_64h+0x5c20d)
#1 0x10532ceba in main use-after-free.c:4
#2 0x7fff7c0a13d4 in start (libdyld.dylib:x86_64+0x163d4~~~~
previously allocated by thread T0 here:
#0 0x10538c053 in wrap_malloc (libclang_rt.asan_osx_dynamic.dylib:x86_64h+0x5c053)
#1 0x10532ceaf in main use-after-free.c:3
#2 0x7fff7c0a13d4 in start (libdyld.dylib:x86_64+0x163d4)
SUMMARY: AddressSanitizer: heap-use-after-free use-after-free.c:5 in main
Shadow bytes around the buggy address:
0x1c0dffffffc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x1c0dffffffd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x1c0dffffffe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x1c0dfffffff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x1c0e00000000: fa fa fa fa 00 00 00 00 00 00 00 00 03 fa fa fa
=>0x1c0e00000010: fa fa[fd]fd fd fd fd fd fd fd fd fd fa fa fa fa
0x1c0e00000020: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x1c0e00000030: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x1c0e00000040: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x1c0e00000050: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x1c0e00000060: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
Shadow gap: cc
==7332==ABORTING
Abort trap: 6
由提示 SUMMARY: AddressSanitizer: heap-use-after-free use-after-free.c:5 in main
可以知道在 main 函数的第 5 行存在 heap-use-after-free 的内存错误。
工具 2:Valgrind
Valgrind 是一套 Linux 下,开放源代码(GPL V2)的仿真调试工具的集合。Valgrind 由内核(core)以及基于内核的其他调试工具组成。内核类似于一个框架(framework),它模拟了一个 CPU 环境,并提供服务给其他工具;而其他工具则类似于插件 (plug-in),利用内核提供的服务完成各种特定的内存调试任务。
由于 Valgrind 没有和编译器集成,只能另行安装,安装方法就不说明,在此只简单说明其用法。
使用实例
为了使 valgrind 发现的错误更精确,如能够定位到源代码行,建议在编译时加上-g 参数,编译优化选项请选择 O0,虽然这会降低程序的执行效率。
生成可执行程序,如
gcc -g -O0 test.c -o test
生成可执行程序 test 之后,使用以下操作让 Valgrind 来生成内存的记录文件:
valgrind --leak-check=full --log-file=test_valgrind.log --num-callers=30 ./test
-
--log-file 后面的 test_valgrind.log 是指定生成的日志文件名称。
-
--num-callers 后面的 60 是生成的每个错误记录的追踪行数。30 是随便设定的,如果没指定,默认是 12 行(有可能有的追踪行就没显示)。
-
--leak-check=full 表示开启详细的内存泄露检测器。
编写 test.cpp
#include <iostream>
using namespace std;
int main()
{
int *a = new int[10];
a[11] = 0;
cout << a[11]<< endl;
return 0;
}
编译程序
gcc –g –o test test.cpp
使用 valgrind 分析程序
valgrind --tool=memcheck --leak-check=yes --show-reachable=yes test
输出结果如下
==2051== Memcheck, a memory error detector.
==2051== Copyright (C) 2002-2007, and GNU GPL'd, by Julian Seward et al.
==2051== Using LibVEX rev 1804, a library for dynamic binary translation.
==2051== Copyright (C) 2004-2007, and GNU GPL'd, by OpenWorks LLP.
==2051== Using valgrind-3.3.0, a dynamic binary instrumentation framework.
==2051== Copyright (C) 2000-2007, and GNU GPL'd, by Julian Seward et al.
==2051== For more details, rerun with: -v
==2051==
==2051== Invalid write of size 4
==2051== at 0x4009C6: main (test.cpp:7)
==2051== Address 0x4a2005c is 4 bytes after a block of size 40 alloc'd
==2051== at 0x490581B: operator new[](unsigned long) (vg_replace_malloc.c:274)
==2051== by 0x4009B9: main (test.cpp:6)
==2051==
==2051== Invalid read of size 4
==2051== at 0x4009D4: main (test.cpp:8)
==2051== Address 0x4a2005c is 4 bytes after a block of size 40 alloc'd
==2051== at 0x490581B: operator new[](unsigned long) (vg_replace_malloc.c:274)
==2051== by 0x4009B9: main (test.cpp:6)
0
==2051==
==2051== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 9 from 4)
==2051== malloc/free: in use at exit: 40 bytes in 1 blocks.
==2051== malloc/free: 1 allocs, 0 frees, 40 bytes allocated.
==2051== For counts of detected errors, rerun with: -v
==2051== searching for pointers to 1 not-freed blocks.
==2051== checked 198,560 bytes.
==2051==
==2051==
==2051== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
==2051== at 0x490581B: operator new[](unsigned long) (vg_replace_malloc.c:274)
==2051== by 0x4009B9: main (test.cpp:6)
==2051==
==2051== LEAK SUMMARY:
==2051== definitely lost: 40 bytes in 1 blocks.
==2051== possibly lost: 0 bytes in 0 blocks.
==2051== still reachable: 0 bytes in 0 blocks.
==2051== suppressed: 0 bytes in 0 blocks.
从 LEAK SUMMARY:
来看,确定有一块 40byte 的内存泄漏。
AddressSanitizer 和 Valgrind 综合对比
关于 AddressSanitizer 和 Valgrind 的具体实现和源码分析在后续的文章中会详细说明,以下仅做功能性的对比。
| --- | --- | --- | --- |
| |
| technology | CTI | DBI |
| ARCH| x86, ARM, PPC |x86, ARM, PPC, MIPS, S390X, TILEGX |
|OS| Linux, OS X, Windows, FreeBSD, Android, iOS Simulator|Linux, OS X, Solaris, Android |
|Slowdown | 2x| 20x | [Heap OOB](https://github.com/google/sanitizers/wiki/AddressSanitizerExampleHeapOutOfBounds)| yes| yes| |[Stack OOB](https://github.com/google/sanitizers/wiki/AddressSanitizerExampleStackOutOfBounds)| yes| no| |[Global OOB](https://github.com/google/sanitizers/wiki/AddressSanitizerExampleGlobalOutOfBounds)| yes| no| |[UAF](https://github.com/google/sanitizers/wiki/AddressSanitizerExampleUseAfterFree)| yes|yes| |[UAR](https://github.com/google/sanitizers/wiki/AddressSanitizerExampleUseAfterReturn)| yes| no| |UMR| no| yes| |Leaks|yes|yes|
各简写含义如下:
DBI: dynamic binary instrumentation(动态二进制插桩)
CTI: compile-time instrumentation (编译时插桩)
UMR: uninitialized memory reads (读取未初始化的内存)
UAF: use-after-free (aka dangling pointer) (使用释放后的内存)
UAR: use-after-return (使用返回后的值)
OOB: out-of-bounds (溢出)
x86: includes 32- and 64-bit.
另外还有一些其他内存检测工具对比和详细说明可参考 AddressSanitizerComparisonOfMemoryTools
部分参考:Linux 中的常用内存问题检测工具
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于