【C++软件异常】排查软件关闭时访问了0xfeeefeee内存地址导致内存访问违例的崩溃

【C++软件异常】排查软件关闭时访问了0xfeeefeee内存地址导致内存访问违例的崩溃,第1张

        最近在使用duilib开源库实现图片查看工具软件ImageViewer,调试时发现,程序关闭时访问了0xfeeefeee内存地址,触发内存访问违例,导致了软件崩溃。本文分享一下这一问题的排查过程。

1、问题描述

       点击Imageviewer工具软件主窗口右上角的关闭按钮,会触发Imageviewer程序的退出流程,紧接着Imageviewer进程就退出了。结果在调试时发现,点击主窗口的关闭按钮后居然产生了崩溃,且崩溃是必现的。

       崩溃时的提示如下:

从图中可以看出,是访问了0xfeeefeee内存地址,产生了内存访问违例崩溃。内存地址0xfeeefeee位于内核态地址范围中,用户态的程序是禁止访问的,所以产生了内存访问违例的崩溃。

       以前我们多次讲过0xcccccccc、0xcdcdcdcd和0xfeeefeee这几个常见的特殊值,如下所示:

即本例中0xfeeefeee含义是:Debug下的程序在调用HeapFree接口将堆内存释放后,系统会将释放的堆内存中的内容置为0xfeeefeee。所以当我们看到是访问0xfeeefeee内存地址触发的内存访问违例,我们的第一反应应该是程序可能访问了已经释放的内存了。

2、初步分析

       崩溃时代码中断在CContainerUI::RemoveAll函数的delete static_cast(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( m_items[it] );这句代码是如何保证子类对象的析构函数被执行到的呢?以前我们经常讲的一句话是,如果对父类指针对象执行delete *** 作(父类指针变量中存放着子类的对象)时,必须将父类的析构函数声明为虚函数:

才能保证子类对象的析构函数才能被执行到。

       这其实通过虚函数表实现的,即在虚函数表中,子类对象的析构函数会把父类的析构函数给覆盖掉的,而调用子类的析构函数后是如何保证父类的析构函数被调用到呢?其实,子类的析构函数会隐式的调用父类的析构函数的,这点需要查看汇编代码才能看到的。在VS调试下,直接右键单击源代码,在d出的菜单中选择“转到反汇编”:

即可查看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的内存地址,这个地址肯定是位于内核态中,用户态的程序是禁止访问的,所以就引发了崩溃。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/1498766.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-06-25
下一篇 2022-06-25

发表评论

登录后才能评论

评论列表(0条)

保存