1. 程式人生 > >Dump文件的生成和使用

Dump文件的生成和使用

targe exe 數據庫 main函數 jump 原理 namespace .net localtime

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 引用

  1. https://www.cnblogs.com/zhoug2020/p/6025388.html
  2. https://blog.csdn.net/hustd10/article/details/52075265
  3. https://blog.csdn.net/itworld123/article/details/79061296

Dump文件的生成和使用