当前位置: 首页 全部资源/编程 正文

C++软件发生异常时的常见原因分析与总结

admin |
144

我们在开发软件的过程中,会经常遇到这样那样的软件异常或软件崩溃的头疼问题。对于初学者或者没有多少经验的开发人员,问题排查起来会很吃力,甚至一点头绪都没有,不知道从何查起。有的疑难问题,即使是个有经验的程序员,排查起来可能也会很费劲。所以有必要总结一下软件异常和软件崩溃的常见原因。

C++软件发生异常时的常见原因分析与总结插图
C++软件发生异常时的常见原因分析与总结

根据多年来的开发经验,结合平时遇到的一些真实的软件异常场景,简单的分析与总结一下引发C++软件异常的原因,在此分享给出来,给大家提供一定的思路和参考。

1、空指针

这是很常见的错误。访问空指针会之所以会产生崩溃,是因为访问了Windows系统的64K禁止访问内存区(即0-64KB的内存区域),这是Windows故意设定的一块小地址内存区域,是为了方便程序员定位问题使用的。一旦访问到该内存区域,就会触发内存访问违例,系统会强制结束进程。

2、野指针

所谓野指针就是该指针指向的内存已经被释放了,但还去访问该指针指向的内存,就会导致内存访问违例。还有一种情形是同一段堆内存被delete或free了两次。

3、内存拷贝时越界

内存拷贝越界不一定会导致崩溃,有可能会越界到附近变量的内存上,即篡改了附近变量的值,导致代码的运行逻辑出异常。内存越界包含栈内存越界、堆内存越界和全局内存越界,这些类型的越界我们之前都遇到过。

4、GDI对象接近或达到1万个后导致的崩溃

当程序中有GDI对象泄露时,程序长时间拷机运行,可能就会出现GDI对象接近或达到1万个,导致绘图函数调用出现异常,窗口绘制不出来等情况,紧接着就会出现崩溃。很多Windows老程序员应该遇到过。

GDI对象有泄漏可能会导致GDI达到系统上限的问题,打开了程序的多个窗口也会导致这个问题,比如之前我们用MFC做UI界面时,一个聊天窗口就占用了200多个GDI对象,测试同事做极限测试时,打开了好几十个聊天窗口,这样也出现了GDI达到上限的问题。

5、线程栈溢出

Windows系统中线程栈默认是1MB(创建线程时也可以指定线程栈的大小),如果线程的栈内存超过上限值就会出现线程栈溢出,导致崩溃。这一般是函数递归调用引起的,函数递归调用导致递归调用的过程中,函数栈内存一直没有释放,栈内存持续增加,导致栈内存达到所在线程的上限值,发生栈溢出。

还有一种情况比较隐蔽,是消息等触发的多个函数之间的闭环调用,即函数调用上的死循环。此外还有第三种情况,如果在一个函数中使用到一个结构体,这个结构体定义的非常大,超过1MB,直接用该结构体定义成局部变量(变量在栈内存上分配内存),直接导致栈溢出。这个问题我们也遇到过,解决办法是在堆内存上new一个结构体对象。

6、系统内存耗尽Out of memory

当程序中有明显的内存泄露,长时间运行后将系统分配的4GB虚拟内存空间耗尽,系统物理内存被占用大半,此时系统为保证其他程序能正常运行,会强制将该问题进程结束掉。

7、switch中的case分支过多导致线程栈溢出

函数中switch语句中的case分支中,我们会在每个case分支中定义局部变量, 很多人可能会想当然的认为,这些分支中的局部变量的生命周期都在当前的case分支中,应该没问题。其实不然,这些case分支中局部变量都会直接占用所在函数的栈内存,case语句过多时就会在调用进该函数时,将当前线程栈内存耗尽,导致线程栈溢出。

我之前也不相信,特意写了测试代码,查看函数入口处的给当前函数分配栈空间的汇编代码,在case语句增加局部变量的定义,会看到函数入口处的分配函数的栈内存会增大。这个多个case分支导致的线程栈溢出问题,我们遇到了不止一次了!

8、函数调用约定不一致,导致的栈不平衡

标准调用约定和C调用约定,在释放函数传入参数占用的栈空间是不同的。如果被调用函数是标准约定,则由被调函数释放传给被调函数的参数的栈空间;如果被调函数是C调用,则由主调函数去释放栈空间。比如我们经常用到的C函数printf,支持多个可变参数的格式化,必须是C调用约定,因为被调函数无法知道传入了哪些参数,只有主调函数才知道,只有主调函数才知道传入参数占用的栈内存的大小,才好去释放参数的栈内存。这个问题我们也遇到。

9、dll库之间版本不一致引起的崩溃

比如有dll库有编译错误导致编译失败,没有生成新的dll,导致dll库之间版本不一致引发崩溃。一般是头文件有变更,比如结构体中有新增字段或者删除字段,多个dll库都使用到这个结构体,一旦有dll编译失败,就会使用老的dll文件,这样就出现了dll库之间版本不匹配的问题,导致崩溃。这个问题我们会经常遇到,我们的软件版本都是使用自动化编译脚本自动编译的,不是人工手动编译的。

不同dll模块结构体不一致,可能会导致主调函数栈内存被破坏。我举个例子,有种情况比较隐蔽,比如有两个模块:dll-A.dll和dll-B.dll,dll-B.dll模块的导出头文件中定义了Strucht1结构体,dll-A.dll模块中的函数Fun-M中定义了dll-B.dll模块的导出头文件中定义的Strucht1结构体对象,函数Fun-M调用dll-B.dll模块中的函数Func-N,并且将Strucht1结构体对象地址传给函数Func-N,在函数Func-N中会将所在模块中相关内存中数据拷贝到该结构体对象地址指向的内存中。假设在编译dll-A.dll时,引用的dll-B库的头文件是旧的(头文件在发布过来时漏发了),dll-B库的dll库文件是新的(头文件和库文件版本不一致),并且新版本的dll-B库中的定义的Strucht1结构体新增了一些字段,即结构变大了。这样dll-A.dll模块的函数Fun-M在调用dll-B库函数Fun-N时,在函数Fun-N中会拷贝信息到传进来的Strucht1结构体对象地址指向的内存中,这时就会发生内存拷贝越界,由于操作的内存地址是主调函数Fun-M中的变量地址,所以越界是越到了主调函数Fun-M栈内存上,可能会越界到主调函数Fun-M中定义的其他局部变量的内存上,即将主调函数中其他变量的值给篡改了。

10、死循环

死循环一般只是引发CPU占用率较高,一般不会导致软件的异常崩溃。死循环一般是for循环或while循环的循环控条件出了问题,可能是循环控制条件写错了,也可能是循环控制条件中变量出现了异常大的值。当然还有一种情况是消息触发的函数调用的死循环。如果是死循环发生在UI线程中,会导致UI界面卡死,UI界面点击没反应,这种情况很容易就判断出可能是UI线程发生了死循环。使用windbg动态调试目标进程,则可能很容易查看问题发生在哪个函数中。

11、死锁

死锁一般发生在多线程同步的时候,比如线程1占,用了锁A,在等待获取锁B,而线程2占用了锁B,在等待获取锁A,两个线程各不相让,在没有获取到自己要获取的锁之前,不会释放另一个锁,这样就导致了死锁。需要做好多个线程间协调,避免死锁问题的出现。使用windbg可以很好的分析出死锁的问题。

结束语

上面系统的总结了多种导致软件异常或崩溃的可能原因,给大家提供一些借鉴或参考。希望大家在了解这些后,在遇到问题时能了然于胸,能更有针对性的去排查和解决你们遇到的问题。

声明:原创文章请勿转载,如需转载请注明出处!