目录
1、概述
2、查看C运行时函数abort的内部实现
3、开源库jsoncpp中调用abort的代码场景说明
4、开源库WebRTC中调用abort的代码场景说明
5、项目问题实例分析
5.1、问题说明
5.2、进一步分析
5.3、动态申请内存失败的可能原因分析
6、最后
VC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/124272585C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/125529931 C++程序在运行过程中发生了异常闪退,但并没有生成包含异常上下文的dump文件,可能是程序执行了abort等直接程序止当前进程的操作。这类问题我们在实际项目中遇到过若干次,在此给大家做了大致的总结与分享。
1、概述
C++程序在运行过程中发生了异常闪退,但没有生成包含异常上下文的dump文件,没有dump文件问题也就不太好分析了。一般情况下,我们通过Windbg事后静态分析包含异常上下文的dump文件(dump文件是程序中安装的异常捕获模块感知到程序发生异常时自动生成的)去找线索、去排查的。dump文件是关键切入点,没有了dump文件,问题就很难排查下去的。
这类异常闪退不生成dump文件的问题,可能是程序中安装的异常捕获模块没捕获到异常,因为异常捕获模块是有一定缺陷的,没法捕获到所有的异常,只能捕捉到大部分情况下的异常。也有可能是程序中执行了abort等直接终止当前进程的操作,我们在实际项目中已经遇到过若干次了,比如jsoncpp库中监测到一些异常时会直接调用abort或exit操作,开源的WebRTC库中在用malloc动态申请内存时失败后会调用abort将进程直接终止运行。今天我们就以jsoncpp和WebRTC开源库为例,来详细讲述这两个库调用abort函数终止进程的问题。
2、查看C运行时函数abort的内部实现
abort是系统C运行时库中的C函数,可以直接在Visual Studio中go到该函数的实现代码,如下所示:
/***
*void abort() - abort the current program by raising SIGABRT
*
*Purpose:
* print out an abort message and raise the SIGABRT signal. If the user
* hasn't defined an abort handler routine, terminate the program
* with exit status of 3 without cleaning up.
*
* Multi-thread version does not raise SIGABRT -- this isn't supported
* under multi-thread.
*******************************************************************************/
void __cdecl abort (
void
)
{
_PHNDLR sigabrt_act = SIG_DFL;
#ifdef _DEBUG
if (__abort_behavior & _WRITE_ABORT_MSG)
{
/* write the abort message */
_NMSG_WRITE(_RT_ABORT);
}
#endif /* _DEBUG */
/* Check if the user installed a handler for SIGABRT.
* We need to read the user handler atomically in the case
* another thread is aborting while we change the signal
* handler.
*/
sigabrt_act = __get_sigabrt();
if (sigabrt_act != SIG_DFL)
{
raise(SIGABRT);
}
/* If there is no user handler for SIGABRT or if the user
* handler returns, then exit from the program anyway
*/
if (__abort_behavior & _CALL_REPORTFAULT)
{
_call_reportfault(_CRT_DEBUGGER_ABORT, STATUS_FATAL_APP_EXIT, EXCEPTION_NONCONTINUABLE);
}
/* If we don't want to call ReportFault, then we call _exit(3), which is the
* same as invoking the default handler for SIGABRT
*/
_exit(3);
}
上述代码中先调用了raise(SIGABRT),该函数是触发一个SIGABRT信号终止异常,如果当前正在调试状态,会让调试器中断下来。接下来调用C函数_exit退出当前进程。
3、开源库jsoncpp中调用abort的代码场景说明
以jsoncpp中的Value::asInt接口为例,接口代码的实现如下:
我们看case intValue分支,会调用isInt接口判断Value对象中的值是否是int类型的。然后我们走进JSON_ASSERT_MESSAGE宏内部:
如果传入的条件表达式condition为FALSE,则会调用JSON_FAIL_MESSAGE宏,然后再看JSON_FAIL_MESSAGE宏的实现部分,会先调用assert断言,紧接着就会调用abort函数。
再回到case intValue分支的代码处,如果isInt接口返回FALSE,则会执行到JSON_FAIL_MESSAGE宏中的代码,进而执行abort函数。以上就是jsoncpp内部触发abort函数调用的流程。
4、开源库WebRTC中调用abort的代码场景说明
以malloc申请动态内存失败的处理代码为例,如下所示:、
上面代码调用malloc申请内存,然后调用宏RTC_CHECK对malloc返回的指针是否为空,于是跳转到RTC_CHECK宏的实现代码:
RTC_CHECK宏中调用了rtc_FatalMessage函数,跳转到该函数的实现处:
rtc_FatalMessage函数中又调用了FatalLog接口,跳转到实现处:
最终FatalLog接口中先是调用了DebugBreak接口,尝试让当前正在调试的调试器中断下来,然后紧接着调用abort接口将进程强行终止掉。
DebugBreak是系统API函数,是通知当前正在调试的调试器,让调试器中断下来。比如当前正在使用Visual Studio调试代码,或者使用Windbg附加到进程上调试,调试器会中断下来!
5、项目问题实例分析
5.1、问题说明
有用户反馈,在其笔记本电脑上我们的程序在入会后会频繁地闪退,虽然不是必现的,但复现的概率很大。但在程序指定的目录中并没有找到dump文件,没生成dump文件可能有以下三种原因:
1)程序中的异常捕获模块没捕获到异常;
2)捕获到异常后生成dump文件时又产生了异常(二次异常),导致dump文件没有生成;
3)代码中检测到异常(比如malloc函数申请堆内存失败,返回空指针,并不是抛出异常,代码中检测该函数返回了空指针),直接调用abort或exit退出进程了,导致程序闪退了。
5.2、进一步分析
对于没有生成dump文件的场景,并且问题较好复现,可以使用Windbg附加到进程上进行动态调试,即Windbg和目标进程一起运行,如果目标进程发生问题,Windbg一般会第一时间感知到并中断下来,然后就可以查看当时的函数调用堆栈去分析了。
于是远程到用户的机器上,重新运行程序,然后启动Windbg,将Windbg附加到已经运行的程序进程上调试运行。复现问题后,Windbg自动中断了下来,于是用kn命令(也可以使用kv或kp命令)查看到如下的函数调用堆栈:
通过函数调用堆栈,出问题的代码就是上面给出的WebRTC中的相关代码。malloc申请堆内存失败,返回空指针,进入RTC_CHECK宏代码,接着进入rtc_FatalMessage函数中,紧接着又进入了FatalLog接口中。在FatalLog接口先是调用了DebugBreak接口,该接口使得当前正在调试运行的Windbg中断了下来,这就是我们在函数调用堆栈中看到的。
如果我们在Windbg中输入g命令让程序继续运行,Windbg还会中断一下,是因为abort内部抛出了一个SIGABRT信号异常,该异常也会让正在调试的调试器中断下来。
5.3、动态申请内存失败的可能原因分析
引发本例问题的原因是,调用malloc申请堆内存失败,返回空指针引起的。至于为啥在项目中会出现malloc申请堆内存失败,我们在这里就不再赘述了。此处,我们详细说一下动态申请堆内存失败可能的几个原因:
1)申请的内存过大,进程中没有这么大内存可用了
可能受一些异常数据的影响,申请了很大尺寸的内存。比如前段时间排查一个崩溃问题,当时因为数据有异常,一次性申请了9999*9999*4*2=762MB的堆内存,进程中没有这么大可用的堆内存了,所以申请失败了,new操作抛出了一个异常,而程序没有对异常处理,直接导致程序崩溃了。
2)用户态的内存已经达到了上限,申请不到内存了
有可能是虚拟内存占用太多,也有可能代码中有内存泄露,导致用户态的内存快被消耗完了。对于一个32程序,系统会给对应的进程分配4GB的虚拟地址空间,而用户态和内核态内存各占一半,即用户态的内存只有2GB,如果程序占用的虚拟内存比较大,比如接近2GB的用户态虚拟内存了,再申请大的内存就会申请失败,因为已经达到2GB的上限,虚拟内存要耗尽了。或者程序中有内存泄露,快要把用户态的2GB的虚拟内存给占用完了,再申请内存可能会申请失败的。
3)进程中的内存碎片过多
如果进程中在大量的new和delete,产生了大量的小块内存碎片,可用的内存大多是一小块一小块的小内存块,而要申请的是一块长度很长的内存,因为到处是内存碎片,没有这么一大块连续的可用内存,可能就会导致内存申请失败。
4)发生堆内存越界,导致堆内存被破坏,导致new操作产生异常(此时new不会返回NULL,会抛出异常)
我们可以在出问题的地方,对该处的new添加一个保护(但不可能对代码中所有new的地方都加这样的保护),我们通过添加try...catch去捕获new抛出的异常,并将异常码打印出来,如下所示:(下面的代码在循环申请内存,直到内存申请失败为止,主要用来测试用)
#include <iostream>
using namespace std;
int main(){
char *p;
int i = 0;
try
{
do{
p = new char[10*1024*1024];
i++;
Sleep(5);
}
while(p);
}
catch(const std::exception& e)
{
std::cout << e.what() << "\n"
<< "分配了" << i*10 << "M" << std::endl;
}
return 0;
}
还有一种方式,在new时传如一个std::nothrow参数,让new在申请不到内存时不要抛出异常,直接返回为NULL,这样我们就可以通过返回的地址是否为NULL(空),判断是否是内存申请失败了,示例代码如下:
#include <iostream>
int main(){
char *p = NULL;
int i = 0;
do{
p = new(std::nothrow) char[10*1024*1024]; // 每次申请10MB
i++;
Sleep(5);
}
while(p);
if(NULL == p){
std::cout << "分配了 " << (i-1)*10 << " M内存" //分配了 1890 Mn内存第 1891 次内存分配失败
<< "第 " << i << " 次内存分配失败";
}
return 0;
}
使用C语言中的malloc函数去申请堆内存是不会抛出异常的,申请失败时会返回NULL。对于代码中出现申请堆内存的失败的问题,一般的做法是终止进程的运行,很多开源库中就是这么处理的,比如我们在WebRTC开源库中就看到过。因为内存申请失败,正常的代码逻辑和业务也就没法正常继续下去了,让程序继续运行可能就没多大意义了。
6、最后
本文详细讲述了调用abort函数强行终止进程导致没有生成dump文件的相关细节及处理办法,这些内容都是通过项目问题实战总结和整理出来的,有一定的参考价值。