try except and try catch ZZ

從本篇文章開始,將全面闡述__try,__except,__finally,__leave異常模型機制,它也即是Windows系列操作系統平臺上提供的SEH模型。主人公阿愚將在這裏與大家分享SEH的學習過程和經驗總結。

  SEH有兩項非常強大的功能。當然,首先是異常處理模型了,因此,這篇文章首先深入闡述SEH提供的異常處理模型。另外,SEH還有一個特別強大的功能,這將在下一篇文章中進行詳細介紹。

try-except入門

  SEH的異常處理模型主要由try-except語句來完成,它與標準C++所定義的異常處理模型非常類似,也都是可以定義出受監控的代碼模塊,以及定義異常處理模塊等。還是老辦法,看一個例子先,代碼如下:

//seh-test.c
#include <stdio.h>

void main()
{
puts("hello");
// 定義受監控的代碼模塊
__try
{
puts("in try");
}
//定義異常處理模塊
__except(1)
{
puts("in except");
}
puts("world");
}

  呵呵!是不是很簡單,而且與C++異常處理模型很相似。當然,爲了與C++異常處理模型相區別,VC編譯器對關鍵字做了少許變動。首先是在每個關鍵字加上兩個下劃線作爲前綴,這樣既保持了語義上的一致性,另外也盡最大可能來避免了關鍵字的有可能造成名字衝突而引起的麻煩等;其次,C++異常處理模型是使用catch關鍵字來定義異常處理模塊,而SEH是採用__except關鍵字來定義。並且,catch關鍵字後面往往好像接受一個函數參數一樣,可以是各種類型的異常數據對象;但是__except關鍵字則不同,它後面跟的卻是一個表達式(可以是各種類型的表達式,後面會進一步分析)。

try-except進階

  與C++異常處理模型很相似,在一個函數中,可以有多個try-except語句。它們可以是一個平面的線性結構,也可以是分層的嵌套結構。例程代碼如下:

// 例程1
// 平面的線性結構
#include <stdio.h>

void main()
{
puts("hello");
__try
{
puts("in try");
}
__except(1)
{
puts("in except");
}

// 又一個try-except語句
__try
{
puts("in try");
}
__except(1)
{
puts("in except");
}

puts("world");
}


// 例程2
// 分層的嵌套結構
#include <stdio.h>

void main()
{
puts("hello");
__try
{
puts("in try");
// 又一個try-except語句
__try
{
puts("in try");
}
__except(1)
{
puts("in except");
}
}
__except(1)
{
puts("in except");
}

puts("world");
}

// 例程3
// 分層的嵌套在__except模塊中
#include <stdio.h>

void main()
{
puts("hello");
__try
{
puts("in try");
}
__except(1)
{
// 又一個try-except語句
__try
{
puts("in try");
}
__except(1)
{
puts("in except");
}

puts("in except");
}

puts("world");
}
1. 受監控的代碼模塊被執行(也即__try定義的模塊代碼);
  2. 如果上面的代碼執行過程中,沒有出現異常的話,那麼控制流將轉入到__except子句之後的代碼模塊中;
  3. 否則,如果出現異常的話,那麼控制流將進入到__except後面的表達式中,也即首先計算這個表達式的值,之後再根據這個值,來決定做出相應的處理。這個值有三種情況,如下:
  EXCEPTION_CONTINUE_EXECUTION (–1) 異常被忽略,控制流將在異常出現的點之後,繼續恢復運行。
  EXCEPTION_CONTINUE_SEARCH (0) 異常不被識別,也即當前的這個__except模塊不是這個異常錯誤所對應的正確的異常處理模塊。系統將繼續到上一層的try-except域中繼續查找一個恰當的__except模塊。
  EXCEPTION_EXECUTE_HANDLER (1) 異常已經被識別,也即當前的這個異常錯誤,系統已經找到了並能夠確認,這個__except模塊就是正確的異常處理模塊。控制流將進入到__except模塊中。

try-except深入

  上面的內容中已經對try-except進行了全面的瞭解,但是有一點還沒有闡述到。那就是如何在__except模塊中獲得異常錯誤的相關信息,這非常關鍵,它實際上是進行異常錯誤處理的前提,也是對異常進行分層分級別處理的前提。可想而知,如果沒有這些起碼的信息,異常處理如何進行?因此獲取異常信息非常的關鍵。Windows提供了兩個API函數,如下:

LPEXCEPTION_POINTERS GetExceptionInformation(VOID);
DWORD GetExceptionCode(VOID);

  其中GetExceptionCode()返回錯誤代碼,而GetExceptionInformation()返回更全面的信息,看它函數的聲明,返回了一個LPEXCEPTION_POINTERS類型的指針變量。那麼EXCEPTION_POINTERS結構如何呢?如下,

typedef struct _EXCEPTION_POINTERS { // exp
PEXCEPTION_RECORD ExceptionRecord;
PCONTEXT ContextRecord;
} EXCEPTION_POINTERS;

  呵呵!仔細瞅瞅,這是不是和上一篇文章中,用戶程序所註冊的異常處理的回調函數的兩個參數類型一樣。是的,的確沒錯!其中EXCEPTION_RECORD類型,它記錄了一些與異常相關的信息;而CONTEXT數據結構體中記錄了異常發生時,線程當時的上下文環境,主要包括寄存器的值。因此有了這些信息,__except模塊便可以對異常錯誤進行很好的分類和恢復處理。不過特別需要注意的是,這兩個函數只能是在__except後面的括號中的表達式作用域內有效,否則結果可能沒有保證(至於爲什麼,在後面深入分析異常模型的實現時候,再做詳細闡述)。看一個例程吧!代碼如下:

#include <windows.h>
#include <stdio.h>

int exception_access_violation_filter(LPEXCEPTION_POINTERS p_exinfo)
{
if(p_exinfo->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION)
{
printf("存儲保護異常/n");
return 1;
}
else return 0;
}

int exception_int_divide_by_zero_filter(LPEXCEPTION_POINTERS p_exinfo)
{
if(p_exinfo->ExceptionRecord->ExceptionCode == EXCEPTION_INT_DIVIDE_BY_ZERO)
{
printf("被0除異常/n");
return 1;
}
else return 0;
}

void main()
{
puts("hello");
__try
{
__try
{
int* p;

// 下面將導致一個異常
p = 0;
*p = 45;
}
// 注意,__except模塊捕獲一個存儲保護異常
__except(exception_access_violation_filter(GetExceptionInformation()))
{
puts("內層的except塊中");
}
}
// 注意,__except模塊捕獲一個被0除異常
__except(exception_int_divide_by_zero_filter(GetExceptionInformation()))
{
puts("外層的except塊中");
}

puts("world");
}

上面的程序運行結果如下:
hello
存儲保護異常
內層的except塊中
world
Press any key to continue

  呵呵!感覺不錯,大家可以在上面的程序基礎之上改動一下,讓它拋出一個被0除異常,看程序的運行結果是不是如預期那樣。

  最後還有一點需要闡述,在C++的異常處理模型中,有一個throw關鍵字,也即在受監控的代碼中拋出一個異常,那麼在SEH異常處理模型中,是不是也應該有這樣一個類似的關鍵字或函數呢?是的,沒錯!SEH異常處理模型中,對異常劃分爲兩大類,第一種就是上面一些例程中所見到的,這類異常是系統異常,也被稱爲硬件異常;還有一類,就是程序中自己拋出異常,被稱爲軟件異常。怎麼拋出呢?還是Windows提供了的API函數,它的聲明如下:

VOID RaiseException(
DWORD dwExceptionCode, // exception code
DWORD dwExceptionFlags, // continuable exception flag
DWORD nNumberOfArguments, // number of arguments in array
CONST DWORD *lpArguments // address of array of arguments
);

  很簡單吧!實際上,在C++的異常處理模型中的throw關鍵字,最終也是對RaiseException()函數的調用,也即是說,throw是RaiseException的上層封裝的更高級一類的函數,這以後再詳細分析它的代碼實現。這裏還是看一個簡單例子吧!代碼如下:

#include <windows.h>
#include <stdio.h>

int seh_filer(int code)
{
switch(code)
{
case EXCEPTION_ACCESS_VIOLATION :
printf("存儲保護異常,錯誤代碼:%x/n", code);
break;
case EXCEPTION_DATATYPE_MISALIGNMENT :
printf("數據類型未對齊異常,錯誤代碼:%x/n", code);
break;
case EXCEPTION_BREAKPOINT :
printf("中斷異常,錯誤代碼:%x/n", code);
break;
case EXCEPTION_SINGLE_STEP :
printf("單步中斷異常,錯誤代碼:%x/n", code);
break;
case EXCEPTION_ARRAY_BOUNDS_EXCEEDED :
printf("數組越界異常,錯誤代碼:%x/n", code);
break;
case EXCEPTION_FLT_DENORMAL_OPERAND :
case EXCEPTION_FLT_DIVIDE_BY_ZERO :
case EXCEPTION_FLT_INEXACT_RESULT :
case EXCEPTION_FLT_INVALID_OPERATION :
case EXCEPTION_FLT_OVERFLOW :
case EXCEPTION_FLT_STACK_CHECK :
case EXCEPTION_FLT_UNDERFLOW :
printf("浮點數計算異常,錯誤代碼:%x/n", code);
break;
case EXCEPTION_INT_DIVIDE_BY_ZERO :
printf("被0除異常,錯誤代碼:%x/n", code);
break;
case EXCEPTION_INT_OVERFLOW :
printf("數據溢出異常,錯誤代碼:%x/n", code);
break;
case EXCEPTION_IN_PAGE_ERROR :
printf("頁錯誤異常,錯誤代碼:%x/n", code);
break;
case EXCEPTION_ILLEGAL_INSTRUCTION :
printf("非法指令異常,錯誤代碼:%x/n", code);
break;
case EXCEPTION_STACK_OVERFLOW :
printf("堆棧溢出異常,錯誤代碼:%x/n", code);
break;
case EXCEPTION_INVALID_HANDLE :
printf("無效句病異常,錯誤代碼:%x/n", code);
break;
default :
if(code & (1<<29))
printf("用戶自定義的軟件異常,錯誤代碼:%x/n", code);
else
printf("其它異常,錯誤代碼:%x/n", code);
break;
}

return 1;
}

void main()
{
puts("hello");
__try
{
puts("try塊中");

// 注意,主動拋出一個軟異常
RaiseException(0xE0000001, 0, 0, 0);
}
__except(seh_filer(GetExceptionCode()))
{
puts("except塊中");
}

puts("world");
}

上面的程序運行結果如下:
hello
try塊中
用戶自定義的軟件異常,錯誤代碼:e0000001
except塊中
world
Press any key to continue

  上面的程序很簡單,這裏不做進一步的分析。我們需要重點討論的是,在__except模塊中如何識別不同的異常,以便對異常進行很好的分類處理。毫無疑問,它當然是通過GetExceptionCode()或GetExceptionInformation ()函數來獲取當前的異常錯誤代碼,實際也即是DwExceptionCode字段。異常錯誤代碼在winError.h文件中定義,它遵循Windows系統下統一的錯誤代碼的規則。每個DWORD被劃分幾個字段,如下表所示:

  例如我們可以在winbase.h文件中找到EXCEPTION_ACCESS_VIOLATION的值爲0 xC0000005,將這個異常代碼值拆開,來分析看看它的各個bit位字段的涵義。
C 0 0 0 0 0 0 5 (十六進制)
1100 0000 0000 0000 0000 0000 0000 0101 (二進制)
第3 0位和第3 1位都是1,表示該異常是一個嚴重的錯誤,線程可能不能夠繼續往下運行,必須要及時處理恢復這個異常。第2 9位是0,表示系統中已經定義了異常代碼。第2 8位是0,留待後用。第1 6 位至2 7位是0,表示是FACILITY_NULL設備類型,它代表存取異常可發生在系統中任何地方,不是使用特定設備才發生的異常。第0位到第1 5位的值爲5,表示異常錯誤的代碼。

  如果程序員在程序代碼中,計劃拋出一些自定義類型的異常,必須要規劃設計好自己的異常類型的劃分,按照上面的規則來填充異常代碼的各個字段值,如上面示例程序中拋出一個異常代碼爲0xE0000001軟件異常。

總結

  (1) C++異常模型用try-catch語法定義,而SEH異常模型則用try-except語法;

  (2) 與C++異常模型相似,try-except也支持多層的try-except嵌套。

  (3) 與C++異常模型不同的是,try-except模型中,一個try塊只能是有一個except塊;而C++異常模型中,一個try塊可以有多個catch塊。

  (4) 與C++異常模型相似,try-except模型中,查找搜索異常模塊的規則也是逐級向上進行的。但是稍有區別的是,C++異常模型是按照異常對象的類型來進行匹配查找的;而try-except模型則不同,它通過一個表達式的值來進行判斷。如果表達式的值爲1(EXCEPTION_EXECUTE_HANDLER),表示找到了異常處理模塊;如果值爲0(EXCEPTION_CONTINUE_SEARCH),表示繼續向上一層的try-except域中繼續查找其它可能匹配的異常處理模塊;如果值爲-1(EXCEPTION_CONTINUE_EXECUTION),表示忽略這個異常,注意這個值一般很少用,因爲它很容易導致程序難以預測的結果,例如,死循環,甚至導致程序的崩潰等。

   (5) __except關鍵字後面跟的表達式,它可以是各種類型的表達式,例如,它可以是一個函數調用,或是一個條件表達式,或是一個逗號表達式,或乾脆就是一個整型常量等等。最常用的是一個函數表達式,並且通過利用GetExceptionCode()或GetExceptionInformation ()函數來獲取當前的異常錯誤信息,便於程序員有效控制異常錯誤的分類處理。

   (6) SEH異常處理模型中,異常被劃分爲兩大類:系統異常和軟件異常。其中軟件異常通過RaiseException()函數拋出。RaiseException()函數的作用類似於C++異常模型中的throw語句。

發佈了40 篇原創文章 · 獲贊 0 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章