C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...) https://blog.csdn.net/chenlycly/article/details/125529931 由于C/C++不是一门内存与线程安全的语言,所以用C/C++开发的程序经常会遇到这样那样的内存与多线程方面的异常问题,特别是内存方面的异常会频繁地出现。在项目开发的过程中,bug的定位与解决速度可能会直接影响项目的整个进度,因此开发者们需要了解一些常用的调试技巧,掌握一些常用调试工具和内存检测工具的使用,以有效地应对出现的问题。今天我根据多年的项目开发实践来详细讲述一下Windows和Linux下排查C++软件异常的常用调试器和内存分析工具。
1、引言
C++软件在运行过程中可能会发生**内存越界、内存访问违例、Stack overflow线程栈溢出、空指针与野指针、死循环、死锁、内存泄露、GDI对象泄露**等异常问题,借助一些分析及调试工具,有的问题能快速的定位,有的问题则可能会比较难查,难查的问题会消耗大量的时间和精力,会给软件研发及发布工作的顺利推进带来不利的影响。
之前已经系统地介绍了**引发C++软件异常的常见原因以及排查C++软件异常常见方法与常用分析工具**,具体可以参见下面的文章:
C++软件异常的常见原因分析与总结(实战经验总结)https://blog.csdn.net/chenlycly/article/details/124996473排查C++软件异常的常见思路与方法(实战经验总结)https://blog.csdn.net/chenlycly/article/details/120629327分析C++软件异常需要掌握的汇编知识详细总结https://blog.csdn.net/chenlycly/article/details/124758670 掌握这些内容后,基本上可以解决大部分异常或崩溃问题,但还有少数问题是很难定位的,特别是隐藏很深的、复杂的内存异常问题。特别是内存问题,是C/C++中的重点难点问题,会贯穿整个软件产品的开发维护周期,几乎伴随着C++软件开发人员的职业生涯,也是C++程序员进阶的必经之路。
下面我们就来详细介绍一下Windows和Linux下的排查C++软件异常的常用调试器和内存检测分析工具。
2、概述
当我们在Debug下运行程序遇到问题时,我们会优先使用IDE去调试代码,使用多种调试手段或技巧去定位问题。当产品进入测试后或者发布到客户手中,在运行过程中产生异常,一般会事后取来异常捕获模块生成的dump文件,**Windows端的软件主要使用Windbg调试器进行分析,Linux下主要使用gdb进行分析。**
如果没有生成dump文件或者通过dump文件很难定位问题,则可能需要**将windbg或gdb调试器附加到目标进程上进行调试,尝试去捕捉一些有用的线索**。对于一些掩藏很深的内存越界或者堆内存被破坏的复杂内存问题,即使将调试器附加到目标进程上调试,可能也很难找到线索,调试器可能也无法第一时间感知到发生问题的点,只能在发生异常崩溃时才能感知到。
这个时候我们可能就需要使用一些专用的内存检测分析工具了,比如Linux下常用的Valgrind工具、AddressSanitizer工具。对于Windows系统,很多老式的内存工具已经停止维护了(比如BoundsCheck),已经不再支持一些版本较新的Visual Studio了,比如VS2017、VS2019,所以Windows系统中基本没有专用的内存分析工具可用了,一般都是用Windbg去分析的,比如Windows系统中遇到内存泄漏,可以使用windbg去定位,但遇到堆内存被破坏的问题,因为缺少专用的内存分析工具,就很难去定位了。
不过最近在搜索问题时发现,**Visual Studio 2019已经将Google出品的内存分析利器AddressSanitizer集成进来了**,在安装时勾选安装AddressSanitizer的选项即可。
AddressSanitizer是Google提供的一款面向C/C++语言的强大内存错误检测工具,该工具原先只支持Linux,现在也可以在Windows上使用了。
3、Windows下常用调试器和分析工具
3.1、Visual Studio开发调试工具
Windows下主要Visual Studio IDE开发环境去开发调试C++程序,Visual Studio则有多个版本,比如古老的Visual Studio 6.0、Visual Studio 2008、Visual Studio 2010、Visual Studio 2017、Visual Studio 2019、Visual Studio 2022等。
大家根据自己的项目需要,选择不同版本的Visual Studio。日常的C++开发调试任务基本都是在Debug下进行的,Debug下遇到的问题,都是在Debug下使用Visual Studio进行调试定位的。
3.2、Windbg调试器
对于relase版本软件运行过程中出现的异常,一般依赖软件中异常捕获模块在发生异常时自动生成包含异常上下文信息的dump文件,事后将dump文件取来后使用windbg进行静态分析。
**使用windbg查看崩溃的那条汇编指令,查看崩溃时的函数调用堆栈,有时还需要使用IDA工具查看二进制文件的汇编代码上下文。**有时,如果windbg查不出问题,且问题是比较好复现的,一般可以尝试使用Visual Studio进行release下调试,一般使用附加到进程调试单个模块的方式去处理。对于个别抓不到dump文件的场景,或者生成dump文件时由于内存上的错误产生了二次崩溃,导致dump文件生成失败,亦或是从dump文件分析不出结果时,可以考虑将windbg附加到目标进程中进行动态调试,看看能不能找到线索。Windbg动态调试非常有用,已多次使用该手段去解决问题了。
以前就遇到一个webrtc开源库导致程序闪退的问题,程序本身并没有产生崩溃,当时是调用malloc去动态申请内存时申请失败了,malloc返回NULL,webrtc内部在发现malloc失败时认为是fatal致命的错误,然后就会调用C函数abort强制将进程终止掉。这是webrtc库内部主动终止了进程,并没有发生标准意义上的异常,所这种情况肯定是不会有dump文件生成的。webrtc库在调用abort之前会调用DebugBreak,如果将windbg附加到目标进程上调试运行,执行到DebugBreak时,该函数就会让调试器windbg中断下来,此时windbg就感知到了,此时查看函数调用堆栈就能找到线索了:
webrtc开源库中调用DebugBreak和abort接口的代码如下:
很多开源库中都有类似的处理,比如libwebsockets、jsoncpp和XMPP开源库中如果发现数据有异常,都会调用abort强制将进程终止的。
**对于静态分析dump文件**,直接将dump文件拖到Windbg中,然后就可以输入Windbg的常用命令去分析了。**对于将Windbg附加到进程上进行动态调试**,打开Windbg后,点击菜单栏中的File->Attach to a Process...,在打开的窗口中找到目标进程,点击确定按钮即进入附加调试状态。**关于windbg静态分析dump文件和动态调试目标程序的完整过程,可以参见之前写的两篇文章**:
使用Windbg静态分析dump文件的完整过程介绍https://blog.csdn.net/chenlycly/article/details/125564806通过Windbg动态调试去捕获C++软件异常的完整过程介绍https://blog.csdn.net/chenlycly/article/details/125455100
3.3、Windows内存分析工具
对于Windows系统,很多老式的内存工具已经停止维护了(比如BoundsCheck),已经不再支持一些版本较新的Visual Studio了,比如VS2017、VS2019,所以Windows系统中基本没有专用的内存分析工具可用了。Windows系统中遇到内存泄漏,可以使用windbg去定位,但是遇到堆内存被破坏的问题,因为缺少专用的内存分析工具,就很难去定位了。
3.3.1、VS自带的运行时检测(/RTC编译选项)
VS自带的RTC运行时异常检测,对应着/RTC编译选项,可以在VS工程属性中进行设置,如下所示:
Debug下默认是开启/RTCsu检测的,Release下默认是关闭运行时检测的。因为其中的一些检测会额外地申请一些多余的内存,去存放一些桩信息或者调试信息,以完成运行时内存检测,所以开启运行时检测会消耗一定的资源,所以Release下是默认关闭的。有的工程为了方便查问题,Release下也开启了运行时检测。
/RTC编译选项主要对应三大类:
1)/RTCc(cast)
** **检查向较小的数据类型赋值导致的数据丢失问题。 在数据赋值时,如果把一个较大的数据类型赋值给一个较小的数据类型,很有可能发生内存中的数据被截断的问题,这个选项就是用来在发生数据截断时提示程序员的。例如,它报告 short 的 0x0101 类型值是否分配给类型 char 的变量。
2)/RTCs(stack)
检查与堆栈有关的错误,主要有以下几类:
**i)将局部变量初始化为一个非零值 **
0xcccccccc : Used by Microsoft's C++ debugging runtime library to mark uninitialised stack memory
VC的DEBUG版会把未初始化的指针自动初始化为0xcccccccc或0xcdcdcdcd,而不是就让取随机值,那是为了方便我们调试程序,如果野指针的初值不确定,那么每次调试同一个程序就可能出现不一样的结果,比如这次程序崩掉,下次却能正常运行,这样显然对我们解bug是非常不利的,所以自动初始化的目的是为了让我们一眼就能确定我们使用了未初始化的野指针了。
ii)检测局部变量(如数组)的溢出和不足
比如可以检查到数组的越界。大致的检测原理是,在数组结尾处多申请若干字节内存,在该段内存中填充指定的固定值(桩信息),在对数组进行写操作以后,再去检测这些桩信息是否有改变,如果对数据有越界操作,就会改变这些桩信息,这样就检测出数据越界了。
iii)检测堆栈指针是否发生损坏
一般是在调用函数结束后检测ESP栈顶寄存器中的值是否有异常,一般是函数调用约定不一致导致的。
3)/RTCu(uninitialized)
检查变量是否已经初始化。
但依赖/RTC运行时检测去排查软件中存在的潜在问题是远远不够的,很多异常问题都无法检测出来,特别是最容易出错的内存问题。关于VS的/RTC运行时检测也可以参看微软官方的说明:
AddressSanitizer是Google提供的一款面向C/C++语言的强大内存错误检测工具,该工具原先只支持Linux,现在也可以在Windows上使用了。微软在Visual Studio 2019的16.9版本们引入了强大的内存分析工具AddressSanitizer,在安装Visual Studio 2019的16.9版本及以后的版本时,会默认安装AddressSanitizer工具的:
对于如何在VS中如何使用AddressSanitizer内存分析工具,可以看一下微软官方的文章:
在Visual Studio中集成AddressSanitizerhttps://docs.microsoft.com/zh-cn/cpp/sanitizers/asan?view=msvc-170我们项目组主要使用使用VS2010和VS2017,还没用到VS2019,所以此处我们就不详细展开了,后面需要使用时再去详细研究一下。
至于AddressSanitizer工具都能检测哪些类型的内存异常,下面在介绍Linux中的AddressSanitizer时会详细讲到,此处就不展开了。
4、Linux下常用调试器和工具
4.1、gcc编译调试工具
一般情况下,跑在Linux下的代码都是在Windows系统中编写的,大家日常工作使用的机器用的基本都是Windows系统,我们一般选择Visual Studio、VSCode或Source Insight等IDE或者文本编辑工具去编写代码,代码编写好后需要拿到Linux系统上去编译。
可以在Windows上安装虚拟机,在虚拟机中安装Linux系统,也可以弄一台主机来安装Linux系统,用作代码编译机来使用。我们可以将Windows系统中项目源代码的根目录设置成共享文件夹,然后到Linux系统中使用mount命令将Windows共享文件夹挂载到Linux系统的某个磁盘上,这样在Linux系统中就可以直接访问Windows系统中的文件夹了,然后在Linux命令行中就可以使用gcc编译代码了。
实现磁盘挂载的mount指令如下所示:
mount –t cifs –o username=test, file_mode=0770 //172.16.38.6/projroot /mnt/wjb2
其中172.16.38.6是Windows系统机器的IP地址,projroot就是代码所在的根目录,将该目录设置为共享文件夹,然后挂载到Linux系统的/mnt/wjb2上,然后使用cd命令切换到makefile所在的路径中,执行make命令:
make –f makefile_x86_linux_r clean all
这样也可以使用gcc来编译调试代码了。
4.2、gdb调试器
Linux系统中的gdb调试器,类似于Windows系统中的Windbg调试器,主要用来分析静态转储文件以及附加到目标进程上进行动态调试的。
Linux中的程序发生崩溃时,Linux系统会自动生成包含异常上下文及进程内存信息的Core转储文件(在Windows系统中,这样的转储文件叫dump文件),事后可以使用gdb去分析生成的Core文件。
4.2.1、Core文件大小及名称的设置
一般我们在Linux运行程序之前,需要执行如下的脚本,一是指定系统自动生成的Core文件大小,二是指定生成的Core文件的路径及文件名:
ulimit -c unlimited
echo "/data/core.%e.%p.%t" > /proc/sys/kernel/core_pattern
** 第一句命令是将Core文件的大小设置为不限制大小。**
使用ulimit -c filesize命令,可以限制core文件的大小(filesize的单位为kbyte)。若ulimit c unlimited,则表示core文件的大小不受限制。如果生成的信息超过此大小,将会被裁剪,最终生成一个不完整的core文件。在调试此 core文件的时候,gdb会提示错误。
** 第二句是使用echo命令直接修改/proc/sys/kernel/core_pattern文件中的内容**,上述目录中的core_pattern文件就是**用来指定生成Core文件的路径以及Core文件的文件名**。
core文件默认的存储位置与对应的可执行程序在同一目录下,文件名是core,大家可以通过下面的命令到core_pattern文件中看到core文件的存在位置:
cat /proc/sys/kernel/core_pattern
文件中的缺省内容是core,内核在coredump时所产生的core文件放在与该程序相同的目录中,并且文件名固定为core。很显然,如果有多个程序产生core文件,或者同一个程序多次崩溃,就会重复覆盖同一个core文件,因此我们有必要对不同程序生成的core文件进行分别命名。我们通过修改core_pattern文件中的内容去指定内核所生成的coredump文件的文件名。
** **例如上面的命令:
echo "/data/core.%e.%p.%t" > /proc/sys/kernel/core_pattern
命令中的%e、%p、%t选项是用来配置core文件名称构成的,详细的参数说明如下:
%p:insert pid into filename 添加pid
%u:insert current uid into filename 添加当前uid
%g:insert current gid into filename 添加当前gid
%s:insert signal that caused the coredump into the filename 添加导致产生core的信号
%t:insert UNIX time that the coredump occurred into filename 添加core文件生成时的unix时间
%h:insert hostname where the coredump happened into filename 添加主机名
%e:insert coredumping executable name into filename 添加执行文件名(其实是线程名),名称太长会被截断
4.2.2、使用gdb打开core文件以及使用gdb去动态调试目标程序
可以使用如下的命令去打开Core文件分析:
gdb ./jsbpool -c /data/core.jsbpool.10882
其中是jsbpool是二进制文件,之所以要指定进程的文件名,是因为Core中涉及到的模块符号表及调试信息都是保存在对应的二进制文件中的,所以要指定对应的二进制文件。再者,/data/core.jsbpool.10882就是系统自动生成的Core文件。
如果要**使用gdb去动态调试**,需要先使用ps命令查看目标进程的进程id,比如:
ps aux|grep jsbpool
然后使用如下命令:
gdb -p 19053
就可以将gdb调试器附加到目标进程上调试了。
4.3、Valgrind内存分析工具
Valgrind是一套Linux下开放源代码(GPL V2)的仿真调试工具的集合,是运行在Linux 上的多用途代码分析和内存调试常用工具。Valgrind由内核(core)以及基于内核的其他调试工具组成。内核类似于一个框架(framework),它模拟了一个CPU环境,并提供服务给其他工具;而其 他工具则类似于插件 (plug-in),利用内核提供的服务完成各种特定的内存调试任务。
Valgrind支持的平台有:X86/Linux, AMD64/Linux, ARM/Linux, ARM64/Linux, PPC32/Linux, PPC64/Linux, PPC64LE/Linux, S390X/Linux, MIPS32/Linux, MIPS64/Linux, X86/Solaris, AMD64/Solaris, ARM/Android (2.3.x and later), ARM64/Android, X86/Android (4.0 and later), MIPS32/Android, X86/FreeBSD, AMD64/FreeBSD, X86/Darwin and AMD64/Darwin (Mac OS X 10.12).
Valgrind的体系结构如下图所示:
如上图,Valgrind提供了Memcheck、Callgrind、Cachegrind、Helgrind和Massif等调试分析工具,每个工具执行某些类型的调试和分析任务,以排查和分析软件中存在的问题。
(1)Memcheck工具
这是valgrind应用最广泛的一个工具,用来检测程序中多种内存问题,所有对内存的读写都会被监测到,一切对malloc、free、new、delete的调用都会被跟踪。该工具可以检测到如下的内存异常:
1)对未初始化内存的使用;
2)读/写已经释放了的内存块(引发内存访问违例);
3)读/写超出分配的内存块(内存越界);
4)读/写不适当的栈中内存块;
5)内存泄露;
6)malloc/free或者new/delete不匹配;
7)memcpy相关函数中的dst和src指针中内存地址重叠;
(2)Callgrind工具
它主要用来检查程序中函数调用过程中出现的问题。它对程序的运行观察细致入微,能给我们提供更多的信息。它不需要在编译源代码时附加特殊选项,但还是推荐加上调试选项。Callgrind收集程序运行时的一些数据,建立函数调用关系图,还可以有选择地进行cache模拟。在运行结束时,它会把分析数据写入一个文件。callgrind_annotate可以把这个文件的内容转化成可读的形式。
(3)Cachegrind工具
** **它主要用来检查程序中使用缓存时出现的问题。它模拟CPU中的一级缓存和二级缓存,能够精确地指出程序中cache的丢失和命中。如果需要,它还能够为我们提供cache丢失次数,内存引用次数,以及每行代码,每个函数,每个模块,整个程序产生的指令数,这对优化程序有很大的帮助。
(4)Helgrind工具
它主要用来检查多线程程序中出现的竞争问题。Helgrind寻找内存中被多个线程访问,而又没有一贯加锁的区域,这些区域往往是线程之间失去同步的地方,而且会导致难以发觉的错误。Helgrind实现了名为Eraser的竞争检测算法,并做了进一步改进,减少了报告错误的次数。
(5)Massif工具
它主要用来检查程序中使用堆栈时出现的问题。它能测量程序在堆栈中使用了多少内存,告诉我们堆块,堆管理块和栈的大小。Massif能帮助我们减少内存的使用,在带有虚拟内存的现代系统中,它还能够加速我们程序的运行,减少程序停留在交换区中的几率。
Massif对内存的分配和释放做profile。程序开发者通过它可以深入了解程序的内存使用行为,从而对内存使用进行优化。这个功能对C++尤其有用,因为C++有很多隐藏的内存分配和释放。
Valgrind是基于仿真的方式对程序进行调试,它先于应用程序获取实际处理器的控制权,并在实际处理器的基础上仿真一个虚拟处理器,并使应用程序运行于这个虚拟处理器之上,从而对应用程序的运行进行监视。应用程序并不知道该处理器是虚拟的还是实际的,已经编译成二进制代码的应用程序并不用重新进行编译,Valgrind 直接解释二进制代码使得应用程序基于它运行,从而能够检查内存操作时可能出现的错误。**所以,在Valgrind下运行的程序运行速度要慢的多,而且使用的内存比目标程序要多的多,这也是Valgrind的一大劣势,这也导致部分场合下没法使用Valgrind去分析。**
4.4、AddressSanitizer内存分析工具
4.4.1、AddressSanitizer简介
在排查Linux中C/C++软件内存异常方面,Valgrind被广泛的采用,但Valgrind的缺陷非常明显,使用Valgrind检测内存时程序的运行速度会明显的下载,也会占用更多的内存。
AddressSanitizer(ASan)是google提供的一款面向C/C++语言的内存错误问题检查工具。它相对于Valgrind要快很多,只拖慢程序两倍左右。它包括一个编译器instrumentation插桩模块和一个提供malloc/free替代项的运行时库。**从gcc 4.8开始,AddressSanitizer成为gcc的一部分。使用时非常方便,我们在编译时指定编译选项就可以了。**gcc 4.8自带的AddressSanitizer还不完善,有明显的缺陷(比如当监测到任何一个error,它就会强制退出主程序,导致程序无法继续运行,再比如没有符号信息),最好使用gcc 4.9及以上版本。
所以,在排查问题时,可能需要高版本的gcc,gcc版本越高,内置的AddressSanitizer版本bug越少,新版本的AddressSanitizer还增加了一些功能优化。比如大家可能日常使用的gcc版本就是4.8.5的,如果要使用AddressSanitizer工具来排查问题,则需要升级gcc版本。当然也可以临时搭建一个环境,环境中安装高版本的gcc,比如:
AddressSanitizer可以监测堆溢出(Heap buffer overflow)、栈溢出(Stack buffer overflow)、全局变量越界(Global buffer overflow)、已释放内存使用(Use after free )、初始化顺序(Initialization order bugs)、内存泄漏(Use after free )等内存问题。
具体如何检测如何使用,可以参见google对AddressSanitizer详细说明页面:
https://github.com/google/sanitizers/wiki/AddressSanitizer
4.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.4.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)设置为不可写即可。
(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)。
(2)插桩
为了防止buffer overflow,需要将原来分配的内存两边分配额外的内存Redzone,并将这两边的内存加锁,设为不能访问状态,这样可以有效的防止buffer overflow(但不能杜绝buffer overflow)。插桩的简化示意图如下:
以下是在栈中插桩的一个例子:
未插桩的代码:
void foo()
{
char a[8];
// ...
return;
}
插桩后的代码:
char redzone1[32]; // 32-byte aligned
char a[8]; // 32-byte aligned
char redzone2[24];
char redzone3[32]; // 32-byte aligned
int*shadow_base = MemToShadow(redzone1);
shadow_base[e] = oxffffffff;// poison redzone1
shadow_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.4.4、如何配置使用AddressSanitizer进行内存检测
**AddressSanitizer是内置在gcc中的,主要设置编译参数去设定是否启用AddressSanitizer的内存检测**。
**如果没使用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或者更高的优化级别编译
** 如果使用makefile,则在编译选项CFLAGS和链接选项LDFLAGS中都要添加-fsanitize=address选项**,如下:
#都要追加-fsanitize=address开关
CFLAGS+=-fsanitize=address
LDFLAGS+=-fsanitize=address
4.4.5、使用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 0x61400000fe44
at pc 0x0000004008f1 bp 0x7ffc9b6e2630 sp 0x7ffc9b6e2620
READ 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部分:前面输出的概要说明。
5、最后
上面就是本文要介绍的内容,还有很多细节需要进一步的展开和细化,部分细节点后面还会撰写专门的文章去阐述。希望上述内容能帮到大家,能给大家提供一定的借鉴或参考。
版权归原作者 dvlinker 所有, 如有侵权,请联系我们删除。