尋找Release版程發生異常退出的地方比Debug版麻煩得多。發生異常的時候Windows通常會彈出一個錯誤對話框,點擊詳細信息,我們能獲得出錯的地址和大概的出錯信息,然後可以用以下辦法分析我們的程序。
一. 用MAP文件定位異常代碼位置。
1.如何生成map文件
打開“Project →Project Settings”,選擇 C/C++ 選項卡,在“Debug Info”欄選擇“Line Numbers Only”(或者在最下面的 Project Options 裡面輸入:/Zd),然後要選擇 Link 選項卡,選中“Generate mapfile”復選框,並再次編輯 Project Options,輸入:/mapinfo:lines,以便在 MAP 文件中加入行信息。然後編譯工程則可以在輸出目錄得到同名的.map文件。
2. 使用map文件定位發生異常的代碼行
編譯得到的map文件可以用文本方式打開,大致是這樣的格式:(括號內是PomeloWu填加的注釋)
0729 (←工程名)
Timestamp is 42e9bc51 (Fri Jul 29 14:19:13 2005) (←時間戳)
Preferred load address is 00400000 (←基址)
……(Data段描述,省略)
Address Publics by Value Rva+Base Lib:Object
0001:00000000 ?_GetBaseMessageMap@C0729App@@KGPBUAFX_MSGMAP@@XZ 00401000 f 0729.obj
……(↑這一行開始是函數信息,下面省略)
Line numbers for .ReleaseShowDlg.obj(C:729ShowDlg.cpp) segment .text
24 0001:00003f90 28 0001:00003fce 29 0001:00003fd1 30 0001:00003fd4
……(行號信息,前面的數字是行號,後一個數字是偏移量,下面省略)
在獲得程序異常的地址以後,首先通過函數信息部分定位出錯的OBJ和函數。做法是用獲得的異常地址與Rva+Base欄地址進行比較(Rva,偏移地址;Base,基址)。找到最後一個比獲得的異常地址小的那個函數,那就是出錯的函數。
之後,用獲得的異常地址減去該函數的Rva+Base,就得到了異常行代碼相對於函數起始地址的偏移。在“Line number for”部分找到相對應的模塊,並把其後的行號信息與上面減得的偏移量對比,找到最接近的一個,前面的行號大致就是目標行了。
二. 獲得錯誤的詳細信息。
實際上,光靠Windows的錯誤消息對話框提供的信息量是很有限的,用自己寫的exception filter可以獲得更多的錯誤信息。用SetUnhandledExceptionFilter設定自定義錯誤處理回調函數替換Win32默認的top-level exception filter:
² SetUnhandledExceptionFilter的函數原型:
LPTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter(
LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter
// exception filter function
);
² SetUnhandledExceptionFilter返回當前的exception filter。應當保存這個函數指針並在不再需要使用自定義錯誤處理函數的時候當作參數再次調用SetUnhandledExceptionFilter。
² lpTopLevelExceptionFilter 是自定義的exception filter函數指針,如果傳入NULL值則指定UnhandledExceptionFilter來負責異常處理。lpTopLevelExceptionFilter其函數原型應該是與UnhandledExceptionFilter同型:
LONG WINAPI UnhandledExceptionFilter(
STRUCT _EXCEPTION_POINTERS *ExceptionInfo // address of
// exception info
);
² lpTopLevelExceptionFilter的返回值應該是下面3種之一:
EXCEPTION_EXECUTE_HANDLER = 1
EXCEPTION_CONTINUE_EXECUTION = -1
這兩個返回值都應該由調用UnhandledExceptionFilter後返回。
EXCEPTION_EXECUTE_HANDLER表示進程結束
EXCEPTION_CONTINUE_EXECUTION表示處理異常之後繼續執行
EXCEPTION_CONTINUE_SEARCH = 0
進行系統通常的異常處理(錯誤消息對話框)
² lpTopLevelExceptionFilter的唯一的參數是_EXCEPTION_POINTERS結構指針。
typedef struct _EXCEPTION_POINTERS { // exp
PEXCEPTION_RECORD ExceptionRecord;
PCONTEXT ContextRecord;
} EXCEPTION_POINTERS;
其中PCONTEXT是一個指向進程上下文結構的指針,保存了各個寄存器在異常發生的時候的值,詳細信息參考《Windows核心編程》。
ExceptionRecord則指向另一個結構體EXCEPTION_RECORD:
typedef struct _EXCEPTION_RECORD { // exr
DWord ExceptionCode;
DWord ExceptionFlags;
struct _EXCEPTION_RECORD *ExceptionRecord;
PVOID ExceptionAddress;
DWord NumberParameters;
DWord ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD;
DWord ExceptionCode;
異常代碼,指出異常原因。常見異常代碼有:
EXCEPTION_Access_VIOLATION = C0000005h
讀寫內存沖突
EXCEPTION_INT_DIVIDE_BY_ZERO = C0000094h
除0錯誤
EXCEPTION_STACK_OVERFLOW = C00000FDh
堆棧溢出或者越界
EXCEPTION_GUARD_PAGE = 80000001h
由Virtual Alloc建立起來的屬性頁沖突
EXCEPTION_NONCONTINUABLE_EXCEPTION = C0000025h
不可持續異常,程序無法恢復執行,異常處理例程不應處理這個異常
EXCEPTION_INVALID_DISPOSITION = C0000026h
在異常處理過程中系統使用的代碼
EXCEPTION_BREAKPOINT = 80000003h
調試時中斷(INT 3)
EXCEPTION_SINGLE_STEP = 80000004h
單步調試狀態(INT 1)
DWord ExceptionFlags;
異常標志
0,表示可修復異常
EXCEPTION_NONCONTINUABLE = 1,表示不可修復異常。在不可修復異常後嘗試繼續執行會導致EXCEPTION_NONCONTINUABLE_EXCEPTION = C0000025H異常。
struct _EXCEPTION_RECORD *ExceptionRecord;
當異常處理程序中發生異常時,此字段被填充,否則為NULL
PVOID ExceptionAddress;
發生異常的地址(EIP)
DWord NumberParameters;
規定與異常相關的參數數量(0-15),是ExceptionInformation數組中元素個數。
DWord ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
異常描述信息,大多數異常都未定義此數組,僅有EXCEPTION_Access_VIOLATION異常的描述信息:
ExceptionInformation[0],描述導致異常的操作類型
= 0 讀異常
= 1 寫異常
ExceptionInformation[1],發生讀寫異常的內存地址
也就是說,只要注冊了自己寫的這個exception filter,一旦發生異常,進入這個exception filter,從參數我們就能獲得各種需要的信息了。而這個exception filter需要做的就是保存這些信息,然後將異常處理的事情交還給系統就行了:
// in the beginning
// Install the unhandled exception filter function
g_previousFilter = SetUnhandledExceptionFilter(MyUnhandledExceptionFilter);
// exception filter
LONG WINAPI MyUnhandledExceptionFilter(PEXCEPTION_POINTERS pExceptionInfo)
{
WriteLogFile(pExceptionInfo); // 寫入文件
if ( g_previousFilter )
return g_previousFilter( pExceptionInfo );
else
return EXCEPTION_CONTINUE_SEARCH;
}
三. 使用COD文件精確分析異常原因
說精確分析多少有點言過其實,發生異常的情況各不相同,分析真正原因很可能是一件極其復雜的事情。不過用COD文件能比MAP文件更精確地定位產生異常的位置。結合匯編代碼和自定義的exception filter獲得的錯誤情報寄存器狀態等各種信息,找到異常發生的直接原因是很容易的。
1. 如何生成cod文件
仍然是打開“Project →Project Settings”,選擇 C/C++ 選項卡,在“Category”欄選擇“Listing Files”然後在Listing file type欄選擇“Assembly with Machine Code”。重新編譯工程後則可以在輸出目錄看到與每一個.cpp文件同名的.cod文件。
2. Cod文件的使用
首先還是利用map文件用獲得的程序異常地址通過函數信息部分定位出錯的OBJ和函數,並同樣記錄偏移地址(用獲得的異常地址減去該函數的Rva+Base的差值)。然後,在相應的cod文件中(而不是在map文件後面的行號信息部分)來查找出錯的函數,找到如下的格式:
?OnPaint@CShowDlg @@IAEXXZ PROC NEAR ; CShowDlg::OnPaint, COMDAT
; 81 : { (←格式為:行號 : 源代碼)
00000 83 ec 64 sub esp, 100 ; 00000064H
(↑偏移地址) (↖機器碼) (↑匯編碼)
00003 56 push esi
; 82 : if (IsIconic())
(下面省略)
找到出錯的函數以後,再用偏移地址就能找到准確的異常發生的地方。然後通過源程序、匯編碼即可進行更詳盡的分析了。