如果你发现你的程序栈写漏了(发现一个值突然发生了你预想不到的变化, 比如int a = 5,然后cout的时候就成了55)。
如果你发现你的程序每天占用内存都变大一点。
如果你发现你的程序core dump了。
如果你发现。。。
除了性能问题,都可以先用asan跑一下,说不定能发现什么。
缺点:
会让程序变得很慢,导致可能有些线程竞争的地方被刚好掩盖掉了。
Use after free (dangling pointer dereference)
Heap buffer overflow
Stack buffer overflow
Global buffer overflow
Use after return
Use after scope
Initialization order bugs
Memory leaks
注释:版本不同,可能功能有所不同。
1. centos 6或7
gcc 4.8.5 : 只有Asan,即只能检测内存越界。
gcc 4.9.2 : 有Asan和Lsan两种,可以用asan来做越界检测,用lsan做内存泄露检测。(建议使用, gcc的安装跟gcc4.8.5一样,详看tensorflow 配置centos6环境)
gcc 7.2 : Asan中集成了LSan。(建议使用, gcc的安装跟gcc4.8.5一样,详看tensorflow 配置centos6环境),意思就是只用asan就可以啦。
因为gcc 4.9.2版本最复杂,所以我们就按照4.9.2的来写,4.8.5的不会有内存检测,7.2的不用做lsan, 以下我们写个编译的例子,很简单。
g++ main.cpp -g -llsan -fsanitize=leak -o main
1. 需要注意的是需要带-g选项,会加入一些调试信息到符号表以供输出使用,不带也可以,如果不带,可能看不到错在哪一行;
2. 需要动态链接库liblsan.so,这个动态链接库会替换掉malloc等系统函数,在自己的malloc中加上统计信息,以达到检测内存泄露的作用。需要注意的点是,我们的环境中可能会有多个gcc或者找不到dso的时候。那么我们可以使用g++ --print-file-name=libasan.so来找到系统的动态链接库(只能是系统的库),这条命令会告诉你so在哪,但在我们的环境中我遇到了一个比较坑爹的问题。就是so是有,但是so的大小仅为4,里面写着让重新下载so。
3. -fsanitize=,这个有好几种选项、
1. asan(内存检测),如标题;
2. ubsan(未定义行为检测), 有的时候debug的程序没问题,release的程序会奇奇怪怪的core dump掉,那么你需要做这个检测;
3. tsan(线程安全检测),如标题;tsan有一个点需要注意,因为大家代码跑通后一般不会用tsan做检测,再者tsan出来时间不长,一些老的库会有非常多的线程安全问题。再加上检测条件非常严格。所以,大型项目第一用tsan做检查的时候,可能每一行都会有线程安全问题。
4. leak(泄露检测),被1包括了。
4. 如果我们有多个libasan.so,我们需要跟-L/path/to/lib,这个大家都懂,就不再叙述了。
5. 如果提示请加载PRELOAD,那么请export LD_PRELOAD=或者export LD_PRELOAD=/path/to/liblsan.so/libasan.so
我们今天主要讲asan,如果对tsan有兴趣可以看下官网,或者这篇文章,自己给自己打个广告,哈哈。
我们写如下代码:
#include
using namespace std;
int main()
{
int *p = new int(5);
std::cout << *p << std::endl;
return 0;
}
该代码只有new,但没有delete函数。当我们使用上述例子,编译:
./g++ main.cpp -g -llsan -fsanitize=leak
然后我们可能会得到编译错误的提示 lsan没有找到。可以使用print找到,如下:
./g++ --print-file-name=liblsan.so
加上-L即可。这时我们就编好了一个带内存泄漏检查的可执行文件,之后我们./a.out。
可能会出现提示:请配置LD_PRELOAD环境变量(7.2),或者直接core dump(4.9), 或者提示本该属于leak检查的内存映射段被其他dso占用了(4.9)。都需要配置正确的LD_PRELOAD环境变量。
我们用之前找到的liblsan,配置到LD_PRELOAD内,例如:
export LD_PRELOAD=/root/local/lib64/libasan.so
运行即可,如果还崩,那就大象zhaozheng09。
如上代码,我们可以得到结果:
=================================================================
==1712101==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 4 byte(s) in 1 object(s) allocated from:
#0 0x7fd00aabac28 in operator new(unsigned long) ../../../../libsanitizer/lsan/lsan_interceptors.cc:161
#1 0x4008a7 in main /root/local/bin/main.cpp:7
#2 0x7fd009ee0504 in __libc_start_main (/lib64/libc.so.6+0x22504)
SUMMARY: LeakSanitizer: 4 byte(s) leaked in 1 allocation(s).
第9行,是个总结,他会告诉你总共泄露了4bytes,这4bytes属于1次开辟的。
4-7行,我们这里只泄露了一次,如果多次的话会有多个4-7行,会告诉你那个线程开辟的空间,哪个线程释放的空间。并且打印出详细的开辟和释放的函数栈,当然我们注意第五行,我们用的malloc已经被替换成lsan下的malloc了。
第2行,错误类型。
首先注明一点,不一定只有越界导致core dump了才会打错误报告,只要访问到了不合法的内存都会报错,比如申请了一个包涵四个int元素的数组,而我们访问/修改了这个数组的第五个元素。那么就会报错。
直接看代码吧,如下代码:
#include
#include
using namespace std;
int main()
{
char p[5] = "";
uint8_t tmp = 5;
p[-1] = 7;
cout << (void*)p << endl;
cout << (void*)&tmp << endl;
cout << (uint32_t)tmp << endl;
return 0;
}
编译代码:
g++ main.cpp -lasan -fsanitize=address -g
得到了a.out,然后我们去./a.out。如果我们裸跑,那么我们很奇怪的发现tmp被修改成了7。其实我就想描述一下,别的线程或者一不小心的越界都会导致各种奇奇怪怪的错误。这个时候,我们用上述命令编译之后。就会报告如下错误。
其实我觉得可能误报,因为这是一个在c++看来是允许的操作。但只是我们看起来用法不对而已。(过于偏激,不认同请忽略)
=================================================================
==1729613==ERROR: AddressSanitizer: stack-buffer-underflow on address 0x7ffd1db351ef at pc 0x400c24 bp 0x7ffd1db351b0 sp 0x7ffd1db351a8
WRITE of size 1 at 0x7ffd1db351ef thread T0
#0 0x400c23 in main /root/code/test/asan/main.cpp:10
#1 0x7fc9489b4504 in __libc_start_main (/lib64/libc.so.6+0x22504)
#2 0x400a98 (/root/code/test/asan/main+0x400a98)
Address 0x7ffd1db351ef is located in stack of thread T0 at offset 31 in frame
#0 0x400b75 in main /root/code/test/asan/main.cpp:7
This frame has 1 object(s):
[32, 37) 'p' <== Memory access at offset 31 underflows 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-underflow /root/code/test/asan/main.cpp:10 main
Shadow bytes around the buggy address:
0x100023b5e9e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100023b5e9f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100023b5ea00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100023b5ea10: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100023b5ea20: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x100023b5ea30: 00 00 00 00 00 00 00 00 00 00 f1 f1 f1[f1]05 f4
0x100023b5ea40: f4 f4 f3 f3 f3 f3 00 00 00 00 00 00 00 00 00 00
0x100023b5ea50: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100023b5ea60: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100023b5ea70: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100023b5ea80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
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 right 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
Contiguous container OOB:fc
ASan internal: fe
==1729613==ABORTING
我们来分析一下结果。
第二行报出了错误类型。stack-buffer-underflow,并报出了sp,pc,bp寄存器里存放的值。
第三行报出操作WRITE,写了多少字节,且操作的线程是T0
第4-6行是T0的函数栈。
第八行表示那个地址出错,这个地址是属于谁,谁开辟的。
剩下的行表示出错误细节以及出错虚拟内存的情况,下面有有F1, F2等注解,可以详细看说明,我们这里表示错在了那个位置,覆盖了那个不该覆盖的变量。
我们只需要看函数栈就可以。
下面的不用看了。上面的一般就够了,我也没用过,如果写错了,不用大象我。
asan设置了ASAN_OPTIONS环境变量,可以带更个性化的参数,比如可以选择是否出现内存泄露立刻停止,或者正常跑完后停止,或者不检测哪些文件的内存泄露,等等;但不知道我们的gcc4.8,4.9是不是支持属性。
详细内容请看上述官方文档。
一般除了使用tsan之外,不需要设置OPTIONS变量。
1. 尽量将gcc版本更新到最高。
2. 在bazel的编译选项中加入-g -llsan -fsanitize=leak或-g -lasan -fsanitize=address。
3. 如果找不到leak,那么请设置CC和CXX环境变量。编译即可。
4. 如果让配置LD_PRELOAD,那么export LD_PRELOAD=即可。或者指向相应的dso。
5.注意:在使用asan和lsan的时候,我发现Python本身会报一些内存泄露或者误报,我们都不需要管,只要在里面没有出现我们自己写的代码的内存泄露或者越界错误即可。
6. ubsan可以正常使用,但是tsan遇到了conflict shadow memory的问题,让加-FPIC,加入后遇到编译错误。估计是一些.cc不支持固定地址。