第一次遇到程序崩溃的问题,之前为单位开发了一个插件程序,在本机运行没有出现问题,但把生成的可执行文件拷贝到服务器上一运行程序,刚进入插件代码,插件服务就崩溃了,当时被这个问题整的很惨,在同事的帮助下了解到,对于程序崩溃,最快的解决方式是生成dump文件,通过生成dump文件使用调试工具进行调试,还原程序崩溃时的状态,能够起到快速定位排查问题的作用。Dump文件是进程的内存镜像。可以把程序的执行状态通过调试器保存到dump文件中。Dump文件是用来给驱动程序编写人员调试驱动程序用的,这种文件必须用专用工具软件打开,比如使用WinDbg、VS打开。因为第一次遇到此类问题,完全没有头绪,但同事很快通过dump文件很快定位到空指针问题,秉承着遇到的问题在遇到第二次不能再是问题的原则,对dump文件的含义、生成、作用、分析、定位排查的过程进行说明,算是对遇到的程序崩溃的问题总结。
本文档适用于开发人员。
2 Dump文件的含义和作用
2.1 Dump文件的类型
Windows下Dump文件分为两大类,内核模式Dump和用户模式Dump。内核模式Dump是操作系统创建的崩溃转储,最经典的就是系统蓝屏,这时候会自动创建内核模式的Dump。用户模式Dump进一步可以分为完整Dump(Full
Dump)和迷你Dump(Minidump)。完整Dump包含了某个进程完整的地址空间数据,以及许多用于调试的信息,而Minidump则有许多类型,根据需要可以包含不同的信息,有的可能只包含某个线程和部分模块的信息。在程序开发过程中出现的应用崩溃属于用户模式Dump。因此,要弄清楚这种Dump文件的组成、生成方式、作用。
2.2 Dump文件的作用
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文件生成的控制代码,很快便定位到了该空指针异常。
2.3 Dump文件的生成
程序在运行时,难免会有一些异常情况发生,特别是在条件不容许去挂调试器的时候,如何快速的定位错误的方法就显得很重要。
都是一种很重要的定位错误的方法,出得好的日志可以方便程序员快速的定位问题所在。但日志有时也显不足:
日志有时只能定位大体错误范围,却无法确认问题所在,比如程序抓到一个未知的异常。
没有机会来出日志,或者能出日志的时候已经无法获得和错误相关的信息,比如程序崩溃的时候。
日志明显不足的时候,把进程中相关数据DUMP下来分析就是一个比较实用方便的方法。很多应用都会提供这类功能,以便在程序出现问题时可以把相关的数据发给开发者,方便开发者分析问题。类似Office这样的应用都会有这个功能,当应用崩溃时会弹出对话框,提示是否发送错误相关的数据。
由于Dump文件能够保存程序内部的内存、堆栈、句柄、线程等程序运行相关的信息,非常具有重要性,因此了解如何生成Dump文件也是避免茫然无措,不知如何下手场景的途径之一。
2.3.1 通过使用任务管理器生成
该方式可以生成.DMP文件,通过打开任务管理器,找到插件服务对应的进程,右击,选择创建转储文件:
.DMP文件的存放位置如下图所示:
生成的转储文件可以通过VS打开,但是正常运行的程序生成.DMP文件并没有什么大的作用。上述的方法要求在程序崩溃时并不直接退出时才可以使用,一般场景下,程序崩溃比较粗暴,因此可以使用下述的方式创建Dump文件
2.3.2 通过编程自动生成
当程序遇到未处理异常(主要指非指针造成)导致程序崩溃死,如果在异常发生之前调用了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完成的,函数原型如下:
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为默认的异常处理器工作产生的,此时可以点击中断或者继续,而在对应的右下方可以看到调用堆栈,对于我们排查定位非常有帮助。
2.3.3 修改注册码生成
修改注册码的方式,没有使用过,但通过查询网上的材料,总结如下:
2.3.3.1 打开注册表
Win + R 输入regedit打开注册表
2.3.3.2 依次找到如下对应项
HKEY_LOCAL_MACHINESOFTWAREMicrosoftWindowsWindowsError ReportingLocalDumps
2.3.3.3 添加项
在该栏目下添加项如图所示:
这样可以保证倘若程序故障后自行退出,则此方法就难以应用。不过,我们可以在注册表中添加如下信息已确保系统在程序崩溃后自行保存一个dump文件。
2.3.4 通过编程自动生成实践
单位对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 "SocketCompleteSocket.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是同一时间产生的,则直接点击调试即可直接进入程序中断,这样通过查看调用堆栈,即可快速定位问题。
3 如何使用Dump文件排查崩溃问题
3.1 程序架构
插件程序的架构如下,我负责开发的模块对接设备,即通过调用SDK调用报警盒子和中心管理机进行呼叫、广播、挂断,这是通过代理实现的。DeviceAccess包含main函数,其主要是进行接收socket数据报,然后对数据报进行解析。HikTalkClientPlugin是开发的代理设备接口,该项目导出HikTalkClientPlugin.dll供DeviceAccess进行调用,包括连接、断开连接、调用广播呼叫等功能。集成到服务器上时,以服务的形式运行了DeviceAccess,由其接收socket数据报,并通过对接对讲平台进行设备功能的调用。
3.2 调试过程
3.2.1 编译HikTalkClientPlugin
首先编译运行HikTalkClientPlugin生成对应的HikTalkClientPlugin.dll和HikTalkClientPlugin.pdb。然后把生成的HikTalkClientPlugin.dll和HikTalkClientPlugin.pdb复制到DeviceAccess项目的Release的指定目录下(因为DeviceAccess的可执行程序要调用HikTalkClientPlugin.dll嘛,所以肯定要和DeviceAccess一起发布的)。
3.2.2 编译DeviceAccess
以Release模式编译并运行DeviceAccess项目,可以在输出目录中生成可执行文件,截图如下:
3.2.3 服务方式启动程序
压缩成rar并拷贝到服务器上以服务的方式启动,让前段发出设备操作请求,安静等待程序崩溃。和预期一样,程序崩溃,生成了dump文件。如图所示:
3.2.4 拷贝.dmp文件到开发机
把在服务器端生成的.dmp文件拷贝到开发机DeviceInterfaceAgent.exe对应的位置,在该目录需要有DeviceInterfaceAgent.pdb。pdb文件,是VS生成的用于调试的符号文件(program
database),保存着调试的信息。在VS的工程属性,C/C++,调试信息格式,设置/Zi,那么VS就会在构建项目时创建PDB文件。需要保证源代码、pdb文件、可执行文件是与服务器上相同的版本,这样才可以进行正常的调试。
3.2.5 VS打开.dmp文件
使用VS打开.dmp文件进行调试,会发现程序直接在程序崩溃处停了下来。此时,查看调用堆栈信息[若没有,点击Alt + 7即可出现]。通过查看调用堆栈即可快速定位。
3.2.6 通过调用堆栈定位排查问题
通过调用堆栈定位排查问题,可以看到如2.2中第二个崩溃原因是空指针异常,因此找到空指针出现的位置,并在通过函数为指针赋值之后添加空指针判断和操作成功的判断。发现这是HikTalkClientPlugin插件的问题,因此修改HikTalkClientPlugin的源代码,重新编译生成DLL,并把HikTalkClientPlugin.dll和HikTalkClientPlugin.pdb文件拷贝到服务器上Release/hplugin/
HikTalkClientPlugin目录内,因为DeviceInterfaceAgent并没有修改源代码,无需变动。重新启动服务接收socket请求。
以同样的方式定位注册时插件崩溃的问题,修改HikNetSdkClientPlugin源代码,重新编译HikNetSdkClientPlugin,生成HikNetSdkClientPlugin.pdb和HikNetSdkClientPlugin.dll,把该两个文件拷贝至服务器Release/hplugin/HikNetSdkClientPlugin内,重新启动服务,插件不再崩溃。问题得到解决。
4 实践过程
以VS为例,在开发机上开发代码时,如果程序崩溃并且崩溃时并不是直接退出,那么点击中断之后的界面即为调试Dump文件的情景。因此,dump文件对于这种本地开发或许作用并不大,但是如果程序在服务器端崩溃,那么此时生成的Dump文件并非常重要,它可以避免你在一个较大的项目代码面前茫然无措。但下面的代码片段均为本地机上的代码,并且异常也是刻意为之,只是为了演示dmp文件的生成和调试。其中的代码均为在网上搜索到,仅用来演示使用。
4.1 DumpTest1
4.1.1 新建项目DumpTest1
添加头文件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; }
可以清楚的看到,在测试程序中使用了空指针,即对空指针解引用,并对其进行赋值操作,违法操作。
4.1.2 使用everything查找dbghelp.dll
把该dbghelp.dll放置在该项目的目录下,即右击项目,打开资源管理器所在目录。
4.1.3 生成dump文件
在VS中进行如下的配置:
属性–>链接器—>调试–>生成调试信息–>是。
属性–>配置属性–>常规–>字符集–>使用多字节字符集
点击调试–>开始执行(不调试)–>查看运行结果
注意:如果点击了调试–>启动调试,程序直接崩溃,但没有退出,程序所呈现的界面即为使用dmp文件调试bug的界面。可以在项目所在目录下看到dmp文件已经生成,如下图所示:
4.1.4 调试过程
4.1.4.1 拷贝dmp文件到exe、pdb文件所在目录
找到程序的输出目录,在该目录下可以看到两个文件生成,DumpTest1.exe, DumpTest1.pdb,把之前生成dmp文件拷贝到该目录下。
4.1.4.2 使用VS打开dmp文件观看运行界面
Microsoft Visual Studio给出的弹窗提示写入位置为0x00000000,而调用堆栈可以指示出程序崩溃时的位置。通过这两个位置可以快速的帮助我们定位出问题代码。
4.2 DumpTest2
4.2.1 新建项目DumpTest2
添加代码片段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; }
4.2.2 使用everything查找dbghelp.dll
同上
4.2.3 生成dmp文件并使用vs2008调试dmp
过程如上所示,因此不再赘述。可以通过VS2008的堆栈帧函数调用层次。
5 注意事项
5.1 pdb文件
程序数据库 (.pdb)
文件(也称为符号文件)将你在类、方法和其他代码的源文件中创建的标识符映射到在项目的已编译可执行文件中使用的标识符。 .pdb
文件还可以将源代码中的语句映射到可执行文件中的执行指令。 调试器使用此信息确定两个关键信息:显示在 Visual Studio IDE
中的源文件和行号,以及可执行文件中在设置断点时要停止的位置。 符号文件还包含源文件的原始位置以及(可选)源服务器的位置(可从中检索源文件)。
在 Visual Studio IDE 中调试项目时,调试器需要知道查找代码的 .pdb 和源文件的确切位置。
如果要在项目源代码之外调试代码(如项目调用的 Windows 或第三方代码),则你必须指定
.pdb(也可以是外部代码的源文件)的位置,这些文件需要与可执行文件完全匹配。pdb文件主要存储了如下调试信息:
(1)public, private,和static函数地址。
(2)全局变量的名称和地址。
(3)参数和局部变量的名称及它们在栈中的偏移量。
(4)类型定义,包括class, structure,和 data definitions。
5.2 exe、dll和pdb一致性问题
调试时,系统会查找exe或者dll中指定位置的pdb文件,并且会跟踪exe或者dll中pdb的校验码GUID来对现有的pdb文件进行版本校验。这里需要知道,即使源码没有做任何更改,该pdb文件对应的校验码也是不同的。详细了解可以参考引用。
6 C++崩溃常见问题
在编程实践中,遭遇到了诸如内存无效访问、无效对象、内存泄漏、堆栈溢出等很多C / C++ 程序员常见的问题,最后都是同一个结果:程序崩溃,为解决崩溃问题,过程都是非常让人难以忘怀的;
可谓吃一堑长一智,出现过几次这样的折腾后就寻思找出它们的原理和规律,把这些典型的编程错误一网打尽,经过系统性的分析和梳理,发现其内在机理大同小异,通过对错误表现和原理进行分类分析,把各种导致崩溃的错误进行归类,详细分类如下:
错误类型 具体表现 备注(案例)
声明错误 变量未声明 编译时错误
初始化错误 未初始化或初始化错误 运行不正确
访问错误 1、 数组索引访问越界
2、 指针对象访问越界
3、 访问空指针对象
4、 访问无效指针对象
5、 迭代器访问越界
6、 空指针调用函数
内存泄漏 1、 内存未释放
2、 内存局部释放
参数错误 本地代理、空指针、强制转换
堆栈溢出 调用堆栈溢出:
1、递归调用
2、循环调用
3、消息循环
4、大对象参数
5、大对象变量 参数、局部变量都在栈(Stack)上分配
转换错误 有符号类型和无符号类型转换
内存碎片 小内存块重复分配释放导致的内存碎片,最后出现内存不足 数据对齐,机器字整数倍分配
其它如内存分配失败、创建对象失败等都是容易理解和相对少见的错误,因为目前的系统大部分情况下内存够用;此外除0错误也是容易理解和防范;