C++ delete指针后依然可以访问的问题
这两天在定位一个BUG:我们的程序运行过程中,点击停止运行,程序偶现无响应和崩溃,此时无查询操作(因为当并发查询的时候,有出现其他bug,特意确定没有查询)。最终发现是delete指针导致的问题,写篇文章备份记录下此次定位问题的经过,以及总结。
在此次分析问题的过程中,总共捕捉到了2个Dump文件(注意,环境现场和我们自己的机子都是64位环境,需要在C:\Windows\SysWOW64里找到taskmgr.exe,即32位的任务管理器,再进行转储操作),下载两个版本程序对应的集成包,取出pdb和map文件。
一,第一个dump分析
配置好Windbg环境,打开dump,首先查看EXCEPTION CODE为C000005,看到这就推测是地址问题,比如野指针访问,数组越界,无效地址等等。
然后再看具体的堆栈信息:
可以看到是在停止运行时,卸载各个组件时崩溃的,再往上看也看不出有效信息了,而Windbg也没有给出有可能会导致崩溃的地址信息建议。
下面是基于目前dump分析自己的两种推测。
推测一:
由于考虑到FreeLibrary时,如果组件中的线程没有终止会导致程序异常,经过检验代码,在FreeLibrary之前,都已经退出各个组件中的线程了,同时也可以通过~*kb命令,查看各个线程的堆栈如下:
篇幅有限,图就不截全了,总共有6个线程,一个主线程,另外五个都是t2sdk相关线程(公司研发中心提供的网络通信库)。所以,这种推测也被推翻。(题外话:我们的程序在加载组件时,有打印日志,但是卸载组件时,却没有打印日志,这一点对于我们查找卸载到哪一个组件时崩溃很有难处,后续需要加上这个日志。)
推测二:
由于此时程序表现出来的是“无响应”现象,推测是否是主程序中的UI线程(主线程)和其他线程死锁导致的?在Windbg中用~locks命令看出此时堆栈中的锁的情况如下:
扫描了500多个锁,此时程序中只有两个锁,第一个锁77d920c0是由1b2c线程所拥有的,通过上面的~*kb命令,可以知道1b2c也就是主线程,ntdll!LdrpLoaderLock中的锁,也是FreeLibrary中实现的锁(此锁用于保护在DllMain做保护,具体可以自己下去了解)第二个锁,是在hq_bjsxsb.dll组件中的hq_bjsxsb!SpecialDeal+46b31位置,执行命令u hq_bjsxsb!SpecialDeal+46b31,查看具体的地址如下所示:
通过之前讲过的计算偏移地址的方法,计算出偏移地址为479f0,但是在map文件中找不到任何地址信息。而且这两个锁的LockCount(表示还有多少个线程在等待这个临界区)都是0,也就是说当前没有线程在阻塞等待它们。此路不通。
从这一个dump文件中还没有定位到具体原因。
二,第二个DUMP分析
由于目前信息定位不出有问题的代码,那么只能尽可能复现了,运气比较好,我操作了几次,就在同样的环境下复现了,取出dump,分析如下:
从这里可以看到最上面的信息,已经定位到hq_bjsxsb.dll中了,但是具体的代码信息没有,我们往下看,WinDbg给我们提示了可能地异常地址,如下所示:
由于之前介绍过根据map文件来定位代码具体行号,此处不再重复介绍,我们根据偏移地址,得到代码行号为DealXSB.cpp中的第243行代码,是个析构函数入口:
程序为什么会突然崩溃在这里呢,查看g_pDbfSqliteSynImpl变量,发现在UnPrepare函数中,已经进行了Uninit和Release操作,其中,Release函数中进行了delete this操作,但是没有把它赋空,导致这里对已经delete的指针进行二次访问,会造成未定义现象,这也能解释地通为什么第一次出现的时候是程序无响应,而第二次程序直接崩溃。下面我们对deleet的指针仍能够继续访问这一现象继续分析。
三,VC6.0和VS2015下delete指针验证
想起了之前学习C++基础知识的时候,总是看到书上或者听到其他人说“指针delete后,一定要置空”,当时以为delete一个指针后,这一块区域就已经不存在了,那么再对这个指针进行任何操作,都会导致程序崩溃,可事实呢?为什么上述现象的程序可以运行这么长时期而没有出现崩溃问题呢,为什么上述现象不是必现的?让我们通过代码来验证。
这是在VC6.0编译器上编译出的程序,我们可以看到在delete t操作之后,我们仍旧可以访问它的成员函数,输出成员变量(输出一个随机值),并且对该成员变量进行赋值,能输出改变之后的成员变量值。更令人不可思议的在后面,我们再用vs2015编译器做同样的测试。
此时程序中断,但是在delete t之后,还是可以调用成员函数,只是当访问成员变量的时候,程序会中断(Debug和Rlease不一样)。
对这两个编译器,在delete t后对a进行变量监视,发现在VC6.0中,此时变量a的地址还是有效的,但是在vs2015中a的地址却是不可访问的。由此可见,不同编译器对于这种未定义的错误表现不一样,使用不同编译器运行的程序表现也不一样,但是很明显,vs2015的检错能力更强。
四,结论
delete指针时,编译器到底干了什么呢?在so上查询到同样的问题,有人解答过:
delete一个指针时,指针指向的内存区域并不会被清空,因为这样会占用CPU周期,此时它是一个危险的指针,会造成一些未定义的现象。像这样的代码有可能会工作很多年,只会在某个时候崩溃,因为程序中的其他地方发生了一些小的改变导致。这也很好地解释了为什么我们Delete一个指针后要将其置为NULL,是为了保证我们使用的是一个有效的指针,而不是会造成不可预知错误的野指针。
C ++不会阻止你写入内存中的任意位置。当我们使用new或分配内存时malloc,C ++会在内存中找到一些未使用的空间,将其标记为“已分配”(以便不会意外地再次分发),并为我们提供其地址。当使用delete操作指针, C ++会将其指向的内存区域标记为“未分配”,并可能将其交给任何要求它的人。我们仍然可以写入并从中读取,但此时,其他人可能正在使用它。当我们在内存中写入该位置时,可能会覆盖其他位置分配的某些值,特别是类指针,根据类的结构去分配内存大小时,改变内容,那么内存大小也就改变了,再去操作类对象或类成员就会出错。
因此,切记,delete指针后,一定要将其置空。