巧用 gcc 和 glibc 调试内存问题
工程中,我们常常会遇到内存访问越界导致的问题。一般而言,如果被踩到的内存没有被释放或者用作他途(比如恰好记录着 heap 的数据结构),是不会发生问题的。但是一旦发生 core dump,定位起来就非常麻烦。
先来看这样一段危险代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | //malloc_check.cc #include <stdio.h> #include <string.h> void func() { int* ptr = new int; memset(ptr, 0, sizeof(int) * 128); } int main(int argc, const char* argv[]) { func(); printf("hello world!\n"); return 0; } |
这里虽然发生了严重内存访问越界,但是运行时并没有什么异常:
1 2 3 | $ g++ -g malloc_check.cc $ ./a.out hello world! |
但如果被踩到的内存后续被释放的话,就会发生问题。
1 2 3 4 5 | void func() { int* ptr = new int; memset(ptr, 0, sizeof(int) + 1); delete ptr; } |
我们将先前申请的内存释放掉,然后编译运行:
1 2 3 4 | $ g++ -g malloc_check.cc $ ./a.out *** Error in `./a.out': free(): invalid next size (fast): 0x0000000001c45010 *** [1] 36756 unknown signal (core dumped) ./a.out |
可以看到,在 free 的时候发生了问题。这里可以使用 gdb 找到发生内存访问越界的地方。但是如果遇到大型程序的话,类似方式往往很难定位到确切的位置。
这里提供两种方式,分别使用 C 运行库 glibc 和编译器 gcc 内置的功能,来帮忙调试类似的问题。
一、MALLOC_CHECK_
GNU 的 C 标准库(glibc)支持动态内存调试,需要通过环境变量 MALLOC_CHECK_ 来设定其行为,其取值有四个等级:
- MALLOC_CHECK_=0 关闭所有检查
- MALLOC_CHECK_=1 当有错误被探测到时,在标准错误输出(stderr)上打印错误信息
- MALLOC_CHECK_=2 当有错误被探测到时,不显示错误信息,直接调用 abort 进行中断
- MALLOC_CHECK_=3 当有错误被探测到是,同时执行 1 和 2
默认情况下,该环境变量是没有设定的,需要我们手工去设定:
1 | $ export MALLOC_CHECK_=3 |
再次运行上述程序,glibc 就会调用 abort 中断程序,并给出丰富的提示信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | $ ./a.out *** Error in `./a.out': free(): invalid pointer: 0x00000000022c1010 *** ======= Backtrace: ========= /lib/x86_64-linux-gnu/libc.so.6(+0x7329f)[0x7f8360e5729f] /lib/x86_64-linux-gnu/libc.so.6(+0x803fe)[0x7f8360e643fe] ./a.out[0x400715] ./a.out[0x40072b] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf5)[0x7f8360e05f45] ./a.out[0x400619] ======= Memory map: ======== 00400000-00401000 r-xp 00000000 08:02 82776200 /home/nerd/a.out 00600000-00601000 r--p 00000000 08:02 82776200 /home/nerd/a.out 00601000-00602000 rw-p 00001000 08:02 82776200 /home/nerd/a.out 022c1000-022e2000 rw-p 00000000 00:00 0 [heap] 7f83608c8000-7f83608de000 r-xp 00000000 08:02 82116801 /lib/x86_64-linux-gnu/libgcc_s.so.1 7f83608de000-7f8360add000 ---p 00016000 08:02 82116801 /lib/x86_64-linux-gnu/libgcc_s.so.1 7f8360add000-7f8360ade000 rw-p 00015000 08:02 82116801 /lib/x86_64-linux-gnu/libgcc_s.so.1 7f8360ade000-7f8360be3000 r-xp 00000000 08:02 82179404 /lib/x86_64-linux-gnu/libm-2.19.so 7f8360be3000-7f8360de2000 ---p 00105000 08:02 82179404 /lib/x86_64-linux-gnu/libm-2.19.so 7f8360de2000-7f8360de3000 r--p 00104000 08:02 82179404 /lib/x86_64-linux-gnu/libm-2.19.so 7f8360de3000-7f8360de4000 rw-p 00105000 08:02 82179404 /lib/x86_64-linux-gnu/libm-2.19.so 7f8360de4000-7f8360fa2000 r-xp 00000000 08:02 82179418 /lib/x86_64-linux-gnu/libc-2.19.so 7f8360fa2000-7f83611a2000 ---p 001be000 08:02 82179418 /lib/x86_64-linux-gnu/libc-2.19.so 7f83611a2000-7f83611a6000 r--p 001be000 08:02 82179418 /lib/x86_64-linux-gnu/libc-2.19.so 7f83611a6000-7f83611a8000 rw-p 001c2000 08:02 82179418 /lib/x86_64-linux-gnu/libc-2.19.so 7f83611a8000-7f83611ad000 rw-p 00000000 00:00 0 7f83611ad000-7f8361293000 r-xp 00000000 08:02 128451945 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.19 7f8361293000-7f8361492000 ---p 000e6000 08:02 128451945 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.19 7f8361492000-7f836149a000 r--p 000e5000 08:02 128451945 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.19 7f836149a000-7f836149c000 rw-p 000ed000 08:02 128451945 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.19 7f836149c000-7f83614b1000 rw-p 00000000 00:00 0 7f83614b1000-7f83614d4000 r-xp 00000000 08:02 82179407 /lib/x86_64-linux-gnu/ld-2.19.so 7f83616b8000-7f83616be000 rw-p 00000000 00:00 0 7f83616d2000-7f83616d3000 rw-p 00000000 00:00 0 7f83616d3000-7f83616d4000 r--p 00022000 08:02 82179407 /lib/x86_64-linux-gnu/ld-2.19.so 7f83616d4000-7f83616d5000 rw-p 00023000 08:02 82179407 /lib/x86_64-linux-gnu/ld-2.19.so 7f83616d5000-7f83616d6000 rw-p 00000000 00:00 0 7ffdeee83000-7ffdeeea5000 rw-p 00000000 00:00 0 [stack] 7ffdeef45000-7ffdeef47000 r-xp 00000000 00:00 0 [vdso] 7ffdeef47000-7ffdeef49000 r--p 00000000 00:00 0 [vvar] ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall] [1] 36960 unknown signal (core dumped) ./a.out |
二、AdressSanitizer
AddressSanitizer 是 Google 出品的一款开源的内存检测工具,可以用来检测内存损坏错误,例如缓冲区溢出或访问悬空指针等一系列问题。它首先被 clang 支持,继而又被 gcc 支持,gcc 4.8 及以后的版本直接可用。
1 | $ g++ -g malloc_check.cc -fsanitize=address |
再次运行该程序,可以看到 AddressSanitizer 提示 heap-buffer-overflow 并给出了相关内存地址的申请和读写操作的 backtrace,可以快速定位问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | $ ./a.out ================================================================= ==37328== ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60040000dff4 at pc 0x400957 bp 0x7fffa7c7e480 sp 0x7fffa7c7e478 WRITE of size 1 at 0x60040000dff4 thread T0 #0 0x400956 (/home/nerd/a.out+0x400956) #1 0x400999 (/home/nerd/a.out+0x400999) #2 0x7f19a8a9df44 (/lib/x86_64-linux-gnu/libc-2.19.so+0x21f44) #3 0x400808 (/home/nerd/a.out+0x400808) 0x60040000dff4 is located 0 bytes to the right of 4-byte region [0x60040000dff0,0x60040000dff4) allocated by thread T0 here: #0 0x7f19a8e5681a (/usr/lib/x86_64-linux-gnu/libasan.so.0.0.0+0x1181a) #1 0x4008de (/home/nerd/a.out+0x4008de) #2 0x400999 (/home/nerd/a.out+0x400999) #3 0x7f19a8a9df44 (/lib/x86_64-linux-gnu/libc-2.19.so+0x21f44) Shadow bytes around the buggy address: 0x0c00ffff9ba0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x0c00ffff9bb0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x0c00ffff9bc0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x0c00ffff9bd0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x0c00ffff9be0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa =>0x0c00ffff9bf0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa[04]fa 0x0c00ffff9c00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x0c00ffff9c10: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x0c00ffff9c20: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x0c00ffff9c30: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x0c00ffff9c40: 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 Heap righ redzone: fb Freed Heap region: fd Stack left redzone: f1 Stack mid redzone: f2 Stack right redzone: f3 Stack partial redzone: f4 Stack after return: f5 Stack use after scope: f8 Global redzone: f9 Global init order: f6 Poisoned by user: f7 ASan internal: fe ==37328== ABORTING |
当然,这么强大的工具,也是有缺陷的。AddressSanitizer 编译的程序的堆、栈占用比原生程序的大,因此对于某些嵌入式设备可能并不太适用。