1. 程式人生 > >[Win32]一個偵錯程式的實現(三)異常

[Win32]一個偵錯程式的實現(三)異常

這回接著處理上一篇文章留下的問題:如何處理EXCEPTION_DEBUG_EVENT這類除錯事件。這類除錯事件是偵錯程式與被除錯程序進行互動的最主要手段,在後面的文章中你會看到偵錯程式如何使用它完成斷點、單步執行等操作。所以,關於這類除錯事件的處理很自由,偵錯程式的作者可以根據需要進行不同的處理。但是,在對其進行處理之前必須要了解一些關於異常的知識,這也是本文的重點。(本文的內容參考了《軟體除錯》一書)

異常的分類

根據異常發生時是否可以恢復執行,可以將異常分為三種類型,分別是錯誤異常,陷阱異常以及中止異常。

錯誤異常和陷阱異常一般都可以修復,並且在修復後程序可以恢復執行。兩者的不同之處在於,錯誤異常恢復執行時,是從引發異常的那條指令開始執行;而陷阱異常是從引發異常那條指令的下一條指令開始執行。例如下面的三條指令:

i1

i2

i3

若i2引發了一個錯誤異常,恢復執行時是從i2開始執行;若引發的是陷阱異常,恢復執行時是從i3開始執行。

中止異常屬於嚴重的錯誤,程式不可以再繼續執行。

根據異常產生的原因,可以將異常分為硬體異常和軟體異常。硬體異常即由CPU引發的異常,Windows定義了以下的硬體異常程式碼:

異常

描述

EXCEPTION_ACCESS_VIOLATION

0xC0000005

程式企圖讀寫一個不可訪問的地址時引發的異常。例如企圖讀取0地址處的記憶體。

EXCEPTION_ARRAY_BOUNDS_EXCEEDED

0xC000008C

陣列訪問越界時引發的異常。

EXCEPTION_BREAKPOINT

0x80000003

觸發斷點時引發的異常。

EXCEPTION_DATATYPE_MISALIGNMENT

0x80000002

程式讀取一個未經對齊的資料時引發的異常。

EXCEPTION_FLT_DENORMAL_OPERAND

0xC000008D

如果浮點數操作的運算元是非正常的,則引發該異常。所謂非正常,即它的值太小以至於不能用標準格式表示出來。

EXCEPTION_FLT_DIVIDE_BY_ZERO

0xC000008E

浮點數除法的除數是0時引發該異常。

EXCEPTION_FLT_INEXACT_RESULT

0xC000008F

浮點數操作的結果不能精確表示成小數時引發該異常。

EXCEPTION_FLT_INVALID_OPERATION

0xC0000090

該異常表示不包括在這個表內的其它浮點數異常。

EXCEPTION_FLT_OVERFLOW

0xC0000091

浮點數的指數超過所能表示的最大值時引發該異常。

EXCEPTION_FLT_STACK_CHECK

0xC0000092

進行浮點數運算時棧發生溢位或下溢時引發該異常。

EXCEPTION_FLT_UNDERFLOW

0xC0000093

浮點數的指數小於所能表示的最小值時引發該異常。

EXCEPTION_ILLEGAL_INSTRUCTION

0xC000001D

程式企圖執行一個無效的指令時引發該異常。

EXCEPTION_IN_PAGE_ERROR

0xC0000006

程式要訪問的記憶體頁不在實體記憶體中時引發的異常。

EXCEPTION_INT_DIVIDE_BY_ZERO

0xC0000094

整數除法的除數是0時引發該異常。

EXCEPTION_INT_OVERFLOW

0xC0000095

整數操作的結果溢位時引發該異常。

EXCEPTION_INVALID_DISPOSITION

0xC0000026

異常處理器返回一個無效的處理的時引發該異常。

EXCEPTION_NONCONTINUABLE_EXCEPTION

0xC0000025

發生一個不可繼續執行的異常時,如果程式繼續執行,則會引發該異常。

EXCEPTION_PRIV_INSTRUCTION

0xC0000096

程式企圖執行一條當前CPU模式不允許的指令時引發該異常。

EXCEPTION_SINGLE_STEP

0x80000004

標誌暫存器的TF位為1時,每執行一條指令就會引發該異常。主要用於單步除錯。

EXCEPTION_STACK_OVERFLOW

0xC00000FD

棧溢位時引發該異常。

雖然異常程式碼有很多,而且有一些不容易理解,但其中的大部分異常在使用高階語言程式設計時幾乎不會遇到。比較常見的異常有EXCEPTION_ACCESS_VIOLATION,EXCEPTION_INT_DIVIDE_BY_ZERO 和EXCEPTION_STACK_OVERFLOW。

軟體異常即程式呼叫RaiseException函式引發的異常,C++的throw語句最終也是呼叫該函式來丟擲異常的。軟體異常的異常程式碼可以在呼叫RaiseException時由程式設計師任意指定。通過throw語句丟擲的異常的異常程式碼是由編譯器指定的,對於Visual C++的編譯器來說,異常程式碼總是0xE06D7363,對應“.msc”的ASCII碼。

硬體異常和軟體異常都可以通過Windows提供的結構化異常處理機制來捕捉和處理,這種處理機制可以讓程式在發生異常的地方繼續執行,或者轉到異常處理塊內執行。而C++提供的異常處理機制只能捕捉和處理由throw語句丟擲的異常,簡單地說,這是通過檢查異常程式碼是否0xE06D7363來決定的。另外,C++的異常處理機制只能轉到異常處理塊中執行,而不能在異常發生的地方繼續執行。實際上C++的異常處理是對Windows的結構化異常處理的包裝。

異常的分發

一個異常一旦發生了,就要經歷一個複雜的分發過程。一般來說,一個異常有以下幾種可能的結果:

1.異常未被處理,程式因“應用程式錯誤”退出。

2.異常被偵錯程式處理了,程式在發生異常的地方繼續執行(具體取決於是錯誤異常還是陷阱異常)。

3.異常被程式內的異常處理器處理了,程式在發生異常的地方繼續執行,或者轉到異常處理塊內繼續執行。

下面來看一下異常的分發過程。為了突出重點,這裡省略了很多細節:

1.程式發生了一個異常,Windows捕捉到這個異常,並轉入核心態執行。

2.Windows檢查發生異常的程式是否正在被除錯,如果是,則傳送一個EXCEPTION_DEBUG_EVENT除錯事件給偵錯程式,這是偵錯程式第一次收到該事件;如果否,則跳到第4步。

3.偵錯程式收到異常除錯事件之後,如果在呼叫ContinueDebugEvent時第三個引數為DBG_CONTINUE,即表示偵錯程式已處理了該異常,程式在發生異常的地方繼續執行,異常分發結束;如果第三個引數為DBG_EXCEPTION_NOT_HANDLED,即表示偵錯程式沒有處理該異常,跳到第4步。

4.Windows轉回到使用者態中執行,尋找可以處理該異常的異常處理器。如果找到,則進入異常處理器中執行,然後根據執行的結果繼續程式的執行,異常分發結束;如果沒找到,則跳到第5步。

5.Windows又轉回核心態中執行,再次檢查發生異常的程式是否正在被除錯,如果是,則再次傳送一個EXCEPTION_DEBUG_EVENT除錯事件給偵錯程式,這是偵錯程式第二次收到該事件;如果否,跳到第7步。

6.偵錯程式第二次處理該異常,如果呼叫ContinueDebugEvent時第三個引數為DBG_CONTINUE,程式在發生異常的地方繼續執行,異常分發結束;如果第三個引數為DBG_EXCEPTION_NOT_HANDLED,跳到第7步。

7.異常沒有被處理,程式以“應用程式錯誤”結束。

下面的流程圖表達了這個過程:

下面使用幾個例子來加深對異常分發過程的理解。偵錯程式使用的是上一篇文章的示例程式碼。如果你已熟悉了異常分發的過程,那麼可以略過這部分不看。

①引發硬體異常,在收到異常除錯事件的時候以DBG_CONTINUE呼叫ContinueDebugEvent。

被除錯程式的程式碼:

#include <stdio.h>
#include <Windows.h>

int wmain() {

    OutputDebugString(TEXT("Warning! An exception will be thrown!"));

    __try {

        int a = 0;
        int b = 10 / a;

    }
    __except(EXCEPTION_EXECUTE_HANDLER) {

        OutputDebugString(TEXT("Entered exception handler."));
    }
}

偵錯程式的OnException函式程式碼:

void OnException(const EXCEPTION_DEBUG_INFO* pInfo) {

    std::wcout << TEXT("An exception was occured.") << std::endl
               << TEXT("Exception code: ") << std::hex << std::uppercase << std::setw(8) 
               << std::setfill(L'0') << pInfo->ExceptionRecord.ExceptionCode << std::dec << std::endl;

    if (pInfo->dwFirstChance == TRUE) {

        std::wcout << TEXT("First chance.") << std::endl;
    }
    else {

        std::wcout << TEXT("Second chance.") << std::endl;
    }
}

執行偵錯程式程式,會看到它進入了一個死迴圈,不斷輸出“An exception was occurred…”資訊,而且一直都是“First chance.”。結合上面的流程圖來看這個過程:我們以DBG_CONTINUE繼續被除錯程序執行,意味著我們已經處理了該異常,被除錯程序從發生異常的地方開始繼續執行。由於EXCEPTION_INT_DIVIDE_BY_ZERO是一個錯誤異常,int b = 10 / a這條語句會再次執行。然而實際上偵錯程式並沒有進行任何處理異常的操作,這條語句還是會引發異常。就這樣周而復始,陷入了死迴圈。從這個例子也看出,即使引發異常的語句被一個__try塊包圍,最先捕獲到異常的卻是偵錯程式。

②引發硬體異常,在收到異常除錯事件的時候以DBG_EXCEPTION_NOT_HANDLED呼叫ContinueDebugEvent

仍然使用上面例子的程式碼,但是將ContinueDebugEvent的第三個引數改成DBG_EXCEPTION_NOT_HANDLED。執行偵錯程式,這次只輸出了一次“An exception was occurred…”資訊,後面接著被除錯程序的輸出資訊,表明被除錯程序的異常處理器被執行了。過程:我們以DBG_EXCEPTION_NOT_HANDLED繼續被除錯程序的執行,意味著異常未被處理,所以Windows尋找異常處理器。由於存在異常處理器,而且它返回EXCEPTION_EXECUTE_HANDLER,因此被除錯程序進入了異常處理器執行。如果將EXCEPTION_EXECUTE_HANDLER改成EXCEPTION_CONTINUE_EXECUTION,那麼被除錯程序就會再次執行引發異常的語句,結果也是陷入一個死迴圈。

假如我們將__try__except塊去掉,那麼將沒有異常處理器處理異常,偵錯程式會第二次收到異常除錯資訊。如果仍然以DBG_EXCEPTION_NOT_HANDLED呼叫ContinueDebugEvent,被除錯程序就會退出;如果以DBG_CONTINUE進行呼叫,那麼被除錯程序繼續執行,結果又是陷入死迴圈。

上面的兩個例子使用了硬體異常以及Windows結構化異常處理。如果是使用軟體異常以及C++的異常處理,又會出現什麼現象呢?下面幾個問題留給大家去解決:

③被除錯程式的程式碼如下:

#include <stdio.h>
#include <Windows.h>

int wmain() {

    OutputDebugString(TEXT("Warning! An exception will be thrown!"));

    try {

        throw 9;
    }
    catch(int ex) {

        OutputDebugString(TEXT("Entered exception handler."));
    }
}

分別以DBG_CONTINUEDBG_EXCEPTION_NOT_HANDLED呼叫ContinueDebugEvent,仔細觀察偵錯程式的輸出,解釋一下為什麼會這樣。

④將上例的程式碼改成這樣:
#include <stdio.h>
#include <Windows.h>

int wmain() {

    OutputDebugString(TEXT("Warning! An exception will be thrown!"));

    try {

        throw 9;

        OutputDebugString(TEXT("Will this message be shown?"));
    }
    catch(int ex) {

        OutputDebugString(TEXT("Entered exception handler."));
    }
}

分別以DBG_CONTINUEDBG_EXCEPTION_NOT_HANDLED呼叫ContinueDebugEvent,仔細觀察偵錯程式的輸出,解釋一下為什麼會這樣。

⑤根據上面兩個例子回答:軟體異常屬於錯誤異常還是陷阱異常?

再談OutputDebugString

在上面的第一、第二個例子中,你可能會注意到一個小問題:第一個例子中,被除錯程序用OutputDebugString輸出的字串只顯示一次;但在第二個例子中卻顯示兩次。這是因為OutputDebugString在內部呼叫了RaiseException,它本質上是通過軟體異常來工作的,Windows將它引發的異常轉換成了OUTPUT_DEBUG_STRING_EVENT除錯事件來通知偵錯程式。

所以,當我們以DBG_CONTINUE呼叫ContinueDebugEvent時,OutputDebugString的異常被處理了,偵錯程式只收到一次OUTPUT_DEBUG_STRING_EVENT事件;以DBG_EXCEPTION_NOT_HANDLED呼叫時,該異常未被處理,偵錯程式會第二次收到OUTPUT_DEBUG_STRING_EVENT。這就是為什麼在第二個例子中這些資訊會輸出兩次了。

那麼,為什麼在偵錯程式第二次處理OUTPUT_DEBUG_STRING_EVENT之後以DBG_EXCEPTION_NOT_HANDLED呼叫ContinueDebugEvent時,被除錯程序不會結束呢?這隻能說是因為OutputDebugString引發的異常屬於特殊的異常,Windows對它有特別的處理。OutputDebugString的目的是為了向偵錯程式輸出除錯資訊,而不是為了報告一個錯誤,如果被除錯程序在呼叫OutputDebugString之後立即結束了,肯定會讓人感到莫名奇妙。

EXCPETION_DEBUG_EVENT的處理

好了,上面進行了那麼多鋪墊,終於可以回到正題了。EXCEPTION_DEBUG_INFO結構體描述了該類除錯事件的詳細資訊。dwFirstChance指明是第一次還是第二次接收到同一個異常,為1是第一次,為0是第二次。ExceptionRecord則是一個EXCEPTION_RECORD結構體,包含了異常的詳細資訊:

ExceptionCode 異常程式碼

ExceptionFlags 異常標誌,為0表示這是一個可繼續執行的異常,否則為EXCEPTION_NONCONTINUABLE

ExceptionRecord 指向另一個異常的指標。一個異常可以巢狀另一個異常,形成鏈式結構。

ExceptionAddress 引發異常的指令地址。

ExceptionInformation 如果異常需要包含更多資訊,則用該陣列來儲存這些資訊。

NumberParameters ExceptionInformation陣列中元素的個數。

由上文的描述可以看出,ContinueDebugEvent的第三個引數對於偵錯程式的行為有很大的影響,所以我們不能僅僅使用DBG_CONTINUE或者DBG_EXCEPTION_NOT_HANDLED,而應該根據異常程式碼執行不同的操作,然後使用適當的值呼叫ContinueDebugEvent。例如,遇到除零異常,我們可以將除數的值改為非零,然後以DBG_CONTINUE繼續被除錯程序的執行。又如,我們希望只在異常沒有被異常處理器處理的情況下才對其處理,那麼我們可以在第一次接收到異常除錯事件時以DBG_EXCEPTION_NOT_HANDLED繼續執行,在第二次接收到異常除錯事件時才對其進行處理。

最後說明一下,對於EXCEPTION_DEBUG_EVENTOUTPUT_DEBUG_STRING_EVENT之外的除錯事件,DBG_CONTINUEDBG_EXCEPTION_NOT_HANDLED的作用是一樣的,都是繼續被除錯程序的執行,兩者沒有什麼不同。

示例程式碼

這次的示例程式碼添加了一個全域性變數g_continueStatus,在呼叫ContinueDebugEvent時以它作為第三個引數。OnExceptionOnOutputDebugString函式都會修改這個值。對於異常,第一次接收時以DBG_EXCEPTION_NOT_HANDLED繼續被除錯程序執行,第二次接收時以DBG_CONTINUE繼續其執行。

作者:Zplutor
出處:http://www.cnblogs.com/zplutor/
本文版權歸作者和部落格園共有,歡迎轉載。但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連線,否則保留追究法律責任的權利。