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 = &num;
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工具会和其它工具组合冲突,建议在新增线程或者线程中可能出现数据竞争的情况下单独使用。

posted @ 2024-08-17 17:58  孔子?孟子?小柱子!  阅读(124)  评论(0编辑  收藏  举报