第一次遇到程序崩溃的问题,以前为单位开发了一个插件程序,在本机运行没有出现问题,但把生成的可执行文件拷贝到服务器上一运行程序,刚进入插件代码,插件服务就崩溃了,当时被这个问题整的很惨,在同事的帮助下了解到,对于程序崩溃,最快的解决方式是生成dump文件,经过生成dump文件使用调试工具进行调试,还原程序崩溃时的状态,可以起到快速定位排查问题的做用。Dump文件是进程的内存镜像。能够把程序的执行状态经过调试器保存到dump文件中。Dump文件是用来给驱动程序编写人员调试驱动程序用的,这种文件必须用专用工具软件打开,好比使用WinDbg、VS打开。由于第一次遇到此类问题,彻底没有头绪,但同事很快经过dump文件很快定位到空指针问题,秉承着遇到的问题在遇到第二次不能再是问题的原则,对dump文件的含义、生成、做用、分析、定位排查的过程进行说明,算是对遇到的程序崩溃的问题总结。
本文档适用于开发人员。php
Windows下Dump文件分为两大类,内核模式Dump和用户模式Dump。内核模式Dump是操做系统建立的崩溃转储,最经典的就是系统蓝屏,这时候会自动建立内核模式的Dump。用户模式Dump进一步能够分为完整Dump(Full Dump)和迷你Dump(Minidump)。完整Dump包含了某个进程完整的地址空间数据,以及许多用于调试的信息,而Minidump则有许多类型,根据须要能够包含不一样的信息,有的可能只包含某个线程和部分模块的信息。在程序开发过程当中出现的应用崩溃属于用户模式Dump。所以,要弄清楚这种Dump文件的组成、生成方式、做用。html
Dump文件是进程的内存镜像,能够把程序的执行状态经过调试器保存到dump文件中。主要是用来在系统中出现异常或者崩溃的时候来生成dump文件,而后用调试器进行调试,这样就能够把生产环境中的dmp文件拷贝到本身的开发机上,调试就能够找到程序出错的位置。
在C++编程实践中,一般都会遇到内存访问无效、无效对象、堆栈溢出、空指针调用等常见的C/C++问题,而这些问题最后常会致使:系统崩溃。为解决崩溃问题经常使用的手段一个就是生成dump文件进行代码调试,另一个就是使用远程调试remote debugger进行调试。但remote debugger在要求程序源代码和可执行文件在同一个局域网内,对环境的要求较高。所以对于程序崩溃较好的解决方式即是生成dump文件进行解析,快速定位到程序崩溃位置,对问题进行排查。在本次插件崩溃的过程当中,程序崩溃的两行代码以下:程序员
NETSDKPLUGIN_TRACE("- CHikNetDevice::SetSipConfig Starts"); std::string ServerIp; int ServerPort; std::string UserName; std::string Password; int enabledAutoLogin; std::string localNo; int loginCycle; DWORD errCode; char szLan[128] = {0}; if (!GetCallParam(*ParamNode, enabledAutoLogin, ServerIp, ServerPort, localNo,loginCycle, Msg)) { NETSDKPLUGIN_ERROR("The parameter passed to config the sip configuration is invalid in CHikNetDevice::SetSipCOnfig"); return DEV_ERR_FAILED; } //%s对应的是char*,若传入了std::string,程序在此崩溃。调用std::string对象的c_str()能够生成对应的const char* NETSDKPLUGIN_DEBUG("- enabledAutoLogin: %d, ServerIp: %s, ServerPort: %d, UserName: %s,Password:*******,localNo: %s, loginCycle: %d", enabledAutoLogin, ServerIp, ServerPort, UserName.c_str(), localNo.c_str(), loginCycle);
在程序运行的过程当中,在插件打印了"- CHikNetDevice::SetSipConfig Starts"以后,程序崩溃,这能够经过日志打印出来,在最下面NETSDKPLUGIN_DEBUG函数中,对应%s,应为C风格字符串指针,而传入的倒是C++ std::string类型的对象,致使了程序崩溃。
以后的崩溃代码以下:数据库
ISpVoice *pVoice = NULL; if (FAILED(::CoInitialize(NULL))) return FALSE; HRESULT hr = CoCreateInstance(CLSID_SpVoice, NULL, CLSCTX_ALL, IID_ISpVoice, (void **)&pVoice); if (SUCCEEDED(hr) && (NULL != pVoice)) { CComPtr <ISpStream> cpWavStream; CComPtr <ISpStreamFormat> cpOldStream; CSpStreamFormat originalFmt; hr = pVoice->GetOutputStream(&cpOldStream); //在没有声卡的状况下pVoice->GetOutputStream()中cpOldStream会生成空指针 //以前的代码并有对hr和cpOldStream进行非空判断,致使了程序在此处发生崩溃,由于生成了空指针 if (FAILED(hr) || NULL == cpOldStream) { TALKCLIENTPLUGIN_ERROR("- GetOutputStream failed, lastError:[%d][%d]", hr, GetLastError()); return FALSE; } originalFmt.AssignFormat(cpOldStream); char SaveName[30]; // 基于当前系统的当前日期/时间 time_t now = time(0); tm *t = localtime(&now); sprintf_s(SaveName, "%d-%d-%d %d-%d-%d.wav", t->tm_year+1900, t->tm_mon+1, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec); strcpy_s(SzFileName, strlen(SaveName)+1, SaveName); hr = SPBindToFile(SaveName, SPFM_CREATE_ALWAYS, &cpWavStream, &originalFmt.FormatId(), originalFmt.WaveFormatExPtr());
上述代码片断蓝色划线处originalFmt.AssignFormat(cpOldStream)的函数体编程
HRESULT AssignFormat(ISpStreamFormat * pStream) { ::CoTaskMemFree(m_pCoMemWaveFormatEx); m_pCoMemWaveFormatEx = NULL; HRESULT hr = pStream->GetFormat(&m_guidFormatId, CoMemWaveFormatEx); if (SUCCEEDED(hr) && m_pCoMemWaveFormatEx) { if (m_pCoMemWaveFormatEx->wFormatTag == WAVE_FORMAT_PCM) { m_pCoMemWaveFormatEx->cbSize = 0; // Always set ze to zero for WAVE_FORMAT_PCM. } if (m_pCoMemWaveFormatEx->nAvgBytesPerSec == 0 || m_pCoMemWaveFormatEx->nBlockAlign == 0 || m_pCoMemWaveFormatEx->nChannels == 0) { Clear(); hr = E_INVALIDARG; } } return hr; }
用了pStream->GetFormat()函数,而在以前,若声卡被禁用或者在远程桌面未设置下图,则pStream为空指针,而在使用以前并无进行cpOldStream非空判断,所以程序崩溃,致使程序出现了崩溃。
而星辰在定位这个问题时在main函数中插入了dump文件生成的控制代码,很快便定位到了该空指针异常。windows
程序在运行时,不免会有一些异常状况发生,特别是在条件不允许去挂调试器的时候,如何快速的定位错误的方法就显得很重要。
都是一种很重要的定位错误的方法,出得好的日志能够方便程序员快速的定位问题所在。但日志有时也显不足:
日志有时只能定位大致错误范围,却没法确认问题所在,好比程序抓到一个未知的异常。
没有机会来出日志,或者能出日志的时候已经没法得到和错误相关的信息,好比程序崩溃的时候。
日志明显不足的时候,把进程中相关数据DUMP下来分析就是一个比较实用方便的方法。不少应用都会提供这类功能,以便在程序出现问题时能够把相关的数据发给开发者,方便开发者分析问题。相似Office这样的应用都会有这个功能,当应用崩溃时会弹出对话框,提示是否发送错误相关的数据。
因为Dump文件可以保存程序内部的内存、堆栈、句柄、线程等程序运行相关的信息,很是具备重要性,所以了解如何生成Dump文件也是避免茫然无措,不知如何下手场景的途径之一。数组
该方式能够生成.DMP文件,经过打开任务管理器,找到插件服务对应的进程,右击,选择建立转储文件:
.DMP文件的存放位置以下图所示:
生成的转储文件能够经过VS打开,可是正常运行的程序生成.DMP文件并无什么大的做用。上述的方法要求在程序崩溃时并不直接退出时才可使用,通常场景下,程序崩溃比较粗暴,所以可使用下述的方式建立Dump文件服务器
当程序遇到未处理异常(主要指非指针形成)致使程序崩溃死,若是在异常发生以前调用了SetUnhandledExceptionFilter()函数,异常交给函数处理。MSDN中描述为:
Issuing SetUnhandledExceptionFilter replaces the existing top-level exception filter for all existing and all future threads in the calling process.
于是,在程序开始处增长SetUnhandledExceptionFilter()函数,并在函数中利用适当的方法生成Dump文件,便可实现须要的功能。
在编程过程当中,能够预期的异常都经过结构化异常(try/catch)进行了处理。此时,若是发生了未预期的异常,这些异常处理代码没法处理,则转由Windows提供的默认异常处理器来进行处理,这个特殊的异常处理函数为UnhandledExceptionFilter。该函数会显示一个消息框,提示发生了未处理的异常,同时,让用户选择结束或调试该进程。也就是以下界面:
所以,为了更友好的处理未预期的异常(主要是建立内存转储),能够覆盖默认的异常处理操做。这是经过函数SetUnhandledExceptionFilter完成的,函数原型以下:markdown
LPTOP_LEVEL_EXCEPTION_FILTER WINAPI SetUnhandledExceptionFilter( _In_ LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter
lpTopLevelExceptionFilter即异常处理函数指针,若是设置为NULL,则默认使用UnhandledExceptionFilter。所以咱们能够对照lpTopLevelExceptionFilter自定义一个异常处理函数。咱们须要建立内存转储。这经过函数MiniDumpWriteDump来实现。
下述代码是一个经过MiniDumpWriteDump函数来实现转储文件建立架构
LONG WINAPI MyUnhandledExceptionFilter( struct _EXCEPTION_POINTERS *ExceptionInfo ) { HANDLE hFile = CreateFile("mini.dmp", GENERIC_READ|GENERIC_WRITE, FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if( hFile == INVALID_HANDLE_VALUE ) return EXCEPTION_EXECUTE_HANDLER; MINIDUMP_EXCEPTION_INFORMATION mdei; mdei.ThreadId = GetCurrentThreadId(); mdei.ExceptionPointers = ExceptionInfo; mdei.ClientPointers = NULL; MINIDUMP_CALLBACK_INFORMATION mci; mci.CallbackRoutine = NULL; mci.CallbackParam = 0; MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), hFile, MiniDumpNormal, &mdei, NULL, &mci); CloseHandle(hFile); AfxMessageBox("已成功建立崩溃转储!"); return EXCEPTION_EXECUTE_HANDLER; }
本机调试代码,出现异常时出现的弹窗即UnhandledExceptionFilter为默认的异常处理器工做产生的,此时能够点击中断或者继续,而在对应的右下方能够看到调用堆栈,对于咱们排查定位很是有帮助。
修改注册码的方式,没有使用过,但经过查询网上的材料,总结以下:
Win + R 输入regedit打开注册表
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\WindowsError Reporting\LocalDumps
在该栏目下添加项如图所示:
这样能够保证假若程序故障后自行退出,则此方法就难以应用。不过,咱们能够在注册表中添加以下信息已确保系统在程序崩溃后自行保存一个dump文件。
单位对minidump文件生成方式经过编程进行了封装。头文件定义以下:
enum CRASHAPI_DUMP_TYPE { MiniDumpType = 0x00000000,/*MiniDumpNormal*/ FullDumpType = 0x00009b67 /*Full*/ }; /************************************************************************** * Function: CrashAPI_Init * Description: Init the Crash API lib.Call this function as early in the start-up process as possible. * Input: (null) * Output: (null) * Return: returns true on success. **************************************************************************/ CRASH_EXTERN bool CRASH_API CrashAPI_Init(); /************************************************************************** * Function: CrashAPI_Uninit * Description: uninstall the library * Input: (null) * Output: (null) * Return: none **************************************************************************/ CRASH_EXTERN void CRASH_API CrashAPI_Uninit(); /************************************************************************** * Function: CrashAPI_SetDumpPath * Description: set the minidump file path.the file will be generated in the current dictionary if you don't set it. * Input: dump_path the path of the * Output: (null) * Return: returns true on success. **************************************************************************/ CRASH_EXTERN bool CRASH_API CrashAPI_SetDumpPath(char * dump_path); /************************************************************************** * Function: CrashAPI_SetDumpType * Description: set the minidump file type.the file will be MiniDumpNormal if you don't set it. * Input: dump_type of MINIDUMP_TYPE * Output: (null) * Return: returns true on success. **************************************************************************/ CRASH_EXTERN bool CRASH_API CrashAPI_SetDumpType(CRASHAPI_DUMP_TYPE dump_type); /************************************************************************** * Function: CrashAPI_WriteMinidump * Description: writes a minidump immediately.it can be used to * capture the execution state independently of a crash. * Input: (null) * Output: (null) * Return: returns true on success. **************************************************************************/ CRASH_EXTERN bool CRASH_API CrashAPI_WriteMinidump(); /************************************************************************** * Function: CrashAPI_SetCallBack * Description: Set the call back function which will be called when the crash occurs. * Input: (null) * Output: (null) * Return: returns true on success. **************************************************************************/ CRASH_EXTERN bool CRASH_API CrashAPI_SetCallBack(CrashCallback callback);
在插件程序的main函数中插入以下代码行,便可在插件崩溃时自动生成dump文件。
#include "stdafx.h" #include "Socket\CompleteSocket.h" #include "DeviceManager.h" #include <signal.h> #include "CrashAPI.h" #pragma comment(lib, "CompleteSocket_md.lib") #pragma comment(lib, "CrashAPI.lib") int _tmain(int argc, _TCHAR* argv[]) { … CrashAPI_Init(); CrashAPI_SetDumpType(FullDumpType); hlog_init("DA"); … hlog_fini(); CrashAPI_Uninit(); … return ret; }
在程序的main函数中添加头文件中对应的CrashAPI_Init、CrashAPI_Uninit,而且设置生成dump文件的类型。在代码中设置生成的dump文件类型为FullDumpType。_tmain()函数所在的模块是所写插件的调用层DeviceAccess。由于添加了”CrashAPI.h”头文件,同时#pragma comment(lib," CrashAPI.lib ")表示连接CrashAPI.lib这个库。 和在工程设置里写上链入CrashAPI.lib的效果同样,不过这种方法写的 程序别人在使用你的代码的时候就不用再设置工程settings了。
想要使得这些崩溃的dump文件生成的代码生效,还须要把CrashAPI.lib对应的DLL文件拷贝到DeviceAccess的Release目录下,该目录存放了可执行文件和依赖的DLL,发布程序时把包含该CrashAPI.dll在内的Release同时发布到服务器上。以服务的方式启动便可。
这样在程序崩溃时,程序自动生成dump文件。若dmp文件是exe在另外一台机器上产生的,则咱们最好把exe,pdb,dmp放到同一文件夹下,必须保证pdb与出问题的exe是同一时间生成的,用VS打开dmp文件后还须要设置符号表文件路径和源代码路径,必须保证.exe,pdb,dmp是同一时间产生的,则直接点击调试便可直接进入程序中断,这样经过查看调用堆栈,便可快速定位问题。
插件程序的架构以下,我负责开发的模块对接设备,即经过调用SDK调用报警盒子和中心管理机进行呼叫、广播、挂断,这是经过代理实现的。DeviceAccess包含main函数,其主要是进行接收socket数据报,而后对数据报进行解析。HikTalkClientPlugin是开发的代理设备接口,该项目导出HikTalkClientPlugin.dll供DeviceAccess进行调用,包括链接、断开链接、调用广播呼叫等功能。集成到服务器上时,以服务的形式运行了DeviceAccess,由其接收socket数据报,并经过对接对讲平台进行设备功能的调用。
首先编译运行HikTalkClientPlugin生成对应的HikTalkClientPlugin.dll和HikTalkClientPlugin.pdb。而后把生成的HikTalkClientPlugin.dll和HikTalkClientPlugin.pdb复制到DeviceAccess项目的Release的指定目录下(由于DeviceAccess的可执行程序要调用HikTalkClientPlugin.dll嘛,因此确定要和DeviceAccess一块儿发布的)。
以Release模式编译并运行DeviceAccess项目,能够在输出目录中生成可执行文件,截图以下:
压缩成rar并拷贝到服务器上以服务的方式启动,让前段发出设备操做请求,安静等待程序崩溃。和预期同样,程序崩溃,生成了dump文件。如图所示:
把在服务器端生成的.dmp文件拷贝到开发机DeviceInterfaceAgent.exe对应的位置,在该目录须要有DeviceInterfaceAgent.pdb。pdb文件,是VS生成的用于调试的符号文件(program database),保存着调试的信息。在VS的工程属性,C/C++,调试信息格式,设置/Zi,那么VS就会在构建项目时建立PDB文件。须要保证源代码、pdb文件、可执行文件是与服务器上相同的版本,这样才能够进行正常的调试。
使用VS打开.dmp文件进行调试,会发现程序直接在程序崩溃处停了下来。此时,查看调用堆栈信息[若没有,点击Alt + 7便可出现]。经过查看调用堆栈便可快速定位。
经过调用堆栈定位排查问题,能够看到如2.2中第二个崩溃缘由是空指针异常,所以找到空指针出现的位置,并在经过函数为指针赋值以后添加空指针判断和操做成功的判断。发现这是HikTalkClientPlugin插件的问题,所以修改HikTalkClientPlugin的源代码,从新编译生成DLL,并把HikTalkClientPlugin.dll和HikTalkClientPlugin.pdb文件拷贝到服务器上Release/hplugin/ HikTalkClientPlugin目录内,由于DeviceInterfaceAgent并无修改源代码,无需变更。从新启动服务接收socket请求。
以一样的方式定位注册时插件崩溃的问题,修改HikNetSdkClientPlugin源代码,从新编译HikNetSdkClientPlugin,生成HikNetSdkClientPlugin.pdb和HikNetSdkClientPlugin.dll,把该两个文件拷贝至服务器Release/hplugin/HikNetSdkClientPlugin内,从新启动服务,插件再也不崩溃。问题获得解决。
以VS为例,在开发机上开发代码时,若是程序崩溃而且崩溃时并非直接退出,那么点击中断以后的界面即为调试Dump文件的情景。所以,dump文件对于这种本地开发或许做用并不大,可是若是程序在服务器端崩溃,那么此时生成的Dump文件并不是常重要,它能够避免你在一个较大的项目代码面前茫然无措。但下面的代码片断均为本地机上的代码,而且异常也是刻意为之,只是为了演示dmp文件的生成和调试。其中的代码均为在网上搜索到,仅用来演示使用。
添加头文件CCreateDump.h,代码片断以下:
#pragma once #include <string> using namespace std; class CCreateDump { public: CCreateDump(); ~CCreateDump(void); static CCreateDump* Instance(); static long __stdcall UnhandleExceptionFilter(_EXCEPTION_POINTERS* ExceptionInfo); //声明Dump文件,异常时会自动生成。会自动加入.dmp文件名后缀 void DeclarDumpFile(std::string dmpFileName = ""); private: static std::string strDumpFile; static CCreateDump* __instance; };
添加CcreateDump.cpp,代码片断以下:
#include <Windows.h> #include "CCreateDump.h" #include <DbgHelp.h> #pragma comment(lib, "dbghelp.lib") CCreateDump* CCreateDump::__instance = NULL; std::string CCreateDump::strDumpFile = ""; CCreateDump::CCreateDump() { } CCreateDump::~CCreateDump(void) { } long CCreateDump::UnhandleExceptionFilter(_EXCEPTION_POINTERS* ExceptionInfo) { HANDLE hFile = CreateFile(strDumpFile.c_str(), GENERIC_WRITE, FILE_SHARE_WRITE, NULL, CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL, NULL); if(hFile!=INVALID_HANDLE_VALUE) { MINIDUMP_EXCEPTION_INFORMATION ExInfo; ExInfo.ThreadId = ::GetCurrentThreadId(); ExInfo.ExceptionPointers = ExceptionInfo; ExInfo.ClientPointers = FALSE; // write the dump BOOL bOK = MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), hFile, MiniDumpNormal, &ExInfo, NULL, NULL ); CloseHandle(hFile); if (!bOK) { DWORD dw = GetLastError(); //写dump文件出错处理,异常交给windows处理 return EXCEPTION_CONTINUE_SEARCH; } else { //在异常处结束 return EXCEPTION_EXECUTE_HANDLER; } } else { return EXCEPTION_CONTINUE_SEARCH; } } void CCreateDump::DeclarDumpFile(std::string dmpFileName) { SYSTEMTIME syt; GetLocalTime(&syt); char c[MAX_PATH]; sprintf_s(c,MAX_PATH,"[%04d-%02d-%02d %02d:%02d:%02d]",syt.wYear,syt.wMonth,syt.wDay,syt.wHour,syt.wMinute,syt.wSecond); strDumpFile = std::string(c); if (!dmpFileName.empty()) { strDumpFile += dmpFileName; } strDumpFile += std::string(".dmp"); SetUnhandledExceptionFilter(UnhandleExceptionFilter); } CCreateDump* CCreateDump::Instance() { if (__instance == NULL) { __instance = new CCreateDump; } return __instance; }
添加测试程序Test.cpp,代码片断以下:
#include <Windows.h> #include "CCreateDump.h" int main(void) { CCreateDump::Instance()->DeclarDumpFile("dumpfile"); int *p = NULL; *p =5; return 0; }
能够清楚的看到,在测试程序中使用了空指针,即对空指针解引用,并对其进行赋值操做,违法操做。
在VS中进行以下的配置:
属性–>连接器—>调试–>生成调试信息–>是。
属性–>配置属性–>常规–>字符集–>使用多字节字符集
点击调试–>开始执行(不调试)–>查看运行结果
注意:若是点击了调试–>启动调试,程序直接崩溃,但没有退出,程序所呈现的界面即为使用dmp文件调试bug的界面。能够在项目所在目录下看到dmp文件已经生成,以下图所示:
添加代码片断minidump.h以下:
#pragma once #include <windows.h> #include <imagehlp.h> #include <cstdlib> #include <tchar.h> #pragma comment(lib, "dbghelp.lib") inline BOOL IsDataSectionNeeded(const WCHAR* pModuleName) { if(pModuleName == 0) { return FALSE; } WCHAR szFileName[_MAX_FNAME] = L""; _wsplitpath(pModuleName, NULL, NULL, szFileName, NULL); if(wcsicmp(szFileName, L"ntdll") == 0) return TRUE; return FALSE; } inline BOOL CALLBACK MiniDumpCallback(PVOID pParam, const PMINIDUMP_CALLBACK_INPUT pInput, PMINIDUMP_CALLBACK_OUTPUT pOutput) { if(pInput == 0 || pOutput == 0) return FALSE; switch(pInput->CallbackType) { case ModuleCallback: if(pOutput->ModuleWriteFlags & ModuleWriteDataSeg) if(!IsDataSectionNeeded(pInput->Module.FullPath)) pOutput->ModuleWriteFlags &= (~ModuleWriteDataSeg); case IncludeModuleCallback: case IncludeThreadCallback: case ThreadCallback: case ThreadExCallback: return TRUE; default:; } return FALSE; } //建立Dump文件 inline void CreateMiniDump(EXCEPTION_POINTERS* pep, LPCTSTR strFileName) { HANDLE hFile = CreateFile(strFileName, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if((hFile != NULL) && (hFile != INVALID_HANDLE_VALUE)) { MINIDUMP_EXCEPTION_INFORMATION mdei; mdei.ThreadId = GetCurrentThreadId(); mdei.ExceptionPointers = pep; mdei.ClientPointers = FALSE; MINIDUMP_CALLBACK_INFORMATION mci; mci.CallbackRoutine = (MINIDUMP_CALLBACK_ROUTINE)MiniDumpCallback; mci.CallbackParam = 0; MINIDUMP_TYPE mdt = (MINIDUMP_TYPE)0x0000ffff; MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), hFile, MiniDumpNormal, &mdei, NULL, &mci); CloseHandle(hFile); } } LPTOP_LEVEL_EXCEPTION_FILTER WINAPI MyDummySetUnhandledExceptionFilter(LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter) { return NULL; } BOOL PreventSetUnhandledExceptionFilter() { 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 = &MyDummySetUnhandledExceptionFilter; 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; } LONG WINAPI UnhandledExceptionFilterEx(struct _EXCEPTION_POINTERS *pException) { TCHAR szMbsFile[MAX_PATH] = { 0 }; ::GetModuleFileName(NULL, szMbsFile, MAX_PATH); TCHAR* pFind = _tcsrchr(szMbsFile, '\\'); if(pFind) { *(pFind+1) = 0; _tcscat(szMbsFile, _T("CreateMiniDump.dmp")); CreateMiniDump(pException,szMbsFile); } // TODO: MiniDumpWriteDump FatalAppExit(-1, _T("Fatal Error")); return EXCEPTION_CONTINUE_SEARCH; } //运行异常处理 void RunCrashHandler() { SetUnhandledExceptionFilter(UnhandledExceptionFilterEx); PreventSetUnhandledExceptionFilter(); }
添加测试代码片断,main.cpp,以下:
#include "minidump.h" #include "cstdio" class CrashTest { public: void Test() { Crash(); } private: void Crash() { strcpy(NULL,"adfadfg"); } }; int main(int argc, char* argv[]) { //设置异常处理函数 RunCrashHandler(); CrashTest test; test.Test(); getchar(); return 0; }
同上
过程如上所示,所以再也不赘述。能够经过VS2008的堆栈帧函数调用层次。
程序数据库 (.pdb) 文件(也称为符号文件)将你在类、方法和其余代码的源文件中建立的标识符映射到在项目的已编译可执行文件中使用的标识符。 .pdb 文件还能够将源代码中的语句映射到可执行文件中的执行指令。 调试器使用此信息肯定两个关键信息:显示在 Visual Studio IDE 中的源文件和行号,以及可执行文件中在设置断点时要中止的位置。 符号文件还包含源文件的原始位置以及(可选)源服务器的位置(可从中检索源文件)。
在 Visual Studio IDE 中调试项目时,调试器须要知道查找代码的 .pdb 和源文件的确切位置。 若是要在项目源代码以外调试代码(如项目调用的 Windows 或第三方代码),则你必须指定 .pdb(也能够是外部代码的源文件)的位置,这些文件须要与可执行文件彻底匹配。pdb文件主要存储了以下调试信息:
(1)public, private,和static函数地址。
(2)全局变量的名称和地址。
(3)参数和局部变量的名称及它们在栈中的偏移量。
(4)类型定义,包括class, structure,和 data definitions。
调试时,系统会查找exe或者dll中指定位置的pdb文件,而且会跟踪exe或者dll中pdb的校验码GUID来对现有的pdb文件进行版本校验。这里须要知道,即便源码没有作任何更改,该pdb文件对应的校验码也是不一样的。详细了解能够参考引用。
在编程实践中,遭遇到了诸如内存无效访问、无效对象、内存泄漏、堆栈溢出等不少C / C++ 程序员常见的问题,最后都是同一个结果:程序崩溃,为解决崩溃问题,过程都是很是让人难以忘怀的;
可谓吃一堑长一智,出现过几回这样的折腾后就寻思找出它们的原理和规律,把这些典型的编程错误一网打尽,通过系统性的分析和梳理,发现其内在机理大同小异,经过对错误表现和原理进行分类分析,把各类致使崩溃的错误进行归类,详细分类以下:
错误类型 具体表现 备注(案例)
声明错误 变量未声明 编译时错误
初始化错误 未初始化或初始化错误 运行不正确
访问错误 一、 数组索引访问越界
二、 指针对象访问越界
三、 访问空指针对象
四、 访问无效指针对象
五、 迭代器访问越界
六、 空指针调用函数
内存泄漏 一、 内存未释放
二、 内存局部释放
参数错误 本地代理、空指针、强制转换
堆栈溢出 调用堆栈溢出:
一、递归调用
二、循环调用
三、消息循环
四、大对象参数
五、大对象变量 参数、局部变量都在栈(Stack)上分配
转换错误 有符号类型和无符号类型转换
内存碎片 小内存块重复分配释放致使的内存碎片,最后出现内存不足 数据对齐,机器字整数倍分配
其它如内存分配失败、建立对象失败等都是容易理解和相对少见的错误,由于目前的系统大部分状况下内存够用;此外除0错误也是容易理解和防范;