• QQ
  • nahooten@sina.com
  • 常州市九洲新世界花苑15-2

技术天地

让程序在解体时面子的退出之SEH

原创内容,转载请注明原文网址:http://homeqin.cn/a/wenzhangboke/jishutiandi/2019/0729/586.html

 
 SEH的全称是Structured Exception Handling,是Windows操作系统提供的一种异常处置方式。SEH是属于操作系统的特性,不为特定言语设计,从它的名字就能看出它是常州平台运营一种构造化的异常处置方式。SEH包括了2个局部:终止处置__try/__finally和异常处置__try/__except,下面分别停止引见。
 
        终止处置__try/__finally
        __try/__finally能够保证无论try块内的代码执行结果如何,finally块内的代码总会被调用和执行。如今用下面的这个VC++中的控制台程序来阐明。
 
view plain
int _tmain(int argc, _TCHAR* argv[])  
{  
    __try  
    {  
        MessageBox(NULL, _T("Message from '__try' section"), _T("Test"), MB_OK);  
        // 除零,人为的使程序解体  
        //  
        int i = 13;  
        int j = 0;  
        int m = i / j;  
    }  
    __finally  
    {  
        // 在这里添加处置程序解体状况的代码  
        //  
        // 这里以弹出一个对话框为例子  
        //  
        MessageBox(NULL, _T("Message from '__finally' section"), _T("Test"), MB_OK);  
    }  
    MessageBox(NULL, _T("Funcation completed"), _T("Test"), MB_OK);  
    return 0;  
}  
        编译上面的代码。运转生成的EXE,会弹出下面的对话框。
 
 
 
        点击OK按钮后,程序会解体。
 
 
 
        在呈现上面这个对话框的时分点击Cancel,将控制权返还给程序,那么下面的对话框就会弹出。
 
 
 
        点击OK按钮后,程序正常退出。
        由上面的例子能够看出,无论try块中的代码会不会呈现异常,在程序终止的时分,finally块中的代码都会被调用和执行。所以普通状况下,finally块中的代码都是用来做一些清算工作和资源的释放。
 
        异常处置__try/__except
        __try/__except是用来捕捉异常的,只要当try块中的代码呈现异常的时分,except块中的常州微信公众平台代码才会被调用和执行。它的语法是这样的:
 
view plain
__try  
{  
    // guarded code  
}  
__except(expression)  
{  
     // exception handler code  
}  
        它最大的一个益处就是能够完整控制异常进程。expression的值决议了异常被处置完后,进程该如何执行。下面仍然用VC++中的控制台程序来阐明。
 
view plain
int _tmain(int argc, _TCHAR* argv[])  
{  
    __try  
    {  
        MessageBox(NULL, _T("Message from '__try' section"), _T("Test"), MB_OK);  
        // 除零,人为的使程序解体  
        //  
        int i = 13;  
        int j = 0;  
        int m = i / j;  
    }  
    __except(EXCEPTION_EXECUTE_HANDLER)  
    {  
        // 在这里添加处置程序解体状况的代码  
        //  
        // 这里以弹出一个对话框为例子  
        //  
        MessageBox(NULL, _T("Message from '__except' section"), _T("Test"), MB_OK);  
    }  
    MessageBox(NULL, _T("Funcation completed"), _T("Test"), MB_OK);  
    return 0;  
}  
        编译上面的代码。运转生成的EXE,会依次弹出下面的对话框。
 
 
 
        能够看出,在异常处置代码被调用执行后(except块中的代码),程序继续能够继续运转,并正常退出,并没有解体!经过运用__try/__except能够捕捉到任何类型的异常,并保证程序不会解体!(想想这是多么的神奇,一个程序永远不会解体!)
        下面解释一下except中表达式各个值的含义:
 
EXCEPTION_CONTINUE_SEARCH        异常没有被处置,继续向上抛出。假如更上层的代码没有异常捕捉机制,程序就会解体。
EXCEPTION_CONTINUE_EXECUTION   异常曾经被处置,返回异常发作的中央继续执行。
EXCEPTION_EXECUTE_HANDLER        异常曾经被处置,程序继续往后执行。
 
        经过上面的例子能够晓得,SEH的异常处置跟C++的异常处置不同,C++的try/catch不能控制异常进程,关于异常,要么处置,要么继续向上抛出。而SEH却能完整控制异常进程,处置完异常之后,还能决议进该进程如何执行。只需SEH运用得当,编写一个永不解体的应用程序成为可能。但是SEH有一个致命的弱点,那就是它是一种构造化的异常处置,所以不支持面向对象。下面用详细的VC++控制台程序来阐明。
 
view plain
// 一个有函数调用的类  
//   
class CrashTest  
{  
public:  
    CrashTest() {}  
    ~CrashTest() {}  
    void Test()   
    {   
        Crash();   
    }  
private:  
    void Crash()   
    {   
        // 除零,人为的使常州微信小程序开发程序解体  
        //  
        int i = 13;  
        int j = 0;  
        int m = i / j;  
    }  
};  
int _tmain(int argc, _TCHAR* argv[])  
{  
    __try  
    {  
        CrashTest test;  
        test.Test();  
    }  
    __except(EXCEPTION_EXECUTE_HANDLER)  
    {  
        // 在这里添加处置程序解体状况的代码  
        //  
    }  
    return 0;  
}  
        上面的代码不能经过编译,VC++编译器会给出这样的错误信息:error C2712: Cannot use __try in functions that require object unwinding。错误缘由很简单,try块内运用了对象。
        要想处理这个问题,能够把运用对象的逻辑放到一个函数里,然后在try里调用这个函数,来骗过编译器。把上面的代码修正成下面这样就能够经过编译。
 
view plain
void Test()  
{  
    CrashTest test;  
    test.Test();  
}  
int _tmain(int argc, _TCHAR* argv[])  
{  
    __try  
    {  
        Test();  
    }  
    __except(EXCEPTION_EXECUTE_HANDLER)  
    {  
        // 在这里添加处置程序解体状况的代码  
        //  
    }  
    return 0;  
}  
 
 
让程序在解体时面子的退出之Unhandled Exception
 
程序是由代码编译出来的,而代码是由人写的。人非圣贤,孰能无过。所以由人写的代码有缺陷是很正常的。当然很多异常都在开发阶段被思索到而添加了处置代码,或者用try/catch对可能呈现异常的中央停止额外的照顾。可是,还是会有一些无法意料的异常(Unhandled Exception)在程序运转的时分呈现。这些异常很多时分都会招致程序的解体。那么有没有什么办法能够让程序在解体的时分面子的退出呢?答案是肯定的。能够用Windows API中的SetUnhandledExceptionFilter来设置一个回调函数来处置这些无法意料的异常。
        要想运用SetUnhandledExceptionFilter,必需#include <Windows.h>,这个函数的声明如下:
 
view plain
LPTOP_LEVEL_EXCEPTION_FILTER WINAPI SetUnhandledExceptionFilter(  
  __in  LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter  
);  
        详细的参数和返回值的解释能够查找MSDN,有很细致的阐明。下面就用详细的代码来阐明怎样运用这个Windows API来使程序在解体的时分面子的退出。代码里面有细致的注释来协助了解。
        用VC创立一个名为Test的控制台程序,添加下面的代码。
 
view plain
#include "stdafx.h"  
int _tmain(int argc, _TCHAR* argv[])  
{  
    // 除零,人为的使程序解体  
    //  
    int i = 13;  
    int j = 0;  
    int k = i / j;  
    return 0;  
}  
        编译上面的代码,到工程的Debug目录下找到编译好的Test.exe,双击运转,在Windows 7下就会呈现下面的对话框通知你程序无法运转,也就是解体了。
 
 
 
        为了防止呈现程序解体的状况,用前面所说的办法,把代码修正成下面这样。
 
view plain
#include "stdafx.h"  
#include <Windows.h>  
// 处置Unhandled Exception的回调函数  
//  
LONG ApplicationCrashHandler(EXCEPTION_POINTERS *pException)  
{     
    // 在这里添加处置程序解体状况的代码  
    // 如今很多软件都是弹出一个发送错误报告的对话框  
    // 这里以弹出一个错误对话框并退出程序为例子  
    //  
    FatalAppExit(-1,  _T("*** Unhandled Exception! ***"));  
    return EXCEPTION_EXECUTE_HANDLER;  
}  
int _tmain(int argc, _TCHAR* argv[])  
{  
    // 设置处置Unhandled Exception的回调函数  
    //   
    SetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER)ApplicationCrashHandler);   
    // 除零,人为的使程序解体  
    //  
    int i = 13;  
    int j = 0;  
    int k = i / j;  
    return 0;  
}  
        编译上面的代码,到工程的Debug目录下找到编译好的Test.exe,双击运转,就会呈现下面这样的对话框,点击OK按钮后,程序正常退出。
 
 
 
让程序在解体时面子的退出之CallStack
 
在我的那篇《让常州网站开发建设程序在解体时面子的退出之Unhandled Exception》中提供了一个捕捉程序解体事情的办法,能够添加代码在程序解体的时分做出恰当的处置。不过,只晓得程序在什么时分解体,但是不晓得为什么解体,这关于程序开发者来说没有任何意义。由于假如不晓得程序解体的缘由,就没法去找到代码中的缺陷,当然就没法去修正代码而防止程序的解体。
        一切调试过代码的开发者都晓得CallStack的重要性。假如在程序解体的时分得到CallStack,那么就能定位程序解体的详细位置,并最终找到处理办法。那么有没有什么办法在程序解体的时分得到CallStack呢?答案是肯定的。微软提供了一个DbgHelp.dll,里面包含了一系列的Windows API来供开发者调用。它是一个调试跟踪相关的模块,用于跟踪进程工作,在进程解体时搜集程序产生异常时的堆栈信息,以供开发人员剖析,从而很快找出使程序呈现异常的缘由。
        下面用详细的例子代码来阐明怎样运用DbgHelp.dll中的Windows API来得到CallStack。代码里面有细致的注释来协助了解。
        用VC创立一个名为Test的控制台程序,添加下面的代码。
 
view plain
// 一个有函数调用的类  
//   
class CrashTest  
{  
public:  
    void Test()   
    {   
        Crash();   
    }  
private:  
    void Crash()   
    {   
        // 除零,人为的使程序解体  
        //  
        int i = 13;  
        int j = 0;  
        int m = i / j;  
    }  
};  
int _tmain(int argc, _TCHAR* argv[])  
{  
    CrashTest test;  
    test.Test();  
    return 0;  
}  
        编译上面的代码,到工程的Debug目录下找到编译好的Test.exe,双击运转,程序就会解体。从代码我们晓得函数调用次第是main() -> CrashTest::Test() -> CrashTest::Crash()。那么怎样在程序解体的时分得到CallStack呢?首先,先添加一些工具函数。
 
view plain
#include <Windows.h>  
#include <DbgHelp.h>  
#include   
#include   
// 添加对dbghelp.lib的编译依赖  
//  
#pragma comment(lib, "dbghelp.lib")  
using namespace std;  
const int MAX_ADDRESS_LENGTH = 32;  
const int MAX_NAME_LENGTH = 1024;  
// 解体信息  
//   
struct CrashInfo  
{  
    CHAR ErrorCode[MAX_ADDRESS_LENGTH];  
    CHAR Address[MAX_ADDRESS_LENGTH];  
    CHAR Flags[MAX_ADDRESS_LENGTH];  
};  
// CallStack信息  
//   
struct CallStackInfo  
{  
    CHAR ModuleName[MAX_NAME_LENGTH];  
    CHAR MethodName[MAX_NAME_LENGTH];  
    CHAR FileName[MAX_NAME_LENGTH];  
    CHAR LineNumber[MAX_NAME_LENGTH];  
};  
// 平安拷贝字符串函数  
//  
void SafeStrCpy(char* szDest, size_t nMaxDestSize, const char* szSrc)  
{  
    if (nMaxDestSize <= 0) return;  
    if (strlen(szSrc) < nMaxDestSize)  
    {  
        strcpy_s(szDest, nMaxDestSize, szSrc);  
    }  
    else  
    {  
        strncpy_s(szDest, nMaxDestSize, szSrc, nMaxDestSize);  
        szDest[nMaxDestSize-1] = '\0';  
    }  
}    
// 得到游戏开发运营程序解体信息  
//  
CrashInfo GetCrashInfo(const EXCEPTION_RECORD *pRecord)  
{  
    CrashInfo crashinfo;  
    SafeStrCpy(crashinfo.Address, MAX_ADDRESS_LENGTH, "N/A");  
    SafeStrCpy(crashinfo.ErrorCode, MAX_ADDRESS_LENGTH, "N/A");  
    SafeStrCpy(crashinfo.Flags, MAX_ADDRESS_LENGTH, "N/A");  
    sprintf_s(crashinfo.Address, "%08X", pRecord->ExceptionAddress);  
    sprintf_s(crashinfo.ErrorCode, "%08X", pRecord->ExceptionCode);  
    sprintf_s(crashinfo.Flags, "%08X", pRecord->ExceptionFlags);  
    return crashinfo;  
}  
// 得到CallStack信息  
//  
vector GetCallStack(const CONTEXT *pContext)  
{  
    HANDLE hProcess = GetCurrentProcess();  
    SymInitialize(hProcess, NULL, TRUE);  
    vector arrCallStackInfo;  
    CONTEXT c = *pContext;  
    STACKFRAME64 sf;  
    memset(&sf, 0, sizeof(STACKFRAME64));  
    DWORD dwImageType = IMAGE_FILE_MACHINE_I386;  
    // 不同的CPU类型,详细信息可查询MSDN  
    //  
#ifdef _M_IX86  
    sf.AddrPC.Offset = c.Eip;  
    sf.AddrPC.Mode = AddrModeFlat;  
    sf.AddrStack.Offset = c.Esp;  
    sf.AddrStack.Mode = AddrModeFlat;  
    sf.AddrFrame.Offset = c.Ebp;  
    sf.AddrFrame.Mode = AddrModeFlat;  
#elif _M_X64  
    dwImageType = IMAGE_FILE_MACHINE_AMD64;  
    sf.AddrPC.Offset = c.Rip;  
    sf.AddrPC.Mode = AddrModeFlat;  
    sf.AddrFrame.Offset = c.Rsp;  
    sf.AddrFrame.Mode = AddrModeFlat;  
    sf.AddrStack.Offset = c.Rsp;  
    sf.AddrStack.Mode = AddrModeFlat;  
#elif _M_IA64  
    dwImageType = IMAGE_FILE_MACHINE_IA64;  
    sf.AddrPC.Offset = c.StIIP;  
    sf.AddrPC.Mode = AddrModeFlat;  
    sf.AddrFrame.Offset = c.IntSp;  
    sf.AddrFrame.Mode = AddrModeFlat;  
    sf.AddrBStore.Offset = c.RsBSP;  
    sf.AddrBStore.Mode = AddrModeFlat;  
    sf.AddrStack.Offset = c.IntSp;  
    sf.AddrStack.Mode = AddrModeFlat;  
#else  
    #error "Platform not supported!"  
#endif  
    HANDLE hThread = GetCurrentThread();  
    while (true)  
    {  
        // 该函数是完成这个功用的最重要的一个函数  
        // 函数的用法以及参数和返回值的详细解释能够查询MSDN  
        //  
        if (!StackWalk64(dwImageType, hProcess, hThread, &sf, &c, NULL, SymFunctionTableAccess64, SymGetModuleBase64, NULL))  
        {  
            break;  
        }  
        if (sf.AddrFrame.Offset == 0)  
        {  
            break;  
        }  
        CallStackInfo callstackinfo;  
        SafeStrCpy(callstackinfo.MethodName, MAX_NAME_LENGTH, "N/A");  
        SafeStrCpy(callstackinfo.FileName, MAX_NAME_LENGTH, "N/A");  
        SafeStrCpy(callstackinfo.ModuleName, MAX_NAME_LENGTH, "N/A");  
        SafeStrCpy(callstackinfo.LineNumber, MAX_NAME_LENGTH, "N/A");  
        BYTE symbolBuffer[sizeof(IMAGEHLP_SYMBOL64) + MAX_NAME_LENGTH];  
        IMAGEHLP_SYMBOL64 *pSymbol = (IMAGEHLP_SYMBOL64*)symbolBuffer;  
        memset(pSymbol, 0, sizeof(IMAGEHLP_SYMBOL64) + MAX_NAME_LENGTH);  
        pSymbol->SizeOfStruct = sizeof(symbolBuffer);  
        pSymbol->MaxNameLength = MAX_NAME_LENGTH;  
        DWORD symDisplacement = 0;  
        // 得到函数名  
        //  
        if (SymGetSymFromAddr64(hProcess, sf.AddrPC.Offset, NULL, pSymbol))  
        {  
            SafeStrCpy(callstackinfo.MethodName, MAX_NAME_LENGTH, pSymbol->Name);  
        }  
        IMAGEHLP_LINE64 lineInfo;  
        memset(&lineInfo, 0, sizeof(IMAGEHLP_LINE64));  
        lineInfo.SizeOfStruct = sizeof(IMAGEHLP_LINE64);  
        DWORD dwLineDisplacement;  
        // 得到文件名和所在的代码行  
        //  
        if (SymGetLineFromAddr64(hProcess, sf.AddrPC.Offset, &dwLineDisplacement, &lineInfo))  
        {  
            SafeStrCpy(callstackinfo.FileName, MAX_NAME_LENGTH, lineInfo.FileName);  
            sprintf_s(callstackinfo.LineNumber, "%d", lineInfo.LineNumber);  
        }  
        IMAGEHLP_MODULE64 moduleInfo;  
        memset(&moduleInfo, 0, sizeof(IMAGEHLP_MODULE64));  
        moduleInfo.SizeOfStruct = sizeof(IMAGEHLP_MODULE64);  
        // 得到模块名  
        //  
        if (SymGetModuleInfo64(hProcess, sf.AddrPC.Offset, &moduleInfo))  
        {  
            SafeStrCpy(callstackinfo.ModuleName, MAX_NAME_LENGTH, moduleInfo.ModuleName);  
        }  
        arrCallStackInfo.push_back(callstackinfo);  
    }  
    SymCleanup(hProcess);  
    return arrCallStackInfo;  
}  
        然后,就能够用《让程序在解体时面子的退出之Unhandled Exception》中提供的捕捉程序解体事情的办法添加一个回调函数,在这个函数里面调用上面的函数来得到程序解体时的CallStack。
 
view plain
// 处置Unhandled Exception的回调函数  
//  
LONG ApplicationCrashHandler(EXCEPTION_POINTERS *pException)  
{     
    // 确保有足够的栈空间  
    //  
#ifdef _M_IX86  
    if (pException->ExceptionRecord->ExceptionCode == EXCEPTION_STACK_OVERFLOW)  
    {  
        static char TempStack[1024 * 128];  
        __asm mov eax,offset TempStack[1024 * 128];  
        __asm mov esp,eax;  
    }  
#endif    
    CrashInfo crashinfo = GetCrashInfo(pException->ExceptionRecord);  
    // 输出Crash信息  
    //  
    cout << "ErrorCode: " << crashinfo.ErrorCode << endl;  
    cout << "Address: " << crashinfo.Address << endl;  
    cout << "Flags: " << crashinfo.Flags << endl;  
    vector arrCallStackInfo = GetCallStack(pException->ContextRecord);  
    // 输出CallStack  
    //  
    cout << "CallStack: " << endl;  
    for (vector::iterator i = arrCallStackInfo.begin(); i != arrCallStackInfo.end(); ++i)  
    {  
        CallStackInfo callstackinfo = (*i);  
        cout << callstackinfo.MethodName << "() : [" << callstackinfo.ModuleName << "] (File: " << callstackinfo.FileName << " @Line " << callstackinfo.LineNumber << ")" << endl;  
    }  
    // 这里弹出一个错误对话框并退出程序  
    //  
    FatalAppExit(-1,  _T("*** Unhandled Exception! ***"));  
    return EXCEPTION_EXECUTE_HANDLER;  
}  
        最后,在main函数的开头添加下面的代码。
 
view plain
// 设置处置Unhandled Exception的回调函数  
//   
SetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER)ApplicationCrashHandler);   
        编译上面的代码,到工程的Debug目录下找到编译好的Test.exe,双击运转,程序在解体的时分就会输出CallStack。
 
 
 
让程序在解体时面子的退出之Dump文件
 
  在我的那篇《让程序在解体时面子的退出之CallStack》中提供了一个在程序解体时得到CallStack的办法。可是要想得到CallStack,必需有pdb文件的支持。但是普通状况下,发布进来的程序都是Release版本的,都不会附带pdb文件。那么我们怎样能在程序解体的时分找到出错的详细位置呢?这个时分就该Dump文件出场了!Dump文件是进程的内存镜像,能够把程序运转时的状态完好的保管下来。 
        要想在程序解体的时分创立Dump文件,就需求用到DbgHelp.dll中Windows API的MiniDumpWriteDump()函数。该函数声明如下:
 
view plain
BOOL WINAPI MiniDumpWriteDump(  
  __in  HANDLE hProcess,  
  __in  DWORD ProcessId,  
  __in  HANDLE hFile,  
  __in  MINIDUMP_TYPE DumpType,  
  __in  PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam,  
  __in  PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam,  
  __in  PMINIDUMP_CALLBACK_INFORMATION CallbackParam  
);  
        详细的参数和返回值的解释能够查找MSDN,有很细致的阐明。下面仍然用上一篇文章中的例子代码来阐明怎样在程序解体的时分创立Dump文件。
 
view plain
// 处置Unhandled Exception的回调函数  
//  
LONG ApplicationCrashHandler(EXCEPTION_POINTERS *pException)  
{     
    // 这里弹出一个错误对话框并退出程序  
    //  
    FatalAppExit(-1,  _T("*** Unhandled Exception! ***"));  
    return EXCEPTION_EXECUTE_HANDLER;  
}  
// 一个有函数调用的类  
//   
class CrashTest  
{  
public:  
    void Test()   
    {   
        Crash();   
    }  
private:  
    void Crash()   
    {   
        // 除零,人为的使程序解体  
        //  
        int i = 13;  
        int j = 0;  
        int m = i / j;  
    }  
};  
int _tmain(int argc, _TCHAR* argv[])  
{  
    // 设置处置Unhandled Exception的回调函数  
    //   
    SetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER)ApplicationCrashHandler);   
    CrashTest test;  
    test.Test();  
    return 0;  
}  
        在上面的程序解体的时分,会调用函数ApplicationCrashHandler()。创立Dump文件的代码就需求添加到该函数中。下面就是一个创立Dump文件的函数。
 
view plain
// 创立Dump文件  
//   
void CreateDumpFile(LPCWSTR lpstrDumpFilePathName, EXCEPTION_POINTERS *pException)  
{  
    // 创立Dump文件  
    //  
    HANDLE hDumpFile = CreateFile(lpstrDumpFilePathName, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);  
    // Dump信息  
    //  
    MINIDUMP_EXCEPTION_INFORMATION dumpInfo;  
    dumpInfo.ExceptionPointers = pException;  
    dumpInfo.ThreadId = GetCurrentThreadId();  
    dumpInfo.ClientPointers = TRUE;  
    // 写入Dump文件内容  
    //  
    MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), hDumpFile, MiniDumpNormal, &dumpInfo, NULL, NULL);  
    CloseHandle(hDumpFile);  
}  
        在函数ApplicationCrashHandler()用相似下面的代码来调用上面的函数,就能够在程序解体的时分创立Dump文件。
 
view plain
CreateDumpFile(_T("C:\\Test.dmp"), pException);  
        下面简单说一下Dump文件的用法。将Dump文件拷贝到含有应用程序和对应的pdb文件的目录,在VS里面翻开Dump文件(或者直接双击Dump文件),VS会自动创立一个Solution,直接调试运转,代码就会停到使程序解体的那一行上。就跟在VS里面调试代码一摸一样。(VS2008)
 
 
        在VS2010里翻开Dump文件,会显现一个Minidump File Summary,并且能够停止下面图中的操作。
 
 
 
 
让程序在解体时面子的退出之SEH+Dump文件
在我上篇文章《让程序在解体时面子的退出之SEH》中解说了SEH中try/except能够捕捉异常,防止程序的解体,并且能够在处置完异常之后,还能决议进该进程如何执行。关于应用程序的运用者来说,并不晓得异常的发作。但是关于软件的开发者来说,固然防止了程序的解体,可是这样能够让程序解体的缺陷存在于代码中,就像一个定时炸弹,不晓得什么时分会爆炸。要想修复这样的缺陷,首先要找到招致程序解体的那行代码。而我在我的那篇《让程序在解体时面子的退出之Dump文件》里面引见了如何用Dump文件来定位使程序解体的代码。这里仍然能够用同样的办法。下面就是创立Dump文件的函数。
 
view plain
// 创立Dump文件  
//   
void CreateDumpFile(LPCWSTR lpstrDumpFilePathName, EXCEPTION_POINTERS *pException)  
{  
    // 创立Dump文件  
    //  
    HANDLE hDumpFile = CreateFile(lpstrDumpFilePathName, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);  
    // Dump信息  
    //  
    MINIDUMP_EXCEPTION_INFORMATION dumpInfo;  
    dumpInfo.ExceptionPointers = pException;  
    dumpInfo.ThreadId = GetCurrentThreadId();  
    dumpInfo.ClientPointers = TRUE;  
    // 写入Dump文件内容  
    //  
    MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), hDumpFile, MiniDumpNormal, &dumpInfo, NULL, NULL);  
    CloseHandle(hDumpFile);  
}  
       从上面的代码中能够看出,要想创立Dump文件,必需得到一个指向EXCEPTION_POINTERS构造的指针。怎样在try/except块中得到这个指针呢?这个时分就需求用到Windows API中的GetExceptionInformation()。这个函数的返回值就是一个指向EXCEPTION_POINTERS构造的指针。下面是详细的代码。
 
view plain
// 作为except块中表达式的函数  
//  
LONG CrashHandler(EXCEPTION_POINTERS *pException)  
{     
    // 在这里添加处置程序解体状况的代码  
    //  
    // 这里以弹出一个对话框为例子  
    //  
    MessageBox(NULL, _T("Message from Catch handler"), _T("Test"), MB_OK);  
    // 创立Dump文件  
    //  
    CreateDumpFile(_T("C:\\Test.dmp"), pException);  
    return EXCEPTION_EXECUTE_HANDLER;  
}  
int _tmain(int argc, _TCHAR* argv[])  
{  
    __try  
    {  
        MessageBox(NULL, _T("Message from '__try' section"), _T("Test"), MB_OK);  
        // 除零,人为的使程序解体  
        //  
        int i = 13;  
        int j = 0;  
        int m = i / j;  
    }  
    // 捕捉到让程序解体的异常时创立Dump文件  
    //  
    __except(CrashHandler(GetExceptionInformation()))  
    {  
        // 这里以弹出一个对话框为例子  
        //  
        MessageBox(NULL, _T("Message from '__except' section"), _T("Test"), MB_OK);  
    }  
    MessageBox(NULL, _T("Funcation completed"), _T("Test"), MB_OK);  
    return 0;  
}  
        编译上面的代码并运转,会依次弹出下面这些对话框,并在C盘创立一个Dump文件Test.dmp。
 
 
 
        有了Dump文件,就能够轻松定位使程序解体的那行代码,详细办法可参考我的《让程序在解体时面子的退出之Dump文件》。
 
让程序在解体时面子的退出之终极处理计划(SEH+Dump+Unhandled Exception Filter)
 
在我的上篇文章《让程序在解体时面子的退出之SEH+Dump文件》我引见了怎样用SEH加上Dump文件来防止程序的解体并在程序解体时创立Dump文件来协助定位呈现异常的代码行。可是只要try/except块中try块中的代码呈现异常才干被捕捉到,try块外面的代码呈现异常,程序照样会解体。
        下面用《让程序在解体时面子的退出之SEH+Dump文件》文中的代码为例子来阐明。
 
view plain
// 创立Dump文件  
//   
void CreateDumpFile(LPCWSTR lpstrDumpFilePathName, EXCEPTION_POINTERS *pException)  
{  
    // 创立Dump文件  
    //  
    HANDLE hDumpFile = CreateFile(lpstrDumpFilePathName, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);  
    // Dump信息  
    //  
    MINIDUMP_EXCEPTION_INFORMATION dumpInfo;  
    dumpInfo.ExceptionPointers = pException;  
    dumpInfo.ThreadId = GetCurrentThreadId();  
    dumpInfo.ClientPointers = TRUE;  
    // 写入Dump文件内容  
    //  
    MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), hDumpFile, MiniDumpNormal, &dumpInfo, NULL, NULL);  
    CloseHandle(hDumpFile);  
}  
// 作为except块中表达式的函数  
//  
LONG CrashHandler(EXCEPTION_POINTERS *pException)  
{     
    // 在这里添加处置程序解体状况的代码  
    //  
    // 这里以弹出一个对话框为例子  
    //  
    MessageBox(NULL, _T("Message from Catch handler"), _T("Test"), MB_OK);  
    // 创立Dump文件  
    //  
    CreateDumpFile(_T("C:\\Test.dmp"), pException);  
    return EXCEPTION_EXECUTE_HANDLER;  
}  
int _tmain(int argc, _TCHAR* argv[])  
{  
    __try  
    {  
        MessageBox(NULL, _T("Message from '__try' section"), _T("Test"), MB_OK);  
        // 除零,人为的使程序解体  
        //  
        int i = 13;  
        int j = 0;  
        int m = i / j;  
    }  
    // 捕捉到让程序解体的异常时创立Dump文件  
    //  
    __except(CrashHandler(GetExceptionInformation()))  
    {  
        // 这里以弹出一个对话框为例子  
        //  
        MessageBox(NULL, _T("Message from '__except' section"), _T("Test"), MB_OK);  
    }  
    MessageBox(NULL, _T("Funcation completed"), _T("Test"), MB_OK);  
    return 0;  
}  
        编译上面的代码并运转,会依次弹出下面这些对话框,并在C盘创立一个Dump文件Test.dmp。
 
 
 
        假如把上面代码中的main()函数改成下面的样子,运转编译后的程序仍然会解体。
 
view plain
int _tmain(int argc, _TCHAR* argv[])  
{  
    __try  
    {  
        MessageBox(NULL, _T("Message from '__try' section"), _T("Test"), MB_OK);  
        // 除零,人为的使程序解体  
        //  
        int i = 13;  
        int j = 0;  
        int m = i / j;  
    }  
    // 捕捉到让程序解体的异常时创立Dump文件  
    //  
    __except(CrashHandler(GetExceptionInformation()))  
    {  
        // 这里以弹出一个对话框为例子  
        //  
        MessageBox(NULL, _T("Message from '__except' section"), _T("Test"), MB_OK);  
    }  
    // 除零,人为的使程序解体  
    //  
    int i = 13;  
    int j = 0;  
    int m = i / j;  
    MessageBox(NULL, _T("Funcation completed"), _T("Test"), MB_OK);  
    return 0;  
}  
        这种状况在实践编程中是很有可能呈现的,毕竟我们不可能事前估计到一切可能呈现招致程序解体的状况,并把这些代码放到try/except块中。那么关于这些不可预知的异常该怎样办呢?这就要用到我那篇《让程序在解体时面子的退出之Unhandled Exception》中的办法:用Windows API中的SetUnhandledExceptionFilter设置一个回调函数来处置这些无法意料的异常。下面是在上面的例子代码上修正后的代码。其中函数CreateDumpFile没有任何变化。
 
view plain
// 得到当前时间  
//  
wstring GetPresentTime()  
{  
     SYSTEMTIME time;  
     GetLocalTime(&time);   
     wchar_t wszTime[128];  
     swprintf_s(wszTime, _T("%04d-%02d-%02d %02d-%02d-%02d-%03d"), time.wYear, time.wMonth, time.wDay, time.wHour, time.wMinute, time.wSecond, time.wMilliseconds);  
     return wstring(wszTime);  
}  
// 处置异常的回调函数  
//  
LONG CrashHandler(EXCEPTION_POINTERS *pException)  
{     
    // 在这里添加处置程序解体状况的代码  
    //  
    // 这里以弹出一个对话框为例子  
    //  
    MessageBox(NULL, _T("Message from Catch handler"), _T("Test"), MB_OK);  
    // 以当前时间为文件名  
    //  
    wstring strDumpFileName = _T("C:\\") + GetPresentTime() +_T(".dmp");  
    // 创立Dump文件  
    //  
    CreateDumpFile(strDumpFileName.data(), pException);  
    return EXCEPTION_EXECUTE_HANDLER;  
}  
int _tmain(int argc, _TCHAR* argv[])  
{  
    // 设置处置Unhandled Exception的回调函数  
    //   
    SetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER)CrashHandler);   
    __try  
    {  
        MessageBox(NULL, _T("Message from '__try' section"), _T("Test"), MB_OK);  
        // 除零,人为的使程序解体  
        //  
        int i = 13;  
        int j = 0;  
        int m = i / j;  
    }  
    __except(CrashHandler(GetExceptionInformation()))  
    {  
        // 在这里添加处置程序解体状况的代码  
        //  
        // 这里以弹出一个对话框为例子  
        //  
        MessageBox(NULL, _T("Message from '__except' section"), _T("Test"), MB_OK);  
    }  
    // 除零,人为的使程序解体  
    //  
    int i = 13;  
    int j = 0;  
    int m = i / j;  
    MessageBox(NULL, _T("Funcation completed"), _T("Test"), MB_OK);  
    return 0;  
}  
        编译上面的代码,运转生成的EXE文件,能够看到在弹出上面提到的那一系列的对话框后,程序正常退出,没有解体。同时,在C盘下生成了2个Dump文件,文件名指出了发作异常的时辰。
 
        上面的代码仅仅是为了阐明怎样配合运用SEH和SetUnhandledExceptionFilter,所以except后的表达式和SetUnhandledExceptionFilter中所设置的回调函数都运用了同一个函数CrashHandler。在实践的应用中能够依据不同的需求而运用不同的函数。这个函数的参数必需是一个指向EXCEPTION_POINTERS的指针,返回值必需是这3个中的一个:EXCEPTION_CONTINUE_SEARCH,EXCEPTION_CONTINUE_EXECUTION,EXCEPTION_EXECUTE_HANDLER。这3个值的详细含义能够查阅MSDN或者我的那篇《让程序在解体时面子的退出之SEH》。
        实践状况下,是不应该用同一个回调函数的。由于在except表达式中的函数是处置try块中的代码异常的;而用SetUnhandledExceptionFilter设置的回调函数是用来处置代码中没有被捕捉到的异常的。关于未被捕捉到的异常,这个回调函数是不晓得异常发作的中央的,固然能够经过异常代码晓得异常的类型,但是由于不晓得是什么情况惹起的异常,所以没法做出相应的异常处置。普通状况下,这个回调函数是应用程序解体前的最后一道防线,这个函数中的代码被执行完后,应用程序就会被终止。所以,大局部的应用程序,在这个函数里都是弹出一个发送错误报告的对话框,来通知用户程序发作异常,需求终止,能够把错误报告(普通是包括Dump文件和一些必要的文本信息)发送到指定中央协助开发者来修正代码缺陷,以进步软件质量。
 
        运用上面的办法编写出的应用程序不会解体,并且在呈现异常的时分会产生Dump文件。程序的运用者会取得十分良好的用户体验。假如再给应用程序添加上Log信息,配合上Dump文件,就能够很轻松的定位程序中的异常,协助开发者快速的修复代码中的错误。
 

上篇:上一篇:VC中字符串取子串总结
下篇:下一篇:C支持的日常时间格式或类