目录
1、C++程序中的内存问题
2、AddressSanitizer是什么?
3、AddressSanitizer内存检测原理简述
3.1、内存映射
3.2、插桩
4、为什么选择AddressSanitizer?
4.1、Valgrind介绍
4.2、AddressSanitizer在速度和内存方面为什么明显优于Valgrind
4.3、在很多实际项目中我们需要使用AddressSanitizer
5、无法使用Valgrind的具体项目实例
5.1、使用Valgrind检测导致CPU占满,无法进行检测
5.2、使用Valgrind检测导致程序运行过慢,无法进行检测
6、AddressSanitizer与其他内存工具的比较
7、如何使用AddressSanitizer?
7.1、升级gcc版本
7.2、如何配置使用AddressSanitizer进行内存检测
7.3、使用AddressSanitizer进行内存检测的实例
7.4、使用AddressSanitizer的注意事项
8、Windows平台高版本的Visual Studio也支持AddressSanitizer工具
VC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新…)https://blog.csdn.net/chenlycly/article/details/124272585C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新…)https://blog.csdn.net/chenlycly/article/details/125529931C++软件分析工具从入门到精通案例集锦(专栏文章正在更新中…)https://blog.csdn.net/chenlycly/article/details/131405795C/C++基础与进阶(专栏文章,持续更新中…)https://blog.csdn.net/chenlycly/category_11931267.html C++程序中大部分问题都是内存问题,有些可以快速定位,有些则很难排查,通过日志打印及代码走读很难定位,并且有些难缠的问题只在客户的环境中才会出现(在公司内部测试环境中无法复现),处理起来非常头疼,把人搞的精疲力竭后可能还查不出来。今天给大家介绍来自Google的强大C/C++内存检测工具AddressSanitizer,它可以很好地解决实际应用环境中很多无法快速定位的内存问题。
1、C++程序中的内存问题
在C++程序中,大部分程序运行异常都是内存问题引起的,内存问题也是最让C++程序员头疼的事情。常见的内存异常有空指针、野指针、线程栈溢出、内存越界(栈内存越界、堆内存越界和全局内存越界)、堆内存被破坏、内存泄漏、虚拟内存不足等,如下所示:
其中某些场景下的内存越界问题以及堆内存被破坏问题最为难查,特别是堆内存被破坏问题。一般堆内存被破坏会表现为,程序到处胡乱崩溃,一会崩在这里,一会崩在那里!因为堆内存被破坏,一会崩溃在new的地方,一会崩溃在delete的地方!
关于C++软件异常及内存错误的详细说明,我就不在此赘述了,感兴趣的,可以去看看我之前写的文章:
C++软件异常分析概述https://blog.csdn.net/chenlycly/article/details/123991269引发C++程序内存错误的常见原因分析与总结https://blog.csdn.net/chenlycly/article/details/128599525
2、AddressSanitizer是什么?
AddressSanitizer(简称ASan)是google提供的一款面向C/C++语言的内存错误问题检查工具,它可以检测出堆溢出(Heap buffer overflow)、栈溢出(Stack buffer overflow)、全局变量越界(Global buffer overflow)、已释放内存使用(Use after free )、初始化顺序(Initialization order bugs)、内存泄漏(Use after free )等多个内存问题。
AddressSanitizer项目地址:AddressSanitizer · google/sanitizers Wiki · GitHub
参考文档页面:AddressSanitizerAlgorithm · google/sanitizers Wiki · GitHub
AddressSanitizer相对于Valgrind要快很多,只拖慢程序两倍左右。它包括一个编译器instrumentation插桩模块和一个提供malloc/free替代项的运行时库。从gcc 4.8开始,AddressSanitizer成为gcc的一部分,使用时非常方便,只需要在编译时指定编译选项就可以了。gcc 4.8自带的AddressSanitizer还不完善,有明显的缺陷(比如当监测到任何一个error,它就会强制退出主程序,导致程序无法继续运行,再比如没有符号信息),最好使用gcc 4.9及以上版本。
AddressSanitizer原先只支持Linux,现在也可以在Windows上使用了。微软在Visual Studio 2019的16.9版本中引入了强大的内存分析工具AddressSanitizer。
3、AddressSanitizer内存检测原理简述
AddressSanitizer主要由两部分组成:一个是静态插桩(Instrumentation)模块,将内存访问判断的逻辑直接插入在了二进制中,保证了检测逻辑的执行速度;另一部分则是运行时库(Run-time library),提供部分功能的开启、报错函数和 malloc/free/memcpy 等函数的ASan检测版本。
instrument静态插桩模块,对栈上对象、全局对象、动态分配的对象分配redzone,以及针对这些内存做访问检测。
runtime 运行时库提供了一些运行时的复杂的功能(比如poison/unpoison shadow memory),替换 malloc/free/memcpy/memset等实现,提供报错函数,针对每一次内存读写,编译器都会插入判断逻辑,判断地址是否被投毒(poisoned)。
该算法的思路是,如果要防住Buffer Overflow漏洞,只需要在每块内存区域右端(或两端,能防overflow和underflow)加一块区域(RedZone),使RedZone的区域的影子内存(Shadow Memory)设置为不可写即可。
3.1、内存映射
AddressSanitizer保护的主要原理是对程序中的虚拟内存提供粗粒度的影子内存(每8个字节的内存对应一个字节的影子内存),为了减少overhead,采用了直接内存映射策略,所采用的具体策略如下:Shadow = (Mem >> 3) + offset。每8个字节的内存对应一个字节的影子内存:
影子内存中每个字节存取一个数字k,如果k=0,则表示该影子内存对应的8个字节的内存都能访问,如果0<k<7,表示前k个字节可以访问,如果k为负数,不同的数字表示不同的错误(e.g. Stack buffer overflow, Heap buffer overflow)。
3.2、插桩
为了防止buffer overflow,需要将原来分配的内存两边分配额外的内存Redzone,并将这两边的内存加锁,设为不能访问状态,这样可以有效的防止buffer overflow(但不能杜绝buffer overflow)。插桩的简化示意图如下:
以下是在栈中插桩的一个例子:
1)未插桩的代码:
void foo(){char a[8];// ...return;}
2)插桩后的代码:
char redzone1[32]; // 32-byte alignedchar a[8]; // 32-byte alignedchar redzone2[24];char redzone3[32]; // 32-byte alignedint*shadow_base = MemToShadow(redzone1);shadow_base[e] = oxffffffff;// poison redzone1shadow_base[1] = oxffffffe0;// poison redzone2,unpoison 'a'shadow_base[2] = oxffffffff;// poison redzone3// ...return;
在动态运行库中将malloc/free函数进行了替换。在malloc函数中额外的分配了Redzone区域的内存,将与Redzone区域对应的影子内存加锁,主要的内存区域对应的影子内存不加锁。free函数将所有分配的内存区域加锁,并放到了隔离区域的队列中(保证在一定的时间内不会再被malloc函数分配),可检测Use after free类的问题。
4、为什么选择AddressSanitizer?
Linux平台上常用的内存分析工具主要有Valgrind和AddressSanitizer,这两个工具在使用方式上有一定的区别。Valgrind不需要重新编译代码,可以直接附加到程序上对内存进行监测;AddressSanitizer则需要重新编译代码。所以,很多时候大家为了图方便,会优先使用Valgrind。
但Valgrind会占用大量内存并明显拖慢程序运行的速度,这使得在部分场景下无法正常使用Valgrind。而AddressSanitizer在运行速度和效率上要比Valgrind好很多,所以在Valgrind无法完成检测时可以选择AddressSanitizer。
4.1、Valgrind介绍
Valgrind是一套Linux下开放源代码(GPL V2)的仿真调试工具的集合,是运行在Linux 上的多用途代码分析和内存调试常用工具。Valgrind由内核(core)以及基于内核的其他调试工具组成。内核类似于一个框架(framework),它模拟了一个CPU环境,并提供服务给其他工具;而其他工具则类似于插件 (plug-in),利用内核提供的服务完成各种特定的内存调试任务。
Valgrind是基于仿真的方式对程序进行调试,它先于应用程序获取实际处理器的控制权,并在实际处理器的基础上仿真一个虚拟处理器,并使应用程序运行于这个虚拟处理器之上,从而对应用程序的运行进行监视。
应用程序并不知道该处理器是虚拟的还是实际的,已经编译成二进制代码的应用程序并不用重新进行编译,Valgrind 直接解释二进制代码使得应用程序基于它运行,从而能够检查内存操作时可能出现的错误。所以,在Valgrind下运行的程序运行速度要慢的多,而且使用的内存比目标程序要多的多,这也是Valgrind的一大劣势,这也导致部分场合下没法使用Valgrind去分析。
4.2、AddressSanitizer在速度和内存方面为什么明显优于Valgrind
Valgrind采用的是二进制完全映射的影子内存技术,会占用更多内存才能去有效地监测内存变化。并且开启Valgrind监测之后,会严重降速,比如使用memcheck工具去监测内存,基本上是10到30倍的降速,明显的降速会导致我们的软件在业务上出现不可用的情况。关于降速,Valgrind官网上有着详细的说明:
The main one is that programs run significantly more slowly under Valgrind. Depending on which tool you use, the slowdown factor can range from 5-100. Memcheck runs programs about 10-30x slower than normal.
而Google提供的内存检测工具AddressSanitizer在内存占用和运行速度方面有着卓越的表现,相比于Valgrind,AddressSanitizer的优势相当明显。AddressSanitizer采用了一种取巧的影子内存玩法,将虚拟地址空间的1/8分配给它的影子内存,并使用一个带有比例和偏移量的直接映射将一个应用程序地址转换为它相应的影子地址,确保了少量内存就能完成一个程序的监测。并且AddressSanitizer降速也比较少。AddressSanitizer在内存占用和降速方面,通过USENIX高等计算机系统协会某篇论文中的一段描述可以佐证:
We present AddressSanitizer, a new tool that combines performance and coverage. AddressSanitizer finds out-of-bounds accesses (for heap, stack, and global objects) and uses of freed heap memory at the relatively low cost of 73% slowdown,1.5x-4x memory overhead,making it a good choice for testing a wide range of C/C++ applications.
4.3、在很多实际项目中我们需要使用AddressSanitizer
Valgrind采用的是二进制完全映射的影子内存技术,会占用更多内存才能去有效地监测内存变化,还会明显地拖慢程序的运行速度,可能会导致程序在收到请求后不能及时的响应,没法模拟出真实运行时的场景,可能就不一定能复现问题,甚至还会因为运行速度过慢导致程序根本无法正常的运转。所以我们有时需要使用占用内存少、运行速度更快的AddressSanitizer。
5、无法使用Valgrind的具体项目实例
在实际项目中我们遇到过不少无法使用Valgrind的场景。如果没有内存检测工具,排查起来效率非常低,仅仅通过打印日志和走读代码很难定位问题。如果有内存检测工具,可能很快就能定位出来。所以后来转向使用AddressSanitizer,很多Valgrind无法工作的场景,AddressSanitizer都可以胜任。与Valgrind相比,AddressSanitizer的运行速度是真的快,同时内存错误的检测能力也非常强。
5.1、使用Valgrind检测导致CPU占满,无法进行检测
某客户现场我们的程序出现了内存异常问题,最先使用Valgrind进行检测,发现使用Valgrind检测时机器的CPU一直是100%,直接导致程序业务无法正常工作,由于业务无法运转,导致我们没有办法让程序跑到存在问题的流程,所以检测也就无法实施了。
5.2、使用Valgrind检测导致程序运行过慢,无法进行检测
某客户现场出现了多线程死锁,使用gdb附加到目标程序上调试运行,发现某个公用模块每次都会检测到“它管理的堆的魔数被破坏”,于是强制退出了。由于该公用模块中内存管理器使用的地方比较多,包括上层业务代码和底层库,通过走读代码去分析哪些地方分配了堆内存很难实施。于是我们使用Valgrind分析,但因为速度过慢程序没法运转起来,内存检测任务无法实施。
6、AddressSanitizer与其他内存工具的比较
AddressSanitizer与其他内存检测工具的比较如下所示:
Items \ Tools | AddressSanitizer | Valgrind/Memcheck | Dr. Memory | Mudflap | Guard Page | gperftools | ||||
technology | CTI | DBI | DBI | CTI | Library | Library | ||||
ARCH | x86,ARM,PPC,… | x86,ARM,PPC | x86 | all(” />Heap OOB | yes | yes | yes | yes | some | some |
Stack OOB | yes | no | no | some | no | no | ||||
Global OOB | yes | no | no | ? | no | no | ||||
UAF | yes | yes | yes | yes | yes | yes | ||||
UAR | yes(seeUseAfterReturn) | no | no | no | no | no | ||||
UMR | no (seeMemorySanitizer) | yes | yes | ? | no | no | ||||
Leaks | yes(seeLeakSanitizer) | yes | yes | ? | no | yes |
上表中的相关名词说明如下:
DBI: dynamic binary instrumentation
CTI: compile-time instrumentation
UMR: uninitialized memory reads
UAF: use-after-free (aka dangling pointer)
UAR: use-after-return
OOB: out-of-bounds
x86: includes 32- and 64-bit.
Guard Page: a family of memory error detectors (Electric fenceorDUMAon Linux, Page Heap on Windows, Guard Malloc in Mac)gperftools: various performance tools/error detectors bundled with TCMalloc.Heap checker(leak detector) is only available on Linux.Debug allocatorprovides both guard pages and canary values for more precise detection of OOB writes, so it’s better than guard page-only detectors.
7、如何使用AddressSanitizer?
从gcc 4.8开始,gcc才集成AddressSanitizer工具,所以要使用AddressSanitizer必须将gcc升级到4.8或以上版本。然后使用高版本gcc对代码进行重新编译,在编译时指定编译选项就可以了。
7.1、升级gcc版本
可以到ftp://gcc.gnu.org/pub/gcc上下载高版本的gcc,然后到执行源码树中的contrib/download_prerequisites文件,它会下载和设置GCC编译依赖的组件。然后在GCC源码树同级的目录建立一个编译目录,比如叫build_dir,然后在该编译目录中执行如下命令进行编译和安装:
../src_dir/configure
make
make install
7.2、如何配置使用AddressSanitizer进行内存检测
AddressSanitizer是内置在gcc中的,主要设置编译参数去设定是否启用AddressSanitizer的内存检测。
1)如果没使用makefile,直接gcc命令去编译,则在命令中添加-fsanitize=address选项,如下:
gcc -fsanitize=address -fno-omit-frame-pointer -O1 -g use-after-free.c -o use-after-free
其中:
1)用-fsanitize=address选项编译和链接你的程序。
2)用-fno-omit-frame-pointer编译,以得到更容易理解stack trace。
3)可选择-O1或者更高的优化级别编译
2)如果使用makefile,则在编译选项CFLAGS和链接选项LDFLAGS中都要添加-fsanitize=address选项,如下:
#都要追加-fsanitize=address开关
CFLAGS+=-fsanitize=address
LDFLAGS+=-fsanitize=address
7.3、使用AddressSanitizer进行内存检测的实例
比如下面的代码中,分配array数组并释放,然后返回它的一个元素,返回了一个已经释放了的内存地址:
int main (int argc, char** argv){int* array = new int[100];delete []array;return array[1];}
上述代码放置在use-after-free.c中,直接使用gcc编译该文件即可,命令如下:
gcc -fsanitize=address -fno-omit-frame-pointer -O1 -g use-after-free.c -o use-after-free
然后,运行use-after-fee,AddressSanitizer检测了错误,就会打印出下面的信息:
==3189==ERROR: AddressSanitizer: heap-use-after-free on address 0x61400000fe44at pc 0x0000004008f1 bp 0x7ffc9b6e2630 sp 0x7ffc9b6e2620READ of size 4 at 0x61400000fe44 thread T0#0 0x4008f0 in main /home/ron/dev/as/use_after_free.cpp:9#1 0x7f3763aa882f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f)#2 0x4007b8 in _start (/home/ron/dev/as/build/use_after_free+0x4007b8)0x61400000fe44 is located 4 bytes inside of 400-byte region [0x61400000fe40,0x61400000ffd0)freed by thread T0 here:#0 0x7f3763ef1caa in operator delete[](void*) (/usr/lib/x86_64-linux-gnu/libasan.so.2+0x99caa)#1 0x4008b5 in main /home/ron/dev/as/use_after_free.cpp:8#2 0x7f3763aa882f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f)previously allocated by thread T0 here:#0 0x7f3763ef16b2 in operator new[](unsigned long) (/usr/lib/x86_64-linux-gnu/libasan.so.2+0x996b2)#1 0x40089e in main /home/ron/dev/as/use_after_free.cpp:7#2 0x7f3763aa882f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f)SUMMARY: AddressSanitizer: heap-use-after-free /home/ron/dev/as/use_after_free.cpp:9 main
如上图,打印出的信息主要分三部分:
1)ERROR部分:指出错误类型是heap-use-after-free;
2)READ部分:指出线程名thread T0,操作为READ,发生的位置是use-after-free.c:9(行号)。
该heapk块之前已经在use-after-free.c:8(行号)被释放了;
该heap块是在use-fater-free.c:7(行号)分配的。
3)SUMMARY部分:前面输出的概要说明。
7.4、使用AddressSanitizer的注意事项
使用AddressSanitizer过程中可能会遇到一些问题,此处给大家讲几个注意事项。
1) 如果存在第三方内存管理器,可能需要取消对第三方管理器的依赖
如果存在第三方内存管理器(比如tcmalloc),需要去掉第三方内存管理器的编译选项或连接选项,因为内存管理器分配的内存自身会预留一些管理空间,越界不多只写到这部分空间时,AddressSanitizer越界检测是不会认为它是异常的(因为它们仍然是用户分配的范围之内的,第三方内存管理器,与应用程序对于ASan没有差异的)。同时内存管理器通常会延迟释放内存,这也会影响检测的及时性。此外,如果链接时提示ASan中的符号找不到时给程序显示添加对libasan库的连接(默认在/usr/local/lib目录,找不时使用find命令找下)。
2)内存不足问题
内存检测工具会增加程序的内存消耗,32位程序地址空间只有4G,用户态的通常只有3G,如果程序跑起来之后提示无法分配内存,可以通过设置如下两个选项缓解一下:
export ASAN_OPTIONS=quarantine_size_mb=256:start_deactivated=1。
其中:
quarantine_size_mb设置小点,牺牲使用已经释放了的内存问题的检测能力。
start_deactivated设置为1,启动时不会加载asan的全部功能,用于节省内存。
上面的选项只是缓解,根本的解决之道还是要开发64位版本的程序。
3)错误忽略
有些错误我们改动不了或直接认为绝对安全,也可以在函数上面添加属性,进行错误忽略。比如:
#if defined(__clang__) || defined (__GNUC__)# define ATTRIBUTE_NO_SANITIZE_ADDRESS __attribute__((no_sanitize_address))#else# define ATTRIBUTE_NO_SANITIZE_ADDRESS#endif...ATTRIBUTE_NO_SANITIZE_ADDRESSvoid ThisFunctionWillNotBeInstrumented() {...}
8、Windows平台高版本的Visual Studio也支持AddressSanitizer工具
AddressSanitizer工具原先只支持Linux,现在也可以在Windows上使用了。微软在Visual Studio 2019的16.9版本们引入了AddressSanitizer,在安装Visual Studio 2019的16.9版本及以后的版本时,会默认安装AddressSanitizer工具:(默认勾选“C++ AddressSanitizer”)
对于如何在VS中如何使用AddressSanitizer内存分析工具,可以看一下微软官方文章的详细说明:
在Visual Studio中使用AddressSanitizerhttps://docs.microsoft.com/zh-cn/cpp/sanitizers/asan?view=msvc-170此处我就不详细展开了,大家需要使用的话,可以去详细研究一下。