我用最多的排查c程序的内存问题的工具就是valgrind了,但是它这个工具有时候不是太好用,比如说,速度很慢,平时运行一次1s的程序,用valgrind排查可能要10s左右,还有没有好用的工具那,这不就发现了一个快速内存错误检测工具:
Address Sanitizer。
一 简单介绍
Address Sanitizer是谷歌的快速的内存错误检测工具,它非常快只拖慢程序2倍左右的速度,在这次使用过程中,也是深有体会。在GCC 4.9版本以上,就可以很好的使用了。
Sanitizers是谷歌发起的开源工具集,包括了AddressSanitizer, MemorySanitizer, ThreadSanitizer, LeakSanitizer,Sanitizers项目本是LLVM项目的一部分,但GNU也将该系列工具加入到了自家的GCC编译器中。GCC从4.8版本开始支持Address和Thread Sanitizer,4.9版本开始支持Leak Sanitizer和UB Sanitizer,这些都是查找隐藏Bug的利器。
可以支持的内存检测:
- Use after free
- Heap buffer overflow
- Stack buffer overflow
- Global buffer overflow
- Use after return
- Use after scope
- Initialization order bugs
- Memory leaks
二 使用方法
- 确保自己的gcc版本在4.9以上,4.8版本上缺少符号信息,我的测试机器:
centos 7.x ,gcc的版本是4.8.5版本,需要升级。
gcc -v
gcc version 4.8.5 20150623 (Red Hat 4.8.5-39) (GCC)
升级过程:
wget https://mirrors.ustc.edu.cn/gnu/gcc/gcc-4.9.4/gcc-4.9.4.tar.gz
tar xvf gcc-4.9.4.tar.gz
cd gcc-4.9.4
./contrib/download_prerequisites
mkdir build-gcc
cd build-gcc/
../gcc-4.9.4/configure --enable-checking=release --enable-languages=c,c++ --disable-multilib
make -j 8
make install
注意编译安装gcc的过程,超级慢要1-2个小时。
- 编译时候添加选项:
-fsanitize=address -fno-omit-frame-pointer -fno-optimize-sibling-calls -O0
编译的时候说缺少个依赖的库。
通过:
yum install libasan -y
命令可以安装下。
注意:
Address Sanitizer 会替换malloc和free,如果采用第三方的内存申请库,则无法替换,会造成功能缺失。
主要可以检查的内存问题有:
- Out-of-bounds accesses to heap, stack and globals
- Use-after-free
- Use-after-return (runtime flag ASAN_OPTIONS=detect_stack_use_after_return=1)
- Use-after-scope (clang flag -fsanitize-address-use-after-scope)
- Double-free, invalid free
- Memory leaks (experimental)
三 实践测试
3.1 栈溢出
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 int func0(void)
5 {
6 char str[4] = {0};
7 strcpy(str,"1234");
8 return 0;
9 }
10 int main(int argc,char *argv[])
11 {
12 func0();
13 return 0;
14 }
命令:
gcc -g main.c -o t1 -fsanitize=leak -fsanitize=address -fno-omit-frame-pointer
运行报错:
[root@localhost tests]# ./t1
./t1: error while loading shared libraries: libasan.so.1: cannot open shared object file: No such file or directory
缺少动态库,解决办法:
[root@localhost tests]# find / -name libasan.so.1
/usr/local/lib64/libasan.so.1
[root@localhost tests]# echo /usr/local/lib64 >> /etc/ld.so.conf
[root@localhost tests]# ldconfig
运行显示:
[root@localhost tests]# ./t1
=================================================================
==2890==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffed0cfce50 at pc 0x400965 bp 0x7ffed0cfce20 sp 0x7ffed0cfce18
WRITE of size 5 at 0x7ffed0cfce50 thread T0
#0 0x400964 in func0 /home/miaohq/tests/main.c:7
#1 0x4009d2 in main /home/miaohq/tests/main.c:12
#2 0x7f73dfa19444 in __libc_start_main (/lib64/libc.so.6+0x22444)
#3 0x400738 (/home/miaohq/tests/t1+0x400738)
Address 0x7ffed0cfce50 is located in stack of thread T0 at offset 32 in frame
#0 0x400815 in func0 /home/miaohq/tests/main.c:5
This frame has 1 object(s):
[32, 36) 'str' <== Memory access at offset 32 partially overflows this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism or swapcontext
(longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-buffer-overflow /home/miaohq/tests/main.c:7 func0
Shadow bytes around the buggy address:
....
说明:
- 错误类型是 stack-buffer-overflow
- 不合法操作WRITE发生在线程T0
WRITE of size 5 at 0x7ffed0cfce50 thread T0 - 具体发生的位置:/home/miaohq/tests/main.c:7
- 后面还有影子内存一些指示,后续再开一篇聊下,主要我也不熟悉哈哈:)。
3.2 堆溢出
示例代码如下:
11 void func1(void)
12 {
13 char * p = (char*) malloc(sizeof(char)*4);
14 char chs[] ={"12345"};
15 memset(p,0x0,4);
16 if (p != NULL) {
17 memcpy(p,chs,5);
18 }
19 }
按照同样办法编译,测试如下:
==37125==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60200000eff0 at pc 0x400cbe bp 0x7ffc7be10c20 sp 0x7ffc7be10c18
WRITE of size 5 at 0x60200000eff0 thread T0
#0 0x400cbd in func1 /home/miaohq/tests/main.c:17
#1 0x400d3d in main /home/miaohq/tests/main.c:24
#2 0x7fe88958d444 in __libc_start_main (/lib64/libc.so.6+0x22444)
#3 0x400858 (/home/miaohq/tests/t1+0x400858)
0x60200000eff4 is located 0 bytes to the right of 4-byte region [0x60200000eff0,0x60200000eff4)
allocated by thread T0 here:
#0 0x7fe88998c0f2 in __interceptor_malloc ../../../../gcc-4.9.4/libsanitizer/asan/asan_malloc_linux.cc:96
#1 0x400b60 in func1 /home/miaohq/tests/main.c:13
#2 0x400d3d in main /home/miaohq/tests/main.c:24
#3 0x7fe88958d444 in __libc_start_main (/lib64/libc.so.6+0x22444)
说明:
- 错误类型:heap-buffer-overflow
- 错误原因:WRITE of size 5 at 0x60200000eff0 thread T0
- 发生位置: #0 0x400cbd in func1 /home/miaohq/tests/main.c:17
3.3 释放后使用
就是申请了一块内存区域,释放后没有设置为NULL,后续继续使用了。
代码示例:
21 void func2(void)
22 {
23 int * a = (int*)malloc(sizeof(int)*1);
24 if ( a != NULL ) {
25 *a = 1;
26 printf("a is:%d.",*a);
27 free(a);
28 *a = 2;
29 printf("error a is:%d.",*a);
30 }
31 }
同样的编译方法,报错如下:
==37556==ERROR: AddressSanitizer: heap-use-after-free on address 0x60200000eff0 at pc 0x400ed6 bp 0x7ffd2dafb6d0 sp 0x7ffd2dafb6c8
WRITE of size 4 at 0x60200000eff0 thread T0
#0 0x400ed5 in func2 /home/miaohq/tests/main.c:28
#1 0x400f0c in main /home/miaohq/tests/main.c:36
#2 0x7f5632169444 in __libc_start_main (/lib64/libc.so.6+0x22444)
#3 0x400948 (/home/miaohq/tests/t1+0x400948)
0x60200000eff0 is located 0 bytes inside of 4-byte region [0x60200000eff0,0x60200000eff4)
freed by thread T0 here:
#0 0x7f5632567ec1 in __interceptor_free ../../../../gcc-4.9.4/libsanitizer/asan/asan_malloc_linux.cc:79
#1 0x400e9e in func2 /home/miaohq/tests/main.c:27
#2 0x400f0c in main /home/miaohq/tests/main.c:36
#3 0x7f5632169444 in __libc_start_main (/lib64/libc.so.6+0x22444)
previously allocated by thread T0 here:
#0 0x7f56325680f2 in __interceptor_malloc ../../../../gcc-4.9.4/libsanitizer/asan/asan_malloc_linux.cc:96
#1 0x400e2b in func2 /home/miaohq/tests/main.c:23
#2 0x400f0c in main /home/miaohq/tests/main.c:36
#3 0x7f5632169444 in __libc_start_main (/lib64/libc.so.6+0x22444)
SUMMARY: AddressSanitizer: heap-use-after-free /home/miaohq/tests/main.c:28 func2
说明:
- 错误类型:heap-use-after-free
- 错误原因:WRITE of size 4 at 0x60200000eff0 thread T0
- 发生位置: #0 0x400ed5 in func2 /home/miaohq/tests/main.c:28
这个挺好的,有明确的错误类型,指示位置也很准确。
3.4 全局缓存溢出
这些错误类型比valgrind分的更细致点,让我们来看看代码:
int g_abc[11];
36 int func3(void)
37 {
38 int i = 0;
39 for (i = 0; i <= 100; i++) {
40 printf("value:%d\t",g_abc[i]);
41 if (i%10 == 0 && i != 0) {
42 printf("\n");
43 }
44 }
45 return g_abc[12];
46 }
很不幸,这个翻车了,就算我把循环改成了100也是可以正常处理的,另外在gcc下没有告警,在clang中倒是在编译的时候就给出了告警信息:
clang: warning: argument unused during compilation: '-u se-after-free'
main.c:45:11: warning: array index 12 is past the end of the array (which contains 11 elements) [-Warray-bounds]
return g_abc[12];
^ ~~
main.c:5:1: note: array 'g_abc' declared here
int g_abc[11];
编译器改成g++,编译命令如下:
g++ -fsanitize=address -fno-omit-frame-pointer -fsanitize=leak -use-after-free -g main.c -o t1
再次执行的时候报错:
[root@localhost tests]# ./t1
value:0 value:0 value:0 value:0 value:0 value:0 value:0 value:0 value:0 value:0 value:0
=================================================================
==25944==ERROR: AddressSanitizer: global-buffer-overflow on address 0x00000060224c at pc 0x401079 bp 0x7ffce4e9ffe0 sp 0x7ffce4e9ffd8
READ of size 4 at 0x00000060224c thread T0
#0 0x401078 in func3() /home/miaohq/tests/main.c:40
#1 0x401299 in main /home/miaohq/tests/main.c:60
#2 0x7f3727c92444 in __libc_start_main (/lib64/libc.so.6+0x22444)
#3 0x400a68 (/home/miaohq/tests/t1+0x400a68)
0x00000060224c is located 0 bytes to the right of global variable 'g_abc' from 'main.c' (0x602220) of size 44
SUMMARY: AddressSanitizer: global-buffer-overflow /home/miaohq/tests/main.c:40 func3()
说明:
- 错误类型:global-buffer-overflow
- 错误原因: READ of size 4 at 0x00000060224c thread T0 即发生了越界读
- 错误位置:#0 0x401078 in func3() /home/miaohq/tests/main.c:40
3.5 内存泄漏
内存泄漏代码:
47 int func4(void)
48 {
49 char * p = (char*) malloc(5);
50 memset(p,0x0,5);
51 memcpy(p,"1234",4),
52 printf("%s\n",p);
53 }
在我gcc的4.9.4版本情况下,如果按照上述编译和运行后,并没有看到任何内存泄漏的提示,
在clang的编译器下,可以通过:
ASAN_OPTIONS=detect_leaks=1 ./t1
运行程序来显示内存泄漏,但是我的gcc的版本还是低了,还是无法显示内存泄漏,索性直接升级到最新版本,升级过程太慢,这个无法忍受,我还是先用clang编译器测试下。
yum install clang -y
clang -fsanitize=address -fno-omit-frame-pointer -fsanitize=leak -use-after-free -g main.c -o t1
ASAN_OPTIONS=detect_leaks=1 ./t1
显示信息如下:
root@localhost tests]# ASAN_OPTIONS=detect_leaks=1 ./t1
1234
=================================================================
==108674==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 5 byte(s) in 1 object(s) allocated from:
#0 0x465229 in __interceptor_malloc (/home/miaohq/tests/t1+0x465229)
#1 0x47c59e in func4 /home/miaohq/tests/main.c:49
#2 0x47cabb in main /home/miaohq/tests/main.c:58
#3 0x7efd6b207444 in __libc_start_main (/lib64/libc.so.6+0x22444)
SUMMARY: AddressSanitizer: 5 byte(s) leaked in 1 allocation(s).
说明:
- 显示错误原因为:detected memory leaks
- 被泄漏的内存:Direct leak of 5 byte(s) in 1 object(s) allocated from
- 泄漏的具体位置: 0x47c59e in func4 /home/miaohq/tests/main.c:49
- 总结信息:AddressSanitizer: 5 byte(s) leaked in 1 allocation(s).
提示: 有时候我们编译程序通过-g 或-ggdb调试仍然有问题,只显示地址,这里面可以试试这个办法:
[root@localhost tests]# addr2line -a -C -e ./t1 0x47c46b
0x000000000047c46b
/home/miaohq/tests/main.c:51
五 升级gcc到最新版本
按照上面的方法升级到9.3.0,运行缺少库:
安装:
./bin/suricata: error while loading shared libraries: libasan.so.5: cannot open shared object file: No such file or directory
## 解决办法
yum install centos-release-scl-rh
yum --enablerepo=centos-sclo-rh-testing install libasan5
五 参考
- 利用GCC编译选项快速定位内存错误
- Android native分析工具ASAN和HWASAN原理解析
- Address Sanitizer 用法
- AFL Fuzzing with ASAN
- KASAN实现原理
-
Linux 下的 AddressSanitizer
https://blog.csdn.net/hanlizhong85/article/details/78076668?utm_source=blogxgwz6 - https://blog.csdn.net/tq08g2z/article/details/90347700
- https://clang.llvm.org/docs/AddressSanitizer.html
- https://blog.csdn.net/wads23456/article/details/105141997?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase
网友评论