1. 程式人生 > >Dump檔案的生成和使用

Dump檔案的生成和使用

1 簡介

第一次遇到程式崩潰的問題,之前為單位開發了一個外掛程式,在本機執行沒有出現問題,但把生成的可執行檔案拷貝到伺服器上一執行程式,剛進入外掛程式碼,外掛服務就崩潰了,當時被這個問題整的很慘,在同事的幫助下了解到,對於程式崩潰,最快的解決方式是生成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_MACHINE\SOFTWARE\Microsoft\Windows\WindowsError Reporting\LocalDumps

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 "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是同一時間產生的,則直接點選除錯即可直接進入程式中斷,這樣通過檢視呼叫堆疊,即可快速定位問題。

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錯誤也是容易理解和防範;

7 引用