Sanitizers工具集分享
1.Sanitizers工具集概述
Sanitizers是谷歌发起的开源工具集,包括了AddressSanitizer,ThreadSanitizer,MemorySanitizer,LeakSanitizer和UndefinedBehaviorSanitizer等工具,这些都是查找C/C++程序中隐藏Bug的利器,其中:
- AddressSanitizer工具(简称ASan),用于运行时检测程序中的内存访问错误;
- ThreadSanitizer工具(简称TSan),用于检测多线程的数据竞争和死锁;
- MemorySanitizer工具(简称MSan),用于检测未初始化的内存错误;
- LeakSanitizer工具(简称LSan),用于检测内存泄漏,它是集成在AddressSanitizer中的一个相对独立的工具;
- UndefinedBehaviorSanitizer工具(简称UBsan),用于运行时检测程序中各种未定义的的行为。
Sanitizers项目本是LLVM项目的一部分,但GNU也将该系列工具加入到了自家的GCC编译器中。Sanitizers具备如下优势:
- 与Compiler集成,主流IDE都提供了不同程度的集成,因此使用起来相对简单;
- 目前主流OS的Compiler都能够不同程度上支持Sanitizers,其通用性更强,因此对于需要跨平台的项目而言,它具备很大的优势;
- 相比同类工具,可用性更强,成本更低;
- 能力更强,能够检测的错误类型较多,诊断信息非常准确,能够帮助快速定位问题。
2.Sanitizers工具集原理
2.1.ASan工具原理
ASan工具主要由两部分组成:编译器插桩模块(代码插桩)和运行时库(提供malloc()/free()的替代项)。整体来说,ASan采用Shadow Memory来记录应用程序的每一字节是否可以安全地访问,代码插桩模块会对code进行修改以在每次内存访问时检查shadow state,同时负责在栈和全局对象周围创建用于检测overflow和underflow的poisoned redzones,如图2-1-1所示,运行时库会替换掉malloc/free,在堆空间周围创建用于检测的poisoned redzones,延迟已被free的堆空间的重用(正常情况下为提高内存使用率,已经被free的内存是可以被重用的,但是对于ASan来说,因为要检查use-after-free错误,因此被free的空间会被放到一个队列中,这些来确保已被free的内存如果还会被使用的话能够被发现,当然该队列还是有一定的大小限制,按照FIFO的原则进行退出),同时运行时库还会负责报告错误。
图2-1-1 poisoned redzones区域与主内存区域排布示意图
在整体原理中,涉及到了如下关键技术:
Shadow Memory
为了实现内存错误检测,ASan工具会为应用数据额外分配一块对应的shadow memory来存储相关的元数据,方法是直接将实际地址进行缩放+偏移映射到一个shadow地址,从而将整个的应用程序地址空间映射到一个shadow地址空间。
由于malloc函数返回的地址通常都至少是8字节对齐的,因此,对于应用程序堆内存上任意已对齐的8字节序列来说,它只有9种状态:前k(0=<k<=8)个字节是可寻址的,剩余8-k个不是,这样状态就可以通过一字节的Shadow Memory进行表示。ASan会将虚拟地址空间的1/8作为Shadow Memory,通过对实际地址进行缩放+offset直接将它们转换到对应的Shaodw地址。假设应用程序内存地址为Addr,那么其对应的Shadow地址为(Addr>>3)+Offset(对于1/2/4字节访问方式,原理类似)。
每个Shadow Byte采用如下编码:0表示对应应用程序内存区域的所有8个字节都是可访问的,k表示前k个字节是可以访问的,负值表示所有8个字节都是不可访问的。同时采用不同的负值来标示不同类型的不可访问地址,如下所示,工具运行结果中SUMMARY显示的就是这些影子内存状态。
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
运行时库
运行时库的主要目的是管理Shadow Memory。在应用程序启动时,整个Shadow空间将会被map,以保证程序其他部分无法使用它。malloc和free将会被特制的实现替换掉,malloc会在返回的缓存区域周围设置redzone,redzone将会被标记为有毒的(poisoned),但缓存本身标记为无毒的(unpoisoned),free将整个区域,包括缓存区和保护区都标记为poisoned,并将该区域放入一个特别的队列中,以保证malloc()在相当长的时间内不会再次使用它。内存访问的代码都被编译器进行如下替换:
替换之前:
1 *address = ...;
替换之后:
1 if (IsPoisoned(address)) 2 { 3 ReportError(address, kAccessSize, kIsWrite); 4 } 5 *address = ...;
对于被访问的内存来说,因为从程序地址空间到Shadow Memory有映射关系,所以ASan会计算出对应的Shadow Byte地址ShadowAddr,在对应的Shadow Memory中写入指定值,然后load该byte数据,检查其值是否为0,即在访问之前检查访问地址是否poisoned,如果是则报告错误,伪代码如下所示。这种做法为原始代码中的每一次内存访问(读&写)增加了一次内存读。错误报告代码最多只会被执行一次,但是会被插入到代码的很多地方,因此需要确保它的紧凑性。
1 shadow_address = MemToShadow (address); 2 if (ShadowIsPoisoned(shadow_address)) 3 { 4 ReportError (address, kAccessSize, kIsWrite); 5 }
通过在被保护的栈、全局变量、堆周围建立标记为poisnoned的redzones,以栈缓冲区溢出检测为例, 将缓冲区和redzone通过每8字节对应1字节的映射的方式建立影子内存区,影子内存区的获取函数为MemToShadow,如果出现对redzone的读、写或执行的访问,则ASan可以ShadowIsPoisoned检测出来并报错,报错信息给出出错的源文件名、行号、函数调用关系、影子内存状态。其中影子内存状态信息中出错的部分用中括号[]标识出来。
2.2.TSan工具原理
TSan工具是用来检查数据竞争的,当多个线程同时操作同一个变量的时候,而至少一个的操作是写操作时,就会发生数据竞争。
TSan采用的是动态检测技术,它不会扫描分析源代码,而是以程序运行中产生的一系列离散的事件点为输入,进行分析,从而找到竞争。TSan会记录每一个内存访问时的信息,原理类似于vector clock(向量时钟)的技术。vector clock是一种用向量来表示偏序关系的逻辑时钟,从数据结构上可以理解为一个集合内包含所有节点的“时间戳”,当然这个时间戳并不是物理意义上的时间,而是由程序赋予的逻辑计数,详细内容可参考https://www.jianshu.com/p/7f0cfa824df4。
TSan工具也会对每8个字节分配一个叫做Shadow state的东西,记录最多4个线程对这块内存的访问记录,同时每个线程存储一个结构,包括:
- 线程自己的时间戳;
- 其它线程的时间戳,用来构建同步点;
- 每次内存访问,会增加时间戳的值。
TSan工具分析过程如下:
1.线程1写
先获取一个锁,然后把自己的时间戳从2变为3,接着把内容写入内存中,把相关信息写入shadow中。 线程1在释放锁之前,会根据自己时间戳更新锁,这样锁就有了线程 1的时间戳信息。
2.线程2写
首先,获取锁,把自己线程的时间戳从22增加为23,然后根据锁中线程1的时间戳信息,更新自己线程中线程1的时间戳为3,接着访问内存,并把自己线程的时间戳信息写入shadow。
3.线程2校验
此时比较shadow中线程1的时间戳信息和线程2中线程1的时间戳信息,假设发现没有问题,校验成功,然后更新锁中关于线程2的信息,释放锁。
4.线程3访问
假设线程3访问时没有获取锁,而是直接写入内存,由于没有获取锁,所以线程3中关于其它线程的时间戳信息没有更新。在比较shadow中其他线程的时间戳信息时,发现 shadow中的时间戳比线程3中的时间戳大,此时可以认为发生了数据竞争。
2.3.LSan工具原理
LSan会接管内存申请接口,即用户的内存全都由LSan来管理,当进程退出时触发LSan内存泄漏检测,它首先暂停进程,然后扫描进程的内存。开始内存扫描后,LSan会遍历当前所有已经分配给用户但没有释放的堆内存,扫描这些内存是否被某个指针引用着,这些指针可能是全局变量、局部变量或者是堆内存里面的指针,如果没有则认为是泄漏了;如果内存被引用着,它依然没有被释放,LSan并不会将它标记为泄漏,因为OS会在进程退出时,统一回收进程占用的资源,即使程序员没有释放,大多数情况下也是没有问题的。
2.4.MSan工具原理
ASan无法覆盖到未初始化的内存,对于未初始化的内存,进行读取的行为同样危险,这时候就需要 MSan 工具的检测了。MSan实现的是一个bit to bit的影子内存shadow memory,每一比特都有映射,所以在计算影子内存的位置时,十分高效。
大多数编译器,是允许加载未初始化int或float值,来达到期望的效果的,而且在类或结构体中,对其中内存对齐的部分的加载,也是合乎逻辑的。所以MSan允许对未初始化内存的复制、以及一些安全的操作的,为了能达到这一目的,MSan实现了shadow propagation。对于未初始化内存的读取结果是undefined value(未定义值),它会被临时赋值给一个 shadow value(影子值)来管理,然后当application value(程序里的变量值) 被保存时,把这个shadow value存到对应的影子内存中。某一些对application value的操作,需要是已初始化的,包括有状态跳转、系统调用、解指针等,Msan工具就会对这些操作进行插桩。
除了插桩指令以外,MSan同样需要运行时库的支持,用来在启动时,将低地址内存设置为不可读,然后映射为影子内存。被malloc返回的内存、以及被dealloc的内存、局部的栈对象都会被标记为未初始化的。
2.5.UBSan工具原理
UBSan的工作原理主要包括两个部分:编译时和运行时。在编译阶段,具体来说是语法分析之后,UBSan会对源代码进行修改,对可疑的操作插入特定的检测代码,这些检测代码通常是在可能触发未定义行为的地方,如数组越界、除以零等。在程序运行阶段,UBSan的检测代码会检测各种未定义行为,如果检测到,则会输出报错信息。
3.Sanitizers工具集使用方法
在LLVM 3.1版本之后,Sanitizers就是其中的一个组成部分。对于GCC,从4.8版本开始支持ASan工具和TSan工具,从4.9版本开始支持LSan工具和UBSan工具,但是4.8版本的ASan不支持符号信息,无法显示出问题的函数和行数,从GCC 4.9.2开始支持ASan的所有功能,需要注意的是,GCC暂不支持MSan工具。
在使用Sanitizers的各种工具前,只需要在编译时添加编译选项-fsanitize就可以开启相应的工具,其中:
- -fsanitize=address 编译选项开启ASan工具(>GCC7.2的版本集成了LSan功能);
- -fsanitize=thread 编译选项开启TSan工具;
- -fsanitize=leak 编译选项专门开启LSan工具;
- -fsanitize=memory 编译选项开启MSan工具;
- -fsanitize=undefined 编译选项开启UBSan工具;
除此之外,若想在错误信息中让栈追溯信息更友好,可以加上-fno-omit-frame-pointer编译选项,同时若想时调试信息更加丰富,可以加上-g编译选项。
编译之后,正常运行程序,如果程序无问题,则直接运行结束,如果程序存在问题,则会打印异常信息。
4.基于Sanitizers工具集的代码分析示例
以下示例基于Linux(Ubuntu) x86-64平台,GCC 7.5.0版本和Clang 6.0.0版本编译器的环境进行说明。
4.1.基于ASan工具分析代码
ASan工具支持检测的内存错误种类主要包括:
- 缓冲区溢出,如:堆内存溢出、栈上内存溢出、全局区内存溢出;
- 悬空指针(引用),如:使用释放后的堆上内存、使用返回的栈上内存、使用退出作用域的变量;
- 非法释放,如:重复释放、无效释放;
- 内存泄漏(集成了LSan工具的功能)等;
与Valgrind工具相比,ASan工具在分析代码内存时具备更强大的能力,如下表4-1-1所示。
表4-1-1 Valgrind工具与ASan工具能力对比
Ability | Valgrind | ASan |
Heap out-of-bounds | YES | YES |
Stack out-of-bounds | NO | YES |
Global out-of-bounds | NO | YES |
Use-after-free | YES | YES |
Use-after-return | NO | Sometimes/YES |
Uninitialized reads | YES | NO |
Overhead | 10x – 30x | 1.5x – 3x |
下面针对几种C/C++常见的内存错误例子来说明ASan工具分析代码的方法。
- 堆内存溢出代码
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 5 int main() 6 { 7 char *heap_buf = (char*)malloc(32*sizeof(char)); 8 memcpy(heap_buf+30, "overflow", 8); //在heap_buf的第30个字节开始,拷贝8个字符 9 10 free(heap_buf); 11 12 return 0; 13 }
ASan工具检测结果如下,可以看出该工具检测出的错误类型是堆缓存溢出“heap-buffer-overflow”,不合法的操作是“WRITE”,溢出发生在代码的第9行,并且指出该堆缓存是在第8行申请。
=================================================================
==2657==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x603000000030 at pc 0x7f06979bf77a bp 0x7ffff3239c00 sp 0x7ffff32393a8
WRITE of size 8 at 0x603000000030 thread T0
#0 0x7f06979bf779 (/usr/lib/x86_64-linux-gnu/libASan.so.4+0x79779)
#1 0x55e2bfb489bb in main /data1/liuzhaozhu/nfs/CodeTest/temptest/sanitizers/ASan.cpp:9
#2 0x7f0697576b96 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21b96)
#3 0x55e2bfb488a9 in _start (/data1/liuzhaozhu/nfs/CodeTest/temptest/sanitizers/run+0x8a9)
0x603000000030 is located 0 bytes to the right of 32-byte region [0x603000000010,0x603000000030)
allocated by thread T0 here:
#0 0x7f0697a24b40 in __interceptor_malloc (/usr/lib/x86_64-linux-gnu/libASan.so.4+0xdeb40)
#1 0x55e2bfb4899b in main /data1/liuzhaozhu/nfs/CodeTest/temptest/sanitizers/ASan.cpp:8
#2 0x7f0697576b96 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21b96)
SUMMARY: AddressSanitizer: heap-buffer-overflow (/usr/lib/x86_64-linux-gnu/libASan.so.4+0x79779)
Shadow bytes around the buggy address:
0x0c067fff7fb0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c067fff7fc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c067fff7fd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c067fff7fe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c067fff7ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0c067fff8000: fa fa 00 00 00 00[fa]fa fa fa fa fa fa fa fa fa
0x0c067fff8010: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c067fff8020: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c067fff8030: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c067fff8040: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c067fff8050: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
==2657==ABORTING
- 使用退出作用域的变量
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 5 int main() 6 { 7 int *p; 8 { 9 int num = 10; 10 p = # 11 } 12 printf("%d/n", *p); 13 14 return 0; 15 }
ASan工具检测结果如下,可以看出该工具检测出的错误类型是使用退出作用域的变量“stack-use-after-scope”,不合法的操作是线程“T0”的“READ”,并且指出使用该变量的位置在代码的第13行。
=================================================================
==36461==ERROR: AddressSanitizer: stack-use-after-scope on address 0x7fff63eb5a70 at pc 0x5596a2c61be7 bp 0x7fff63eb5a30 sp 0x7fff63eb5a20
READ of size 4 at 0x7fff63eb5a70 thread T0
#0 0x5596a2c61be6 in main /data1/liuzhaozhu/nfs/CodeTest/temptest/sanitizers/ASan.cpp:13
#1 0x7f731ac24b96 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21b96)
#2 0x5596a2c619f9 in _start (/data1/liuzhaozhu/nfs/CodeTest/temptest/sanitizers/run+0x9f9)
Address 0x7fff63eb5a70 is located in stack of thread T0 at offset 32 in frame
#0 0x5596a2c61ae9 in main /data1/liuzhaozhu/nfs/CodeTest/temptest/sanitizers/ASan.cpp:6
This frame has 1 object(s):
[32, 36) 'num' <== Memory access at offset 32 is inside 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-use-after-scope /data1/liuzhaozhu/nfs/CodeTest/temptest/sanitizers/ASan.cpp:13 in main
Shadow bytes around the buggy address:
0x10006c7ceaf0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10006c7ceb00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10006c7ceb10: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10006c7ceb20: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10006c7ceb30: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x10006c7ceb40: 00 00 00 00 00 00 00 00 00 00 f1 f1 f1 f1[f8]f2
0x10006c7ceb50: f2 f2 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10006c7ceb60: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10006c7ceb70: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10006c7ceb80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10006c7ceb90: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
==36461==ABORTING
- 重复释放
1 #include <stdio.h> 2 #include <stdlib.h> 3 4 5 int main() 6 { 7 char *p = (char*)malloc(32*sizeof(char)); 8 free(p); 9 free(p); 10 11 return 0; 12 }
ASan工具检测结果如下,可以看出该工具检测出的错误类型是内存重复释放“attempting double-free”,重复释放的位置在代码的第9行,并且指出了第一次释放的位置在第8行,申请该内存的位置在第7行。
=================================================================
==44904==ERROR: AddressSanitizer: attempting double-free on 0x603000000010 in thread T0:
#0 0x7fd6556ef7a8 in __interceptor_free (/usr/lib/x86_64-linux-gnu/libASan.so.4+0xde7a8)
#1 0x55ef69f82857 in main /data1/liuzhaozhu/nfs/CodeTest/temptest/sanitizers/ASan.cpp:9
#2 0x7fd655241b96 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21b96)
#3 0x55ef69f82749 in _start (/data1/liuzhaozhu/nfs/CodeTest/temptest/sanitizers/run+0x749)
0x603000000010 is located 0 bytes inside of 32-byte region [0x603000000010,0x603000000030)
freed by thread T0 here:
#0 0x7fd6556ef7a8 in __interceptor_free (/usr/lib/x86_64-linux-gnu/libASan.so.4+0xde7a8)
#1 0x55ef69f8284b in main /data1/liuzhaozhu/nfs/CodeTest/temptest/sanitizers/ASan.cpp:8
#2 0x7fd655241b96 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21b96)
previously allocated by thread T0 here:
#0 0x7fd6556efb40 in __interceptor_malloc (/usr/lib/x86_64-linux-gnu/libASan.so.4+0xdeb40)
#1 0x55ef69f8283b in main /data1/liuzhaozhu/nfs/CodeTest/temptest/sanitizers/ASan.cpp:7
#2 0x7fd655241b96 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21b96)
SUMMARY: AddressSanitizer: double-free (/usr/lib/x86_64-linux-gnu/libASan.so.4+0xde7a8) in __interceptor_free
==44904==ABORTING
4.2.基于TSan工具分析代码
TSan工具的wiki网页上给出了该工具能够检测的几种错误,包括:
- Normal data races;
- Races on C++ object vptr;
- Use after free races;
- Races on mutexes;
- Races on file descriptors;
- Races on pthread_barrier_t;
- Destruction of a locked mutex;
- Leaked threads;
- Signal-unsafe malloc/free calls in signal handlers;
- Signal handler spoils errno;
- Potential deadlocks (lock order inversions);
以Normal data races检测为例:
1 #include <pthread.h> 2 3 int Global; 4 void *Thread1(void *x) 5 { 6 Global++; 7 return NULL; 8 } 9 void *Thread2(void *x) 10 { 11 Global--; 12 return NULL; 13 } 14 15 int main() 16 { 17 pthread_t t[2]; 18 pthread_create(&t[0], NULL, Thread1, NULL); 19 pthread_create(&t[1], NULL, Thread2, NULL); 20 pthread_join(t[0], NULL); 21 pthread_join(t[1], NULL); 22 }
TSan工具的检测结果如下,可以看出该工具给出了“data races”的警告,在线程T2读取了4个字节,位置是代码的第12行,这是检测到竞争的访问,之前在线程T1也读取了4个字节,于是在不加锁的条件下,两个线程同时去修改Global变量,产生了数据竞争。
=================================================================
WARNING: ThreadSanitizer: data races (pid=48315)
Read of size 4 at 0x55dc5de6701c by thread T2:
#0 Thread2(void*) /data1/liuzhaozhu/nfs/CodeTest/temptest/sanitizers/asan.cpp:12 (run+0xb42)
#1 <null> <null> (libtsan.so.0+0x296ad)
Previous write of size 4 at 0x55dc5de6701c by thread T1:
#0 Thread1(void*) /data1/liuzhaozhu/nfs/CodeTest/temptest/sanitizers/asan.cpp:7 (run+0xb03)
#1 <null> <null> (libtsan.so.0+0x296ad)
Location is global 'Global' of size 4 at 0x55dc5de6701c (run+0x00000020201c)
Thread T2 (tid=48318, running) created by main thread at:
#0 pthread_create <null> (libtsan.so.0+0x2bcee)
#1 main /data1/liuzhaozhu/nfs/CodeTest/temptest/sanitizers/asan.cpp:20 (run+0xbd3)
Thread T1 (tid=48317, finished) created by main thread at:
#0 pthread_create <null> (libtsan.so.0+0x2bcee)
#1 main /data1/liuzhaozhu/nfs/CodeTest/temptest/sanitizers/asan.cpp:19 (run+0xbb2)
SUMMARY: ThreadSanitizer: data races /data1/liuzhaozhu/nfs/CodeTest/temptest/sanitizers/asan.cpp:12 in Thread2(void*)
==================
ThreadSanitizer: reported 1 warnings
4.3.基于LSan工具分析代码
从GCC7.2版本开始,ASan中集成了LSan工具的功能,在启动程序时加上ASAN_OPTIONS=detect_leaks=1参数,就可以开启ASan的内存泄漏检测功能。本章节以专门用来检测内存泄漏的LSan工具来分析代码。
1 #include <malloc.h> 2 3 void* p; 4 5 int main () 6 { 7 p = malloc (7); 8 p = 0; 9 return 0; 10 }
TSan工具检测结果如下,可以看出该工具检测出了进程40031的内存泄漏“detected memory leaks”,泄漏的内存是在第8行申请的,泄漏的大小是7 byte(s),1 object(s)表示泄漏了1次。
=================================================================
==40031==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 7 byte(s) in 1 object(s) allocated from:
#0 0x7ff428a39acb in __interceptor_malloc (/usr/lib/x86_64-linux-gnu/liblsan.so.0+0xeacb)
#1 0x556acc607717 in main /data1/liuzhaozhu/nfs/CodeTest/temptest/sanitizers/asan.cpp:8
#2 0x7ff42865bb96 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21b96)
SUMMARY: LeakSanitizer: 7 byte(s) leaked in 1 allocation(s).
4.4.基于MSan工具分析代码
GCC编译器目前还不支持MSan工具,因此这里的代码分析示例采用的是LLVM中MSan工具,采用Clang编译。
1 #include <stdio.h> 2 3 int main(int argc, char** argv) 4 { 5 int* a = new int[10]; 6 a[5] = 0; 7 8 if(a[argc]) printf("memory sanitizer\n"); 9 10 return 0; 11 }
MSan工具检测结果如下,可以看出该工具检测出了一个“use-of-uninitialized-value”类型的错误,该错误是由于代码的第9行使用了一个未初始化的数据。
clang -fsanitize=memory -g -lstdc++ -fno-omit-frame-pointer -o run asan.cpp
=================================================================
==64829==WARNING: MemorySanitizer: use-of-uninitialized-value
#0 0x493ee0 in main /data1/liuzhaozhu/nfs/CodeTest/temptest/sanitizers/asan.cpp:9:8
#1 0x7f42563d6b96 in __libc_start_main /build/glibc-2ORdQG/glibc-2.27/csu/../csu/libc-start.c:310
#2 0x41a769 in _start (/data1/liuzhaozhu/nfs/CodeTest/temptest/sanitizers/run+0x41a769)
SUMMARY: MemorySanitizer: use-of-uninitialized-value /data1/liuzhaozhu/nfs/CodeTest/temptest/sanitizers/asan.cpp:9:8 in main Exiting
4.5.基于UBSan工具分析代码
UBSan用于检测代码的未定义行为,它在编译时修改程序,以捕捉程序执行过程中的各种未定义行为,包括:
- 数组下标越界;
- 超出其数据类型界限的逐位移位;
- 取消引用未对齐的指针或空指针;
- 有符号整数溢出;
- 除0操作;
- 转换到浮点类型、从浮点类型转换或在浮点类型之间转换会使目标溢出等;
以检测数据溢出的错误为例:
1 int main() 2 { 3 int k = 0x7fffffff; 4 k += 2; 5 6 return 0; 7 }
UBSan工具的检测结果如下,其检测出的错误类型是“signed integer overflow”,即k+2超出了int的范围,不能用int表达。
=================================================================
asan.cpp:6:4: runtime error: signed integer overflow: 2147483647 + 2 cannot be represented in type 'int'
5.Sanitizers工具集使用经验
1、使用时有时在堆栈信息中,无法看到对应的文件及精确行号,如下所示,此时很难定位问题,如何解决?
#0 0x5619fc068ba9 in get_pointer() /data1/liuzhaozhu/nfs/temptest/sanitizers/asan.cpp:11
#1 0x5619fc068c19 in main /data1/liuzhaozhu/nfs/temptest/sanitizers/asan.cpp:17
#2 0x7f5f8c1a1b96 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21b96)
经验:
如果只是未显示代码行数,直接使用addr2line <addr> -e <executable>命令转换即可,如:addr2line 0x21b96 -e /lib/x86_64-linux-gnu/libc.so.6
如果堆栈信息中还出现<unknow module>的情况,如内存泄漏问题出现在dlopen加载的动态库中,内存检测工具在程序退出时分析泄漏问题,而dlopen加载的库往往已经手动dlclose,此时可以通过查询/proc/pd/maps来辅助查询,将工具显示的地址减去maps中动态库相应的基地址,得到偏移量,此时再用addr2line工具进行转换即可找到代码的确切位置。
2、代码中存在Stack Use-After-Return的问题,但是使用ASan工具为什么检测不出来?
经验:
对于这一类型问题的检测,ASan工具默认没有开启,需要使用者在运行时手动开启,假设可执行程序是./out,则执行命令需要是:ASAN_OPTIONS=detect_stack_use_after_return=1 ./out
3、为什么在CentOS系统上正确添加编译选项后,还是无法使用ASan工具?
经验:
Ubuntu系统只需GCC版本高于4.8即可,但是在Rhel/CentOS上使用ASan工具,除了GCC版本大于4.8之外,还需要安装libasan,并链接库-static-libasan。
4、ASan工具的能力也存在限制,会发生漏报or误报的情况,常见的有以下几种。
经验:
a、产生局部越界的未对齐的内存访问,如:
1 int *a = new int[2]; // 8-aligned 2 int *u = (int*)((char*)a + 6); 3 *u = 1; 4 delete []a;
b、如果在free和下次use之间,又发生了大量内存的分配和释放,use-after-free错误可能无法被检测到,如下:
1 char *a = new char[1 << 20]; // 1MB 2 delete [] a; // free 3 4 char *b = new char[1 << 28]; // 256MB 5 delete [] b; // free 6 7 char *c = new char[1 << 20]; // 1MB 8 a[0] = 0; // use 9 10 delete [] c; // free
5、TSan工具在实际使用时可能无法检测死锁。
经验:
TSan应该是能支持检测死锁的,毕竟其官方表明支持这种检测能力,但是对于“两个线程持有同一把锁,一个线程在持有过程中析构(没有解锁),导致另一线程死锁”的情况和“两个线程各持有一把锁,即相互持有导致两线程都死锁”的情况,都无法检测出来。
6、Sanitizers工具集的内存泄漏检测功能受到平台的制约。
经验:
目前,并不是所有的平台都默认检测内存泄露,可以指定ASAN_OPTIONS开启,如下:ASAN_OPTIONS=detect_leaks=1 yourapp
而且不是所有的平台支持检测内存泄露,比如ARM,就会得到这样的提示:
==1901==AddressSanitizer: detect_leaks is not supported on this platform.
7、Sanitizers工具集的组合使用建议。
经验:
对可能出现问题的代码,可以使用ASAN、LSan、UBSan三种工具同时检查,建议在每次测试代码时,开启此三项检查,可以排除大部分常见错误。另外,由于TSan工具会和其它工具组合冲突,建议在新增线程或者线程中可能出现数据竞争的情况下单独使用。