引发0xC0000005内存违例几种可能原因分析

目录

1、概述

2、空指针访问

3、已释放内存的访问

4、内存越界

5、总结


        在日常的软件异常排查中,我们经常会遇到0xC0000005内存访问违例的异常。对于简单的异常,windbg分析dump文件,结合源代码,能很快找出原因。但是对于复杂的异常,则很难通过dump文件中的信息直接定位问题,这种情况下,可能就需要我们根据当前的业务流程,按照代码执行的方向,自下而上的去逐一排查代码了。本文就平时遇到的一些软件异常崩溃实例,结合日常的排查经验,简单的总结一下出现0xC0000005内存违例可能是哪些原因造成的。

1、概述

       引起0xC0000005内存违例的原因很多,本文主要讲述空指针访问、已释放内存的访问、内存越界三个方面的原因。通过对这些原因进行分析,给大家提供一定的思路和参考。

2、空指针访问

       0xC0000005内存违例很多时候都是访问空指针引起的。指针变量之前指向的是一个类的对象,后面该类对象释放了,指针变量被置为NULL了,结果仍然拿这个指针变量访问之前的类对象的数据成员或者虚成员函数,把指针变量中存放的地址作为类对象地来访问类对象的数据成员或者虚成员函数。

       如果指针变量的值被置为NULL,那么访问类对象数据成员的地址就是在 NULL值基础上的偏移,所以出现访问小地址内存的情况。而64KB以内的小内存地址是系统禁止访问的,一旦访问,就会访问违例,系统会直接终止所在的进程的运行。关于64KB小内存地址禁止访问的说明,可以查看《Windows核心编程》一书上相关的章节,如下:

2d0fcc3a54a943819051e337face6547.png

       此外,上面还说到了访问空指针对应的类对象的虚成员函数也会引发访问违例。按讲函数地址是代码段的地址,直接call就行了,为啥还会引起数据段的内存访问违例呢?其实是这样的,C++中支持多态,如果类中有虚函数,类中会隐含一个虚函数表指针成员,该指针中存放的就是虚函数表的首地址。要调用虚函数,需要使用类对象地址进行二次寻址:类对象地址就是虚函数表指针变量的地址,此处进行第一次寻址,得到指针中存放的虚函数表的首地址,然后进行第二次寻址,得到虚函数表中存放的虚函数的函数地址(代码段的地址),进而调用虚函数。所以要调用虚函数,也要访问类对象的数据成员(虚函数表指针成员),所以对于用空指针调用虚函数,也会出现访问违例的情况。关于虚函数调用的二次寻址,可以参见之前写的文章:

几秒读懂C++虚函数调用的汇编代码实现icon-default.png?t=M666https://blog.csdn.net/chenlycly/article/details/121046234       

       所以,我们用windbg打开dump后,发现崩溃的那条汇编指令中出现访问小内存地址的,一般可能就是访问空指针引起的。从C++代码上看直接看崩溃没那么直观,从汇编代码看,则能直观的看出代码为什么会崩溃。

3、已释放内存的访问

        此处讲的已释放的内存,指的是堆内存。new出来的类对象地址保存在一个成员指针变量中,该类对象在被delete之后,保存该类对象地址的成员指针变量并没有被置为NULL,后来又使用到该成员指针变量中保存的地址来访问内存,这样就访问到了已经释放的内存,就可能会产生崩溃。

       没有将保存类对象指针的成员指针变量置为NULL,可能是编写代码时忘记将之置为NULL,也可能不需要置为NULL(该成员变量所在对象已经销毁)。使用已经被释放的堆内存地址去访问内存,在有的机器上可能不会崩溃,但是有的机器上是必崩的,之前测试那边就出现过,同一个bug在我们的电脑上都不会崩溃,但是在某台电脑上同样的操作是必崩的(这样的问题我们已经领教两次了)。其实,这和系统中的内存管理机制有关,当你访问到已经释放的内存,系统认为你内存访问违例了,你就崩溃了,系统没有认为你访问违例,你还能继续运行。

程序中访问了不属于不该访问的内存,不一定会触发内存访问违例,即便那块内存不属于你开发的模块中的,系统认为你违例了,你就违例了;系统认为你没违例,就没违例!

      以之前的一个bug为例,在有问题的机器上只要点击创会窗口后将创会窗口关闭掉,就会出现崩溃,而其他的电脑都没这个问题。后来排查源码得知,创会主窗口会有一些子窗口,创会主窗口销毁时会进入创会主窗口类的析构函数,析构函数中会给子窗口们发送WM_CLOSE消息,让子窗口们销毁掉,当时的开发者可能认为这是合理的。但其实这是有问题的,因为父窗口销毁前会自动将子窗口销毁掉,dui框架会自动将子窗口类对象delete掉,但是创会主窗口中成员变量没有同步修改,虽然创会主窗口的析构中对指针判空了,但是因为指针没有置NULL,还是访问了已经释放的堆内存,所以出现了崩溃。

主要是当时的开发人员没搞清楚父子窗口或者owner窗口与被own窗口之间在父窗口被销毁时的关系:子窗口在父窗口销毁时会被自动销毁掉,不需要再去销毁了,从而写出这样错误的代码。

       后来发现的一个截图的bug,和这个bug的表现是类似的,其他电脑上运行都没问题,就这台电脑上有问题。那为什么这台电脑比较诡异,和其他的电脑表现不太一样呢?据测试人员之前TL软件用的好好的,但是一段时间后windows系统检测到当前系统不是正版系统后,就出现了上述的一些问题。是系统发现不是正版的之后将系统的容错性降低了?这就不得而知了。

       当然我们要感谢这台有问题的电脑,帮我们发现了很多掩藏很深的问题。最开始编码时,可能没能觉察到代码问题,当程序出现异常后,才能发现我们代码的缺陷。

4、内存越界

       按照内存类型分类,内存越界主要分三大类:全局内存越界、栈内存越界和堆内存越界。其中栈内存越界要好排查很多,堆内存越界可能会导致堆内存被破坏,一旦导致堆内存被破坏,则会比较难以排查!

       有可能是类似memcpy的内存操作函数调用时传递的长度参数过长,也有可能是for循环时循环次数设置错了(循环体中对内存进行了一定的操作),导致对内存进行越界操作了。可能是越界的时候,系统的内存管理模块认为你访问违例,程序就直接崩溃了。可能当前的内存操作对象是在一个类中的,如果越界范围较小,则只会修改本类中的其他成员的内存内容;也可能其他模块申请的堆内存位于当前越界操作的堆内存后面,所以越界直接越到其他模块上了,所以越界改写了其他模块的内存,在其他模块运行代码时,因为内存中被篡改的异常值产生了崩溃,可能每次越界越到了不同的模块,所以可能会导致每次崩溃的位置都不太一样。

       栈内存越界,如果越的比较小,则会改写函数中其他局部变量的内存中的内容,如果越界越的比较多,则可能会将当前函数主调函数的ebp和返回地址改写掉,这样就会导致栈回溯出问题。栈内存越界可能稍微好排查一点,但是堆内存越界则比较难查,可能每次崩溃的模块都不太一样(每次崩溃都崩溃在不同的模块中,排查人员根本搞不清楚到底是哪个模块有问题,有时可能会很无助很绝望!)。

       对于堆内存的越界,有时也只能结合崩溃时的一些业务操作,结合代码的业务流程,从底层自下而上逐一排查代码了。

5、总结

       文中简单对内存访问违例的一些原因进行简单总结,希望能给大家提供一个借鉴和参考。

© 版权声明
THE END
喜欢就支持一下吧
点赞6613赞赏 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容