[Win32]一個調試器的實現(三)異常

來源:互聯網
上載者:User

標籤:style   blog   http   io   os   使用   ar   strong   for   

[Win32]一個調試器的實現(三)異常

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

這回接著處理上一篇文章留下的問題:如何處理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。

被偵錯工具的代碼:

  1 #include <stdio.h>
 2 #include <Windows.h>
 3 
 4 int wmain() {
 5 
 6     OutputDebugString(TEXT("Warning! An exception will be thrown!"));
 7 
 8     __try {
 9 
10         int a = 0;
11         int b = 10 / a;
12 
13     }
14     __except(EXCEPTION_EXECUTE_HANDLER) {
15 
16         OutputDebugString(TEXT("Entered exception handler."));
17     }
18 } 

 

調試器的OnException函數代碼:

 

  1 void OnException(const EXCEPTION_DEBUG_INFO* pInfo) {
 2 
 3     std::wcout << TEXT("An exception was occured.") << std::endl
 4                << TEXT("Exception code: ") << std::hex << std::uppercase << std::setw(8) 
 5                << std::setfill(L‘0‘) << pInfo->ExceptionRecord.ExceptionCode << std::dec << std::endl;
 6 
 7     if (pInfo->dwFirstChance == TRUE) {
 8 
 9         std::wcout << TEXT("First chance.") << std::endl;
10     }
11     else {
12 
13         std::wcout << TEXT("Second chance.") << std::endl;
14     }
15 } 

 

運行調試器程式,會看到它進入了一個死迴圈,不斷輸出“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++的異常處理,又會出現什麼現象呢?下面幾個問題留給大家去解決:

 

③被偵錯工具的代碼如下:

  1 #include <stdio.h>
 2 #include <Windows.h>
 3 
 4 int wmain() {
 5 
 6     OutputDebugString(TEXT("Warning! An exception will be thrown!"));
 7 
 8     try {
 9 
10         throw 9;
11     }
12     catch(int ex) {
13 
14         OutputDebugString(TEXT("Entered exception handler."));
15     }
16 } 

 

分別以DBG_CONTINUE和DBG_EXCEPTION_NOT_HANDLED調用ContinueDebugEvent,仔細觀察調試器的輸出,解釋一下為什麼會這樣。

 

④將上例的代碼改成這樣:

  1 #include <stdio.h>
 2 #include <Windows.h>
 3 
 4 int wmain() {
 5 
 6     OutputDebugString(TEXT("Warning! An exception will be thrown!"));
 7 
 8     try {
 9 
10         throw 9;
11 
12         OutputDebugString(TEXT("Will this message be shown?"));
13     }
14     catch(int ex) {
15 
16         OutputDebugString(TEXT("Entered exception handler."));
17     }
18 } 

 

分別以DBG_CONTINUE和DBG_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_EVENT和OUTPUT_DEBUG_STRING_EVENT之外的調試事件,DBG_CONTINUE和DBG_EXCEPTION_NOT_HANDLED的作用是一樣的,都是繼續被調試進程的執行,兩者沒有什麼不同。

 

範例程式碼

這次的範例程式碼添加了一個全域變數g_continueStatus,在調用ContinueDebugEvent時以它作為第三個參數。OnException和OnOutputDebugString函數都會修改這個值。對於異常,第一次接收時以DBG_EXCEPTION_NOT_HANDLED繼續被調試進程執行,第二次接收時以DBG_CONTINUE繼續其執行。

http://files.cnblogs.com/zplutor/MiniDebugger3.rar

[Win32]一個調試器的實現(三)異常

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.