Windbg調試一)minidump崩潰捕捉
在日常工作中,本地c++代碼發生崩潰時,編譯器都可以幫我們捕捉到並且定位到具體的代碼,這是因為編譯器接收到了操作系統發送過來的程序異常通知並進行了處理。但是在使用我們軟件的用戶環境上,沒有編譯器幫我們處理這個異常,操作系統會使用它的異常處理機制:彈出程序異常對話框。因此我們需要將崩潰時產生的堆棧信息生成dump文件,傳送到我們的服務器上,通過Windbg工具或者vs編譯器進行崩潰分析。
一,系統的異常處理順序
1,系統首先判斷異常是否應發送給目標程序的異常處理模塊,如果決定應該發送,並且目標程序正在被調試,則系統掛起程序並向調試器發送EXCEPTION_DEBUG_EVENT消息。
2,如果目標程序沒有被調試或者調試器未能處理異常,系統就會繼續查找你是否添加了線程相關的異常處理機制,如果有,系統就把異常發送給你的程序seh處理例程,交由其處理。
3,每個線程相關的異常處理例程可以處理或者不處理這個異常,如果不處理並且安裝了多個線程相關的異常處理例程,,可交由鏈起來的其他例程處理.。
4,如果這些例程均選擇不處理異常,如果程序處於被調試狀態,操作系統仍會再次掛起程序通知調試器。
5,如果程序未處於被調試狀態或者調試器沒有能夠處理,並且程序調用SetUnhandledExceptionFilter安裝了異常捕捉的話,系統轉向調用它的全局異常過濾函數。
6,在調用了SetUnhandledExceptionFilter后,UnhandledExceptionFilter依舊會首先檢查當前應用程序是否在調試器的控制之下,如果是,它將返回EXCEPTION_CONTINUE_SEARCH,由調試器處理當前異常。
7.,如果程序也沒有調用SetUnhandledExceptionFilter,系統會調用默認的系統處理程序,通常顯示一個對話框,“程序無響應”或者“程序中斷”,在安裝了開發環境的機器上,還會彈出附加調試的對話框,如果沒有調試器能被附加於其上或者調試器也處理不了,系統 就調用ExitProcess終結程序.。
而我們程序中需要加入的崩潰捕捉模塊也就是通過SetUnhandledExceptionFilter函數來實現的。
二,SetUnhandleExceptionFilter函數
Windows操作系統提供了一個API函數可以在程序crash之前有機會處理這些異常,就是 SetUnhandleExceptionFilter函數。(C++也有一個類似函數set_terminate可以處理未被捕獲的C++異常。)
SetUnhandleExceptionFilter函數聲明如下:
LPTOP_LEVEL_EXCEPTION_FILTER WINAPI SetUnhandledExceptionFilter(
__in LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter
);
其中 LPTOP_LEVEL_EXCEPTION_FILTER 定義如下:
typedef LONG (WINAPI *PTOP_LEVEL_EXCEPTION_FILTER)(
__in struct _EXCEPTION_POINTERS *ExceptionInfo
);
typedef PTOP_LEVEL_EXCEPTION_FILTER LPTOP_LEVEL_EXCEPTION_FILTER;
簡單來說,SetUnhandleExceptionFilter允許我們設置一個自己的函數作為全局SEH過濾函數,當程序crash前會調用我們的函 數進行處理。我們可以利用的是 _EXCEPTION_POINTERS 結構類型的變量ExceptionInfo,它包含了對異常的描述以及發生異常的線程狀態,過濾函數可以通過返回不同的值來讓系統繼續運行或退出應用程序。
三,Minidump
minidump(小存儲器轉儲)可以理解為一個dump文件,里面記錄了能夠幫助調試crash的最小有用信息。實際上,如果你在 系統屬性 -> 高級 -> 啟動和故障恢復 -> 設置 -> 寫入調試信息 中選擇“小內存轉儲(64 KB)”的話,當系統意外停止時都會在C:\Windows\Minidump\路徑下生成一個.dmp后綴的文件,這個文件就是minidump文件。
只不過這種方式生成的是內核態的minidump。我們要生成的是用戶態的minidump,文件中包含了程序運行的模塊信息、線程信息、堆棧調用信息等。而且為了符合其mini的特性,dump文件是壓縮過的。
windows操作系統也給我們提供了一個API函數可以來寫minidump信息,就是MiniDumpWriteDump函數,函數聲明如下:
BOOL
WINAPI
MiniDumpWriteDump(
IN HANDLE hProcess, //當前進程句柄
IN DWORD ProcessId, //當前進程ID
IN HANDLE hFile, //文件句柄
IN MINIDUMP_TYPE DumpType, //MINIDUMP類型
IN CONST PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam, OPTIONAL //異常信息(最重要)
IN CONST PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam, OPTIONAL //用戶數據流(一般不需要)
IN CONST PMINIDUMP_CALLBACK_INFORMATION CallbackParam OPTIONAL //回調(一般不需要)
);
ExceptionParam是最關鍵的參數,它是一個PMINIDUMP_EXCEPTION_INFORMATION 結構體,結構體內部有一個PEXCEPTION_POINTERS成員,也就是上面PTOP_LEVEL_EXCEPTION_FILTER函數指針的參數。
四,代碼演示
dump捕捉分為進程內和進程外兩種,一般都認為進程內進行dump捕捉是不安全的,因為程序已經異常了,在異常的堆或棧上進行操作是有風險的,會導致二次異常。因此在實際的項目中都會采用進程外捕捉dump,兩個進行通過共享內存來共享PEXCEPTION_POINTERS異常指針,由異常捕捉進行來寫dump文件,並上傳到我們自己的服務器上。具體用哪種方式,我覺得視程序的使用場景來定,下面貼上封裝好了的dump_catch.h文件的代碼,只有一個文件,調用方式很簡單,在程序的入口函數最上方調用::SetUnhandledExceptionFilter(UnhandledExceptionFilterEx);即可,默認在exe同級目錄下的dump文件夾下生成minidump文件(日期命名).
/*
created: 2018/12/25
filename: dump.h
author: libing
depend: dbghelp.lib,Dbghelp.h
build: vc(windows)s
purpose: 實現windows程序崩潰捕捉,落地成dump文件.
沒有封裝成類,是因為沒有必要,都是幾個簡單的靜態函數,寫在一個.h文件中,也方便調用。
useway: 在main函數或者WinMain函數中,
程序初始化之前調用::SetUnhandledExceptionFilter(UnhandledExceptionFilterEx)即可。
*/
#pragma once
#include "Dbghelp.h"
#include <shlwapi.h>
#pragma comment(lib, "Dbghelp.lib")
#pragma comment(lib, "shlwapi.lib")
BOOL CALLBACK MiniDumpCallback(PVOID, const PMINIDUMP_CALLBACK_INPUT input, PMINIDUMP_CALLBACK_OUTPUT output)
{
if (input == NULL || output == NULL)
return FALSE;
BOOL ret = FALSE;
switch (input->CallbackType)
{
case IncludeModuleCallback:
case IncludeThreadCallback:
case ThreadCallback:
case ThreadExCallback:
ret = TRUE;
break;
case ModuleCallback:
{
if (!(output->ModuleWriteFlags & ModuleReferencedByMemory))
{
output->ModuleWriteFlags &= ~ModuleWriteModule;
}
ret = TRUE;
}
break;
default:
break;
}
return ret;
}
void WriteDump(EXCEPTION_POINTERS* exp, const std::string &path)
{
HANDLE h = ::CreateFile(path.c_str(), GENERIC_WRITE | GENERIC_READ, FILE_SHARE_WRITE | FILE_SHARE_READ,
NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if(h == InvalidHandle)
{
//AfxMessageBox("HANDLE h = ::CreateFile");
//這里可以加日志
return;
}
MINIDUMP_EXCEPTION_INFORMATION info;
info.ThreadId = ::GetCurrentThreadId();
info.ExceptionPointers = exp;
info.ClientPointers = NULL;
MINIDUMP_CALLBACK_INFORMATION mci;
mci.CallbackRoutine = (MINIDUMP_CALLBACK_ROUTINE)MiniDumpCallback;
mci.CallbackParam = 0;
MINIDUMP_TYPE mdt = (MINIDUMP_TYPE)(MiniDumpWithIndirectlyReferencedMemory | MiniDumpScanMemory);
MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), h, mdt, &info, NULL, &mci);
::CloseHandle(h);
}
void GetDirPath(std::string & strDir)
{
//dump文件存儲路徑,存儲在exe同級目錄下的Dump文件夾中
char szAppPath[MAX_PATH] = { 0 };
GetModuleFileName(NULL, szAppPath, sizeof(szAppPath) - 1);
(strrchr(szAppPath, '\\'))[0] = 0;
strDir = szAppPath;
strDir.append("\\Dump");
if(!PathFileExists(strDir.c_str()))
{
CreateDirectory(strDir.c_str(), NULL);
}
strDir.append("\\");
}
LONG WINAPI UnhandledExceptionFilterEx(EXCEPTION_POINTERS* exp)
{
SYSTEMTIME szSysDate;
GetLocalTime(&szSysDate);
char szFileName[MAX_PATH] = {0};
sprintf(szFileName, "%04d%02d%02d_%02d%02d%02d.dmp",
szSysDate.wYear, szSysDate.wMonth, szSysDate.wDay, szSysDate.wHour, szSysDate.wMinute, szSysDate.wSecond);
std::string dir;
GetDirPath(dir);
dir.append(szFileName);
WriteDump(exp, dir);
char szBuf[512] = {0};
sprintf(szBuf, "程序崩潰, dump文件為:%s", dir.c_str());
//可以做點別的
return EXCEPTION_EXECUTE_HANDLER;
}
參考資料:
1,https://www.cnblogs.com/lidabo/p/3635960.html
2,<windows核心編程>第25章
from:https://blog.csdn.net/bajianxiaofendui/article/details/94588826