也談Release版本排錯
通常Release除錯都是先通過SetUnhandledExceptionFilter捕獲異常,然後生成報告檔案,最後定位程式碼行,主要以下兩種方法:
(一)通過遍歷呼叫棧,將其呼叫棧資訊輸出到檔案。然後查找出錯地址。
查詢方式有兩種:
(1)通過編譯器生成的包含行資訊的map檔案定位出錯位置。
通過在“工程屬性”-〉“link”-〉“Project Options”手工輸入 /mapinfo:lines,生成包含行資訊map檔案。查詢時首先根據出錯地址範圍找到obj檔名,檢視obj檔案對應的行資訊,根據出錯地址範圍定位程式碼行。
(2)通過編譯器生成的pdb檔案定位出錯位置。
debug
在pdb檔案中查找出錯地址所在的程式碼行,需要通過dbghelp庫(包含在windbg目錄下),通過SymFromAddr函式可以獲取符號資訊,SymGetLineFromAddr64獲取所在程式碼行。
遍歷呼叫棧方法方法也有兩種:
(1)自己遍歷呼叫棧
這種方法的缺陷是Release版本通常會使用FPO(Frame-Pointer Omisstion)
自己遍歷基於以下原理(這個原理只適用於沒有采用FPO優化的函式):
1 函式呼叫時call指令將返回地址(通常是下一條指令的地址)壓入堆疊 。
2 函式執行第一行會將 ebp壓入堆疊,儲存它以使得當函式返回能恢復ebp。
3 Copy當前棧位置esp到 ebp。
4.然後esp自減以空出棧空間容納函式的區域性變數
因此當前函式內的ebp即為第2步壓入ebp後的棧頂位置,由此可推匯出上一層函式的ebp為[ebp],而上一層函式返回地址即為前一個壓入棧的值,即[ebp+4],由此可以一步步往上回溯呼叫棧。
(2)通過dbghelp庫函式StackWalk64遍歷堆疊。
這種方式可以選擇是否載入pdb,對於做了那些被FPO優化的函式,pdb儲存了相關資料來幫助遍歷呼叫棧,如果不能載入到正確的pdb,StackWalk64將使用前面介紹的基於ebp的方式遍歷呼叫棧,從而漏掉那些被FPO優化的函式。
(二)通過生成mini dump檔案定位bug。
通過dbghelp庫函式MiniDumpWriteDump將出錯時資訊寫入檔案,然後用windbg開啟dump檔案,配置好symbols路徑,exe檔案路徑,source code 路徑,輸入.ecxr命令,就可以檢視詳細的呼叫棧,並能自動開啟原始檔定位到程式碼行。因此這種方法是簡單和最可靠的方法。
下面是一個簡單的dume類,只要添入到工程即可,出錯時會自動生成dum檔案。
#include <windows.h>
#include <tchar.h>
#include <assert.h>
//for VC6
#ifndef __in_bcount_opt
#define __in_bcount_opt(x)
#endif
#ifndef __out_bcount_opt
#define __out_bcount_opt(x)
#endif
//end (for VC6)
#include "dbghelp.h"
typedef BOOL (WINAPI *MINIDUMPWRITEDUMP)(
IN HANDLE hProcess,
IN DWORD ProcessId,
IN HANDLE hFile,
IN MINIDUMP_TYPE DumpType,
IN CONST PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam, OPTIONAL
IN CONST PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam, OPTIONAL
IN CONST PMINIDUMP_CALLBACK_INFORMATION CallbackParam OPTIONAL
);
class CMiniDumper
{
public:
CMiniDumper();
private:
static LPTOP_LEVEL_EXCEPTION_FILTER s_pPrevFilter;
static long WINAPI UnhandledExceptionFilter( struct _EXCEPTION_POINTERS *pExceptionInfo );
};
CMiniDumper g_minObject;
LPTOP_LEVEL_EXCEPTION_FILTER CMiniDumper::s_pPrevFilter = 0;
CMiniDumper::CMiniDumper()
{
assert(!s_pPrevFilter);
s_pPrevFilter = ::SetUnhandledExceptionFilter(UnhandledExceptionFilter);
}
long CMiniDumper::UnhandledExceptionFilter( struct _EXCEPTION_POINTERS *pExceptionInfo )
{
long ret = EXCEPTION_CONTINUE_SEARCH;
TCHAR szDbgHelpPath[_MAX_PATH] = {0};
TCHAR szDumpPath[_MAX_PATH] = {0};
TCHAR szPath[_MAX_PATH] = {0};
if (GetModuleFileName(NULL, szPath, _MAX_PATH))
{
TCHAR szDrive[_MAX_DRIVE] = {0};
TCHAR szDir[_MAX_DIR] = {0};
TCHAR szFileName[_MAX_FNAME] = {0};
_tsplitpath(szPath, szDrive, szDir, szFileName, 0);
_tcsncat(szDbgHelpPath, szDrive, _MAX_PATH);
_tcsncat(szDbgHelpPath, szDir, _MAX_PATH - _tcslen(szDbgHelpPath) - 1);
_tcsncat(szDbgHelpPath, _T("dbghelp.dll"), _MAX_PATH - _tcslen(szDbgHelpPath) - 1);
_tcsncat(szDumpPath, szDrive, _MAX_PATH);
_tcsncat(szDumpPath, szDir, _MAX_PATH - _tcslen(szDumpPath) - 1);
_tcsncat(szDumpPath, szFileName, _MAX_PATH - _tcslen(szDumpPath) - 1);
_tcsncat(szDumpPath, _T(".dmp"), _MAX_PATH - _tcslen(szDumpPath) - 1);
}
HMODULE hDll = ::LoadLibrary(szDbgHelpPath);
if (hDll==NULL)
hDll = ::LoadLibrary(_T("dbghelp.dll"));
assert(hDll);
if (hDll)
{
MINIDUMPWRITEDUMP pWriteDumpFun = (MINIDUMPWRITEDUMP)::GetProcAddress(hDll, "MiniDumpWriteDump");
if (pWriteDumpFun)
{
// create the file
HANDLE hFile = ::CreateFile(szDumpPath, 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 = pExceptionInfo;
ExInfo.ClientPointers = FALSE;
// write the dump
if (pWriteDumpFun(GetCurrentProcess(), GetCurrentProcessId(),
hFile, MiniDumpNormal, pExceptionInfo!=0? &ExInfo: 0, NULL, NULL))
ret = EXCEPTION_EXECUTE_HANDLER;
::CloseHandle(hFile);
}
}
}
if (s_pPrevFilter)
ret = s_pPrevFilter(pExceptionInfo);
return ret;
}