好几天前玩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."}},
};