C++软件异常排查软件关闭时访问了0xfeeefeee内存地址导致内存访问违例的崩溃
Posted dvlinker
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++软件异常排查软件关闭时访问了0xfeeefeee内存地址导致内存访问违例的崩溃相关的知识,希望对你有一定的参考价值。
最近在使用duilib开源库实现图片查看工具软件ImageViewer,调试时发现,程序关闭时访问了0xfeeefeee内存地址,触发内存访问违例,导致了软件崩溃。本文分享一下这一问题的排查过程。
1、问题描述
点击Imageviewer工具软件主窗口右上角的关闭按钮,会触发Imageviewer程序的退出流程,紧接着Imageviewer进程就退出了。结果在调试时发现,点击主窗口的关闭按钮后居然产生了崩溃,且崩溃是必现的。
崩溃时的提示如下:
从图中可以看出,是访问了0xfeeefeee内存地址,产生了内存访问违例崩溃。内存地址0xfeeefeee位于内核态地址范围中,用户态的程序是禁止访问的,所以产生了内存访问违例的崩溃。
以前我们多次讲过0xcccccccc、0xcdcdcdcd和0xfeeefeee这几个常见的特殊值,如下所示:
即本例中0xfeeefeee含义是:Debug下的程序在调用HeapFree接口将堆内存释放后,系统会将释放的堆内存中的内容置为0xfeeefeee。所以当我们看到是访问0xfeeefeee内存地址触发的内存访问违例,我们的第一反应应该是程序可能访问了已经释放的内存了。
2、初步分析
崩溃时代码中断在CContainerUI::RemoveAll函数的delete static_cast<CControlUI*>(m_ite
ms[it]);这行代码上:
估计是容器中的这个控件对象已经在其他地方被delete释放了,此处又来delete,即同一块内存被delete两次,于是就产生崩溃了。
我们先查看当前CContainerUI容器对象的this指针中的内容(this指针中存放当前CContainerUI容器对象的首地址),看看当前容器的名称是啥,看看是在delete哪个容器中的子控件触发的崩溃:
看到容器的名称为imagepos,于是到窗口的xml中查看该容器中的元素,发现容器中只有一个子控件:
这是一个自定义的dui控件,对应的控件类为CImageOperateUI,即此控件在其他地方被delete了。
如果是当前父容器中包含了多个子控件,要搞清楚到底是哪个子控件出的问题,可以添加额外的用于条件过滤的变量及代码,因为问题是必现的,再次崩溃时直接查看控件的名称便知道了。
3、进一步分析
ImageViewer工具的主窗口类CImageViewerWnd是使用dui框架中deleteself自销毁机制的,点击关闭按钮,就会触发主窗口CImageViewerWnd的Destroy,然后会触发该窗口对象的deleteself自销毁,窗口类中的所有成员会被销毁,包括窗口中的所有控件都会被自动销毁掉。如果某个控件在其他地方被销毁了,此处走到窗口控件的自销毁的流程中,就会出现同一个对象被delete两次的情况了,就会产生崩溃了。
于是在自定义控件类CImageOperateUI的析构函数中添加断点:
当断点命中时,查看函数调用堆栈,看看到底是何处delete该自定义控件的:
发现是在主窗口类CImageViewerWnd的析构函数中手动delete了该自定义控件类CImageOperateUI对象,显然这就是问题所在了。
那为啥是主窗口类的析构先执行,窗口中所有控件的自销毁后执行呢?CImageViewerWnd是继承于CAppWindow类,在析构时肯定会先执行子类CImageViewerWnd的析构函数,然后执行父类CAppWindow的析构函数,进而执行到dui框架中的窗口控件的自销毁的(CAppWindow类的析构函数中会触发其CPaintManagerUI成员的析构,CPaintManagerUI的析构会触发xml根布局控件m_pRoot的delete,根据多态,即触发了所有控件的delete操作)。
4、解决办法
解决办法是,在CImageViewerWnd的析构函数中不再delete自定义控件类CImageOperateUI对象,该控件对象交由dui框架去清除:
在CContainerUI::RemoveAll接口中,delete static_cast<CControlUI*>( m_items[it] );这句代码是如何保证子类对象的析构函数被执行到的呢?以前我们经常讲的一句话是,如果对父类指针对象执行delete操作(父类指针变量中存放着子类的对象)时,必须将父类的析构函数声明为虚函数:
才能保证子类对象的析构函数才能被执行到。
这其实通过虚函数表实现的,即在虚函数表中,子类对象的析构函数会把父类的析构函数给覆盖掉的,而调用子类的析构函数后是如何保证父类的析构函数被调用到呢?其实,子类的析构函数会隐式的调用父类的析构函数的,这点需要查看汇编代码才能看到的。在VS调试下,直接右键单击源代码,在弹出的菜单中选择“转到反汇编”:
即可查看C++对应的汇编代码,本例中的相关汇编代码如下:
5、为啥自定义控件被第二次delete时会触发对0xfeeefeee内存地址的访问呢?
自定义控件第一次被delete之后,因为是debug程序,系统会将该段被释放的内存中的内容都置为0xfeeefeee。上面讲到了,父类将析构函数定义为虚函数,这样在对父类对象执行delete操作时,会通过虚函数表调用子类的析构函数的,即通过虚函数表的二次寻址找到虚函数去call的。我们继续去看本例中崩溃的那句C++源码对应的汇编代码:
上图中,[ebp-0ECh]中存放的就是要delete的CImageOperateUI对象地址,因为这个对象在其他地方被delete了,所以对象地址指向的内存中的内容都被设置为0xfeeefeee。先将CImageOperateUI对象地址给edx寄存器,然后对edx进行寻址取出内存中的值,是CImageOperateUI类对象的虚函数表的首地址(虚函数表指针中的内容,CImageOperateUI对象的地址,就是该类中虚函数表指针的地址),将虚函数表首地址给eax(再对eax中的值寻址,就是取出虚函数表中的函数代码段地址,就是去call虚函数了),因为CImageOperateUI对象的内存中的内容都被置为0xfeeefeee了,所以此时eax中的值就是0xfeeefeee,这样mov edx, dword ptr [eax]就访问了0xfeeefeee的内存地址,这个地址肯定是位于内核态中,用户态的程序是禁止访问的,所以就引发了崩溃。
以上是关于C++软件异常排查软件关闭时访问了0xfeeefeee内存地址导致内存访问违例的崩溃的主要内容,如果未能解决你的问题,请参考以下文章