C++ 实现强行阻止程序崩溃并返回执行处 SetUnhandledExceptionFilter

好几天前玩PVZ杂交版玩到精彩处给我崩溃了,气坏了,还没保存啊喂,突然就想到了自己正在做的游戏项目,万一崩溃也没办法,就仔细考虑了这个问题。

对于C++标准异常,

例如out_of_range,logic_error,runtime_error等,可以直接 try{}catch(...){} ,可谓非常方便;

而对于C异常,

用C++的这个方法根本无法捕获,对于MSVC编译器可以采用 __try和__except ,(仅MSVC,老古董了,知道的人也不多)这里有一篇详细介绍可以去看:
窥探 try ... catch 与 __try ···__except 的区别

你也可以查看:
微软官方文档

精选代码:

cpp 复制代码
__try {
   int i = readAge();
   printf("Age inputed is %d", i);
} __except (extract(GetExceptionInformation()), EXCEPTION_EXECUTE_HANDLER) {
   printf("Exception happene.");
}

void extract(LPEXCEPTION_POINTERS p) {
   int d = p->ExceptionRecord->NumberParameters; //参数数目, 这里没用到
   unsigned int * ex = (unsigned int *) p->ExceptionRecord->ExceptionInformation[1]; //第二个参数
   AgeException * e = (AgeException *) ex; //转换,得到异常类的实例
   printf("==> %d \n", e->errorAge); //异常的信息可以知道了.
    printf("==> %s \n", e->p); //异常的信息可以知道了.
}

可以看出基本可以实现C++try catch的功能了,甚至还能解析异常信息。

但这样的代码兼容性不好,其他编译器就无法使用。

然后

方法就是用底层函数 SetUnhandledExceptionFilter了,这个函数可以设置未处理的异常(包括C和C++的,C++异常是特殊的一类C异常)的处理回调函数。

比如说,最经典的,崩溃时产生一个dmp错误报告

示例代码,选自DarkVoxel2:

cpp 复制代码
//need dbghelp.h
int GenerateMiniDump(PEXCEPTION_POINTERS pExceptionPointers)
{
	// 定义函数指针
	typedef BOOL(WINAPI * MiniDumpWriteDumpT)(
		HANDLE,
		DWORD,
		HANDLE,
		MINIDUMP_TYPE,
		PMINIDUMP_EXCEPTION_INFORMATION,
		PMINIDUMP_USER_STREAM_INFORMATION,
		PMINIDUMP_CALLBACK_INFORMATION
		);
	//确保目录存在,缺失代码自己补
	if (!ExistFile(DV_DIR + "Dump"))
		CreateDirectoryA((DV_DIR + "Dump").c_str(), nullptr);
	// 从 "DbgHelp.dll" 库中获取 "MiniDumpWriteDump" 函数
	MiniDumpWriteDumpT pfnMiniDumpWriteDump = NULL;
	HMODULE hDbgHelp = LoadLibrary(_T("DbgHelp.dll"));
	if (NULL == hDbgHelp)
	{
		return EXCEPTION_CONTINUE_EXECUTION;
	}
	pfnMiniDumpWriteDump = (MiniDumpWriteDumpT)GetProcAddress(hDbgHelp, "MiniDumpWriteDump");

	if (NULL == pfnMiniDumpWriteDump)
	{
		FreeLibrary(hDbgHelp);
		return EXCEPTION_CONTINUE_EXECUTION;
	}
	// 创建 dmp 文件
	TCHAR szFileName[MAX_PATH] = { 0 };
	string version = "DV2_CRASH_INFO";
	SYSTEMTIME stLocalTime;
	GetLocalTime(&stLocalTime);
	wsprintf(szFileName, (DV_DIR + "Dump\\%s_%04d%02d%02d-%02d%02d%02d.dmp").c_str(),
		version.c_str(), stLocalTime.wYear, stLocalTime.wMonth, stLocalTime.wDay,
		stLocalTime.wHour, stLocalTime.wMinute, stLocalTime.wSecond);
	HANDLE hDumpFile = CreateFileW(szFileName, GENERIC_READ | GENERIC_WRITE,
		FILE_SHARE_WRITE | FILE_SHARE_READ, 0, CREATE_ALWAYS, 0, 0);
	if (INVALID_HANDLE_VALUE == hDumpFile)
	{
		FreeLibrary(hDbgHelp);
		return EXCEPTION_CONTINUE_EXECUTION;
	}
	// 写入 dmp 文件
	MINIDUMP_EXCEPTION_INFORMATION expParam;
	expParam.ThreadId = GetCurrentThreadId();
	expParam.ExceptionPointers = pExceptionPointers;
	expParam.ClientPointers = FALSE;
	pfnMiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(),
		hDumpFile, MiniDumpWithDataSegs, (pExceptionPointers ? &expParam : NULL), NULL, NULL);
	// 释放文件
	CloseHandle(hDumpFile);
	FreeLibrary(hDbgHelp);
	DebugLog("【崩溃】游戏出现全局异常,已崩溃");
	int ch = MessageBox(NULL, "游戏出现了一个问题,造成了崩溃 >_< \n已经在游戏Dump目录下创建了一个最新的崩溃信息Dump文件,将它发送给游戏作者,这个Bug就能得到修复 :D \n那么现在你是否要保存存档呢? :)", "哦,不!!", MB_YESNO | MB_ICONERROR | MB_SYSTEMMODAL);
	if (ch == IDYES)
	{
		Save();
	}
	exit(-1);
	return EXCEPTION_EXECUTE_HANDLER;
}
LONG WINAPI ExceptionFilter(LPEXCEPTION_POINTERS lpExceptionInfo)
{
	
	if (IsDebuggerPresent())
	{   //调试中就交给调试器处理
		return EXCEPTION_CONTINUE_SEARCH;
	}
	return GenerateMiniDump(lpExceptionInfo);
}
cpp 复制代码
SetUnhandledExceptionFilter(ExceptionFilter);

配合一个发送邮箱,就属于是很多应用程序的基本操作了。

大致提一下自定义回调函数的返回值:

宏名 我的理解 机翻释义
EXCEPTION_EXECUTE_HANDLER 闪退 系统将控制权转给异常处理程序,并在找到处理程序的堆栈帧中继续执行。
EXCEPTION_CONTINUE_SEARCH XXX 已停止工作 系统继续搜索处理程序。
EXCEPTION_CONTINUE_EXECUTION 继续执行下一条语句 系统停止处理程序搜索,并将控制权返回到异常发生位置。 如果异常不可连续,则会导致 EXCEPTION_NONCONTINUABLE_EXCEPTION 异常。

所以说如果你想一直保持闪退,直接返回EXCEPTION_EXECUTE_HANDLER。

如果你想像标题那样阻止崩溃并一直返回执行处,你就直接返回EXCEPTION_CONTINUE_EXECUTION,当然是有风险的。

如果硬是要使用setjmp+longjmp实现跳转,这里有个简单的案例可以使用,同时你可以学习这个异常参数该怎么解析:

cpp 复制代码
#include <cstdio>
#include <windows.h>
#include <setjmp.h>
#include <iostream>
using namespace std;

jmp_buf jmpBuffer;

// 自定义的异常处理函数
LONG WINAPI CustomUnhandledExceptionFilter(EXCEPTION_POINTERS* ExceptionInfo) {
//	printf("Unhandled exception occurred!\n");
	
	
	auto record = ExceptionInfo->ExceptionRecord;
	if (record)
	{
		cout << "NumberParams:  " << record->NumberParameters << "\n";
		cout << "ExceptionCode:  " << showbase << hex << record->ExceptionCode << "\n";
		cout << "ExceptionFlags:  " << showbase << hex << record->ExceptionFlags << "\n";
		cout << "Address:  " << showbase << hex << reinterpret_cast<UINT64>(record->ExceptionAddress) << "\n";
	}
	
	printf("使用不死图腾!!\n");	
	// 尝试使用 longjmp 跳过异常引发的代码
	longjmp(jmpBuffer, 
		1	//这里可以传递错误信息,作为setjmp第二次返回值
		);
	
	// 如果 longjmp 成功执行,这里将不会被执行
	return EXCEPTION_CONTINUE_SEARCH;
}

int main() {
	// 设置自定义的未处理异常过滤器
	SetUnhandledExceptionFilter(CustomUnhandledExceptionFilter);
	int ret = 0;
	// 尝试使用 longjmp 跳过异常引发的代码
	if ((ret = setjmp(jmpBuffer)) == 0) {
		// 可能引发异常的代码
		printf("开始渡劫\n");
		
		//1.
		int* ptr = nullptr;
		*ptr = 114514;	//NullPointerException
		
		//2.
		int* ptr2 = reinterpret_cast<int*>(0x01);
		*ptr2 = 42; 	//Access Violation
		
		//3.
		int a[1] {0};
		a[100]=1;		//Access Violation
		
		//abort直接崩溃,不是异常
	} else {
		//printf("Recovered from exception using longjmp.\n");
		printf("获得了成就 [超越生死]!\n");
		//printf("ret = %d\n", ret);	//你传递的错误信息
	}
	
	//printf("Program continues after exception.\n");
	printf("大难不死,劫后余生\n");
	
	return 0;
}

/*
注意事项:
使用 longjmp 跳过异常引发的代码属于非标准做法: 这种方法可能会导致程序状态不一致或未定义行为。因此,一般情况下不建议在生产环境中使用这种方式来处理异常。
更好的做法是预防异常的发生: 在编程中应当遵循良好的异常处理实践,通过合适的检查和错误处理机制来防止空指针访问等异常的发生。
总结来说,SetUnhandledExceptionFilter 可以用于捕获异常,但使用 longjmp 跳过异常引发的代码并不是一个推荐的做法,应该仅作为示例和理论演示。
*/

那假如我不想返回执行处而是想执行到之前一个任意位置,或是加上一个对用户的提示,根据用户的选择执行不同操作,怎么搞呢?

我差不多代码是这样,选自TerraSurvivor:

cpp 复制代码
//need setjmp.h
//*************************************************
//你要跳转回来的地方,其中g.anticrash_jmp_buf的类型是jmp_buf
        int jmp_ret = 0;
        if ((jmp_ret = setjmp(g.anticrash_jmp_buf)) != 0)
        {
            DebugLog("[!] 已跳转回 InGame 保存点");
        }
//*************************************************
cpp 复制代码
// 自定义的异常处理函数
// 未实现函数请自行补充或删除
LONG WINAPI CustomUnhandledExceptionFilter(EXCEPTION_POINTERS* ExceptionInfo) 
{
	
	DebugLog("【异常】发生异常");
	
	if (fequ(GetOption("强行阻止崩溃"), 0.0f)) //功能未启用
		return EXCEPTION_EXECUTE_HANDLER;   //立即崩溃
	
	static bool in{ false };
	
	if (in) //不得递归
	{
		cerr << "<!> 严重错误:异常处理函数 CustomUnhandledExceptionFilter 内部发生异常。为防止爆栈,这里将立即返回。\n";
		return EXCEPTION_CONTINUE_SEARCH; 
	}
	
//	lock_guard lock(g.mtx_anticrash);   //没必要感觉,线程是独立的
	
	in = true;
	
	bool game_running {!game.ui_stages.empty() 
		&& game.ui_stages.top() == TerraSurvivor::UIPlaying 
		&& !game.world.paused};
	if (game_running)
		game.world.TogglePause();  //暂停游戏
	
	
	DWORD errorCode{ 0L };
	auto record = ExceptionInfo->ExceptionRecord;
	if (ExceptionInfo && record)
		errorCode = record->ExceptionCode;
	if (errorCode)
	{
		cerr << "[!] Unhandled exception occurred! Error code: " << hex << showbase << errorCode << '\n';
		cerr << "    发生未解决的异常!错误码:" << hex << showbase << errorCode << '\n';
	}
	else {
		cerr << "[!] Unhandled exception occurred!" << '\n';
		cerr << "    发生未解决的异常!" << '\n';
	}

	string errorText{};
	
	if (g.exception_explanations.find(errorCode) != g.exception_explanations.end())
	{ //错误码解释
		errorText = g.exception_explanations[errorCode].second;
	}
	else {
		errorText = "未知的异常。";
	}
	
	stringstream ss;
	ss << ECSTR("上帝阻止了一次崩溃。") << '\n';
	if (record)
	{
		ss << ECSTR("异常错误码") << ": " << hex << showbase << errorCode << '\n';
		ss << ECSTR("异常信息") << ": " << errorText << '\n';
		// if (g.exception_comments.find(errorCode) != g.exception_comments.end())
		//     ss << ECSTR("作者批注") << ": " << g.exception_comments[errorCode] << '\n';
		ss << ECSTR("异常标志") << ": " << dec << record->ExceptionFlags;
		
		if (EXCEPTION_NONCONTINUABLE == record->ExceptionFlags)
			ss << ECSTR("(不可连续异常)") << '\n';
		else
			ss << '\n';
		
		ss << ECSTR("异常地址") << ": " << showbase << hex << record->ExceptionAddress << '\n';
	}
	cerr << ss.str();
	
	ss << ECSTR("按\"确定\"上帝将尝试挽救游戏") << '\n'
	<< ECSTR("按\"取消\"使游戏崩溃") << '\n';
	
	//弹出对话框发出警告
	int ch = IDOK;
	static clock_t lastRescue = clock();
	RescueTipType rtt = RescueTipType(GetOption("挽救提示"));
	
	if (rtt == RTT_Null)
	{
		MessageBeep(MB_ICONERROR);
	}
	else if (rtt == RTT_MessageBox
		//|| (clock() - lastRescue <= ANTICRASH_RESCUE_CD)  //过于频繁
		)
	{
		ch = MessageBoxA(nullptr, ss.str().c_str(),
			ECSTR("强行阻止崩溃已启用"),
			MB_ICONEXCLAMATION | MB_SYSTEMMODAL | MB_OKCANCEL);
		//由于中文会乱码,还是英文版本好了
		
		/* if (clock() - lastRescue <= ANTICRASH_RESCUE_CD)
	{
		string s{"(X) 防崩溃系统调用过于频繁,本次不得不开启弹窗。"};
		cerr << s << '\n';
		DebugLog(s);
		}*/
	}
	else if (rtt == RTT_Chat)
	{
		chat.AddChat(LSTR("上帝阻止了一次崩溃。"), Choice({ RED, LIGHTRED, GOLD, ORANGE, YELLOW }));
	}
	else if (rtt == RTT_AboveTip)
	{
		AboveTip(LSTR("上帝阻止了一次崩溃。"), Choice({ RED, LIGHTRED, GOLD, ORANGE, YELLOW }));
	}
	else if (rtt == RTT_RightTopTip)
	{
		RightTopTip(LSTR("上帝阻止了一次崩溃。"), Choice({ RED, LIGHTRED, GOLD, ORANGE, YELLOW }));
	}
	else
	{
	}
	
	if (IDCANCEL == ch)
	{
		in = false;
		cerr << "[*] Cancel the game rescue.\n";
		cerr << "    取消挽救游戏。\n";
		DebugLog("【异常】取消挽救游戏。");
		return EXCEPTION_CONTINUE_SEARCH;       //已停止工作
		//return EXCEPTION_EXECUTE_HANDLER;     //立即崩溃退出
	}
	else {
		lastRescue = clock();
		cerr << "[*] Attempt to rescue the game.\n";
		cerr << "    尝试挽救游戏。\n";
		
		RescueGameMethod method = RescueGameMethod(GetOption("挽救方式"));
		
		switch (method)
		{
			case ContinueInGame: {
				break;
			}
			case SaveAndContinueInGame: {
				game.SaveGame();
				break;
			}
			case SaveAndTitle: {
				game.SaveAndQuit();
				break;
			}
			case SaveAndExit: {     //noreturn
				game.SaveGame();
				_Leave();
				break;
			}
			case ImmediateExit: {   //noreturn
				_Leave();
				break;
			}
		}
		in = false;
		if (game_running)
			game.world.TogglePause();
		// 跳回保存点 进行后续实际操作
		longjmp(g.anticrash_jmp_buf, errorCode);
	}
	//不应该执行到这里,因为上面有个 longjmp 已经强行跳回去了
	in = false;
	if (game_running)
		game.world.TogglePause();
	return EXCEPTION_CONTINUE_EXECUTION;       //强行继续
}

下面这段代码用于禁用后续对 SetUnhandledExceptionFilter 的调用,至于为什么,好像说是什么编译器搞不好可能会把自定义的回调函数重置(执行SetUnhandledExceptionFilter(NULL);),这就麻烦了,于是有国外大佬弄了一个内联钩子一样的东西直接禁用后续的使用。这是32位程序专用代码。

cpp 复制代码
//MUST X86
BOOL PreventSetUnhandledExceptionFilter(LPTOP_LEVEL_EXCEPTION_FILTER myFilterFunc)
{
	//ILHook
	HMODULE hKernel32 = LoadLibrary(_T("kernel32.dll"));
	if (hKernel32 == NULL) return FALSE;
	void* pOrgEntry = GetProcAddress(hKernel32, "SetUnhandledExceptionFilter");
	if (pOrgEntry == NULL) return FALSE;
	unsigned char newJump[100];
	DWORD dwOrgEntryAddr = (DWORD)pOrgEntry;
	dwOrgEntryAddr += 5; // add 5 for 5 op-codes for jmp far
	void* pNewFunc = myFilterFunc;
	DWORD dwNewEntryAddr = (DWORD)pNewFunc;
	DWORD dwRelativeAddr = dwNewEntryAddr - dwOrgEntryAddr;

	newJump[0] = 0xE9;  // JMP absolute
	memcpy(&newJump[1], &dwRelativeAddr, sizeof(pNewFunc));
	SIZE_T bytesWritten;
	BOOL bRet = WriteProcessMemory(GetCurrentProcess(),
		pOrgEntry, newJump, sizeof(pNewFunc) + 1, &bytesWritten);
	return bRet;
}

主函数调用:

cpp 复制代码
SetUnhandledExceptionFilter(CustomUnhandledExceptionFilter);
PreventSetUnhandledExceptionFilter(CustomUnhandledExceptionFilter);

这样玩家就能自定义处置方式了:

游戏里比如说碰到一个空指针读写异常:

按下确定直接保存并继续游戏,非常good,按取消直接崩溃。

对于开发者而言,崩溃越早越好,有利于定位BUG,从根本上解决问题;

对于玩家而言,崩溃越少越好,只想要更好的游戏体验。但是,因为一发生异常代码就会跳转,玩家看上去影响不大,但肯定会有少则一条多则一段的语句没有被正确/完整执行,从而发生一些奇奇怪怪的漏洞,这属于代码层面的漏洞,很难被发现,但是如果影响大则很致命。

代码里面有个g.exception_explanations,是个字典,存储了常见异常错误码对应的解释,自己从微软官网整理的,这里放出来:

cpp 复制代码
map<DWORD, pair<string, string>> exception_explanations
{
    {0, make_pair("无异常。","No Exception.")},
    {EXCEPTION_ACCESS_VIOLATION, {"尝试从虚拟地址读取或写入其没有相应访问权限的虚拟地址。","The thread tried to read from or write to a virtual address for which it does not have the appropriate access."}},
    {EXCEPTION_ARRAY_BOUNDS_EXCEEDED, {"尝试访问超出边界且基础硬件支持边界检查的数组元素。","The thread tried to access an array element that is out of bounds and the underlying hardware supports bounds checking."}},
    {EXCEPTION_BREAKPOINT, {"遇到断点。","A breakpoint was encountered."}},
    {EXCEPTION_DATATYPE_MISALIGNMENT, {"尝试读取或写入在不提供对齐的硬件上未对齐的数据。","The thread tried to read or write data that is misaligned on hardware that does not provide alignment."}},
    {EXCEPTION_FLT_DENORMAL_OPERAND, {"浮点运算中的一个操作数是反常运算。非规范值太小,无法表示为标准浮点值。","One of the operands in a floating-point operation is denormal."}},
    {EXCEPTION_FLT_DIVIDE_BY_ZERO, {"尝试将浮点值除以 0 的浮点除数。","The thread tried to divide a floating-point value by a floating-point divisor of zero."}},
    {EXCEPTION_FLT_INEXACT_RESULT, {"浮点运算的结果不能完全表示为小数点。","The result of a floating-point operation cannot be represented exactly as a decimal fraction."}},
    {EXCEPTION_FLT_INVALID_OPERATION, {"另类浮点异常。","Another floating-point exception."}},
    {EXCEPTION_FLT_OVERFLOW, {"浮点运算的指数大于相应类型允许的量级。","The exponent of a floating-point operation is greater than the magnitude allowed by the corresponding type."}},
    {EXCEPTION_FLT_STACK_CHECK, {"堆栈因浮点运算而溢出或下溢。","The stack overflowed or underflowed as the result of a floating-point operation."}},
    {EXCEPTION_FLT_UNDERFLOW, {"浮点运算的指数小于相应类型允许的量级。","The exponent of a floating-point operation is less than the magnitude allowed by the corresponding type."}},
    {EXCEPTION_ILLEGAL_INSTRUCTION, {"尝试执行无效指令。","The thread tried to execute an invalid instruction."}},
    {EXCEPTION_IN_PAGE_ERROR, {"尝试访问不存在的页面,但系统无法加载该页。","The thread tried to access a page that was not present, and the system was unable to load the page."}},
    {EXCEPTION_INT_DIVIDE_BY_ZERO, {"尝试将整数值除以零的整数除数。","The thread tried to divide an integer value by an integer divisor of zero."}},
    {EXCEPTION_INT_OVERFLOW, {"整数运算的结果导致执行结果中最重要的位。","The result of an integer operation caused a carry out of the most significant bit of the result."}},
    {EXCEPTION_INVALID_DISPOSITION, {"异常处理程序向异常调度程序返回了无效处置。","An exception handler returned an invalid disposition to the exception dispatcher."}},   //使用高级语言(如 C)的程序员不应遇到此异常。
    {EXCEPTION_NONCONTINUABLE_EXCEPTION, {"尝试在发生不可连续的异常后继续执行。","The thread tried to continue execution after a noncontinuable exception occurred."}},
    {EXCEPTION_PRIV_INSTRUCTION, {"尝试执行在当前计算机模式下不允许其操作的指令。","The thread tried to execute an instruction whose operation is not allowed in the current machine mode."}},
    {EXCEPTION_SINGLE_STEP, {"跟踪陷阱或其他单指令机制指示已执行一个指令。","A trace trap or other single-instruction mechanism signaled that one instruction has been executed."}},
    {EXCEPTION_STACK_OVERFLOW, {"线程用罄了其堆栈。","The thread used up its stack."}},
};
相关推荐
xlsw_3 分钟前
java全栈day20--Web后端实战(Mybatis基础2)
java·开发语言·mybatis
Dream_Snowar1 小时前
速通Python 第三节
开发语言·python
唐诺1 小时前
几种广泛使用的 C++ 编译器
c++·编译器
XH华1 小时前
初识C语言之二维数组(下)
c语言·算法
高山我梦口香糖2 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
冷眼看人间恩怨2 小时前
【Qt笔记】QDockWidget控件详解
c++·笔记·qt·qdockwidget
信号处理学渣2 小时前
matlab画图,选择性显示legend标签
开发语言·matlab
红龙创客2 小时前
某狐畅游24校招-C++开发岗笔试(单选题)
开发语言·c++
Lenyiin2 小时前
第146场双周赛:统计符合条件长度为3的子数组数目、统计异或值为给定值的路径数目、判断网格图能否被切割成块、唯一中间众数子序列 Ⅰ
c++·算法·leetcode·周赛·lenyiin
jasmine s3 小时前
Pandas
开发语言·python