天天看點

windows使用者态程式排錯——異常處理

關于異常

1、C++标準異常:

c++提供了一種異常的處理方法:try()catch();

不知道小夥伴有沒有這樣的疑惑:1)為何我們要捕獲異常?2)何時使用異常處理?3)異常處理會不會降低程式性能?4)如何處理異常呢?

1)為何需要捕獲異常?何時抛出異常?

異常處理就是處理程式中的錯誤。

異常,通俗來說,對于被調用的函數,如果說因為調用者所引發的錯誤,且無法繼續執行時,需要通知調用者,發生錯誤無法執行了;讓上層的邏輯去處理它。

異常處理,對于程式來說,可以知道什麼出錯了?哪裡出錯了?以及為什麼出錯了?這可以作為程式的一大調試手段。

而且,作為一個底層函數,出了錯誤,如果需要return,則需要定義好一系列的錯誤類型,讓上層的調用程式知道具體的錯誤;當存在大量的if else時,這些錯誤處理代碼與正常的業務代碼就會糾纏在一起,為了編譯開發維護,此時你就該使用try catch處理異常了。

異常處理從另一個角度看,需要調用者自己處理可能發生的錯誤,比如說除數為0,就是需要調用者自己排除才對,如果通過if語句檢查,并return 錯誤碼,如果調用者沒有檢查傳回值呢?是以使用異常處理,可以強制方法調用值處理可能發生的錯誤。

2)何時使用異常處理呢?

這裡引用C++之父的話,一個庫的作者可以檢測出發生了運作時錯誤,但一般不知道怎麼處理它們。而異常的基本目的就是處理這些錯誤,對自身無法處理的錯誤,抛出一個異常,讓它的調用者能夠處理這個問題。

從前面的描述,大緻可以認為:對于可預測的異常情況,像被除數為0是可以用if、else的進行處理,使用exception是為了,讓if/else 純粹的去操作業務邏輯 ,不會應為過多的邏輯分支影響代碼的可讀性和可維護;

對于不可預測的異常情況,比如讀取檔案,序列化對象,面對種種可能引發異常的情況。此時exception 提供了統一的處理方式。

3)異常處理會不會降低性能?

那麼異常處理會不會減低性能呢?從有關資料的測試中來看,C++的 try部分代碼運作并不會有明顯的性能降低問題;當出現異常,catch異常是會消耗一點點時間的,但在正常運作時,并沒有造成什麼性能上的損耗。

4)如何處理異常?

如何處理異常,即異常的基本文法和使用注意項:

<1>首先是抛出與捕獲異常:

抛出使用throw,捕獲使用try.....catch,舉例如下:

https://blog.csdn.net/daheiantian/article/details/6530318?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param

具體這裡不再贅述。

2. Win32系統提供的SEH(結構化異常處理)

所謂的結構化異常,通俗來說就是當程式出現錯誤時,作業系統會調用使用者定義的一個回調函數:

EXCEPTIO_DISPOSITION

__cdecl _except_handler( struct _EXPECTION_RECORD *ExceptionRecord, void * EstablisherFrame, sturct _CONTEXT *ContextRecord, void *DispatcherContext);

值得注意的是,此種方法未windows提供,并不能誇平台。

文法介紹:

1)try-except語句:

try-except語句可以使被保護的代碼出現異常時候進行控制,其中__except表達式括号内的值可以有以下三種值:

EXCEPTION_CONTINUE_EXECUTION(-1)異常被忽略,在發生異常的位置繼續執行。

EXCEPTION_CONTINUE_SEARCH(0)無法處理異常。繼續在調用鍊中尋找能夠處理異常的代碼。

EXCEPTION_EXECUTE_HANDLER(1)使用__except塊裡的代碼處理異常,然後在__except塊後繼續執行。

__try{
    // guarded code
}
__except ( expression ){
    // exception handler code
}
           

2)try-finally語句

在try-finally語句中,不管__try塊是正常結束還是異常結束,__finally塊的代碼總是會被執行。但在__try塊中發生異常時,需要有異常處理程式能否處理這個異常,例如外部嵌套的try-except語句,否則程式将會終止,try-finally語句并不會處理異常。當外部有except異常處理程式,則會優先執行__finally塊,再執行異常處理程式。

in

t main(){
    __try
    {
        __try
        {
            RaiseException(0xc0000005, 0, 0, 0);
        }
        __finally
        {
            cout << "__finally" << endl;
        }
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
        cout << "__except"<< endl;
    }
    system("pause");
}
           

此語句有個非常好的用法就是:在__try塊中配置設定資源,在__finally塊中判斷資源不為空時釋放它,這樣可以保證所有資源都能被安全的釋放。

3)__leave語句

由于在__try塊中使用return、goto、continue、break退出__try塊時會産生額外的開銷,這裡我們可以使用__leave退出__try塊,以避免不必要的開銷。

這裡還需注意異常相關的編譯選項:

/EHs /EHsc(預設選項)

try-catch隻處理C++異常,不捕捉結構化異常

try-except處理C++異常和結構化異常

在使用try-except捕捉到結構化異常時,try塊内的局部對象不會被自動析構

/EHa

try-catch捕捉C++異常和結構化異常

try-except處理C++異常和結構化異常

在使用try-except捕捉到結構化異常時,try塊内的局部對象會自動析構

無/EH

try catch不處理異常

try-except處理C++異常和結構化異常

在使用try-except捕捉到結構化異常時,try塊内的局部對象不會被自動析構

常用的一些API:

GetExceptionCode,擷取異常代碼,隻能在異常過濾表達式或異常處理塊中調用。

GetExceptionInformation,擷取異常相關資訊,隻能在異常過濾表達式中使用,傳回的資料儲存在轉移到異常處理塊中時不再可用。

RaiseException,觸發一個異常,自定義異常代碼最好以0xE開頭(例如0xE0000001),以避免和Windows異常代碼沖突。

AbnormalTermination,如果__try塊正常結束則傳回0(包括__leave導緻的結束),否則傳回非0(包括return,goto,continue,break導緻的結束),隻能在__finally塊中調用該函數。

3. 異常時儲存關鍵資訊minidump

相信使用windows的小夥伴都接觸過windows藍屏問題,windows藍屏時,如果系統設定了記憶體轉儲,則會将奔潰時的記憶體資料存儲在檔案中,用于後續的藍屏問題分析排錯。

這種記憶體轉儲檔案,報錯了故障時所有程式的資料與狀态,我們可以通過windbg分析檔案,分析排查出具體哪個應用、驅動等導緻的作業系統奔潰問題。

這種轉儲檔案一般都是非常多,如果是完全記憶體轉儲,一般會生成和PC的記憶體一樣的轉儲檔案,檔案一般存儲在C:\\windows目錄下;是以如果作業系統的C槽空間不足,也會導緻生成轉儲檔案失敗。

那如果我們的應用程式奔潰時,是否也可以生成奔潰轉儲檔案呢?

自Windows XP以來,微軟提供了一種”minidump”的奔潰轉儲技術,它儲存了故障程序的所有線程的調用堆棧,以及局部變量的值,這種dump檔案相比于系統的轉儲檔案占用很少的位元組,通常隻有幾K位元組。通過觸發這種奔潰轉儲,開發人員可以盡可能的擷取奔潰發生時,程式運作的各種資訊。

那如何在自己的程式中內建minidump,以便程式異常是,可以将異常時刻的故障資訊儲存下來,友善後續的排查?

好在已經有現成的函數API供我們使用了,我們可以通過DbgHelp函數組提供的函數來建立自己程式的minidump:

1)MiniDumpWriteDump函數

BOOL MiniDumpWriteDump(

HANDLE hProcess, //程序句柄

DWORD ProcessId, //程序ID

  HANDLE hFile, //建立的檔案句柄

  MINIDUMP_TYPE DumpType, //MINIDUMP_TYPE

  PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam,

  PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam,

  PMINIDUMP_CALLBACK_INFORMATION CallbackParam

);
           

minidump中應該包含哪些資料?主要就是取決于DumpType變量。

常用的DumpType值:

MiniDumpNormal:值為0,表示一組基礎的資料集合。

MiniDumpWithFullMemory:值為2, 表示包含程序位址空間素有可讀頁面的内容。

這樣我們可以實作一個基礎的寫minidump方法:

void CreateMiniDump( EXCEPTION_POINTERS* pep )
{
  // Open the file
  HANDLE hFile = CreateFile( _T("MiniDump.dmp"), GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL );
  if( ( hFile != NULL ) && ( hFile != INVALID_HANDLE_VALUE ) )//建立檔案
  {
    // Create the minidump
    MINIDUMP_EXCEPTION_INFORMATION mdei;
    mdei.ThreadId = GetCurrentThreadId();
    mdei.ExceptionPointers = pep;//指向故障資訊
    mdei.ClientPointers = FALSE;
    MINIDUMP_TYPE mdt = MiniDumpNormal;//指定minidump收集的資訊
    BOOL rv = MiniDumpWriteDump( GetCurrentProcess(), GetCurrentProcessId(), hFile, mdt, (pep != 0) ? &mdei : 0, 0, 0 );
    if( !rv )
      _tprintf( _T("MiniDumpWriteDump failed. Error: %u \n"), GetLastError() );
    else
      _tprintf( _T("Minidump created.\n") );
    // Close the file
    CloseHandle( hFile );
  }
  else
  {
    _tprintf( _T("CreateFile failed. Error: %u \n"), GetLastError() );
  }
}
           

2)MiniDumpCallback函數

如果MINIDUMP_TYPE不能滿足我們定制minidump内容的需要,我們可以使用MiniDumpCallback函數。這是一個使用者定義的回調函數,MiniDumpWriteDump會調用它,讓使用者來決定是否把某些資料放到minidump中。通過這個函數,我們可以完成這些功能。

具體可見:https://www.cnblogs.com/lidabo/p/3635960.html

基于上述API,我們就可以在自己的代碼中內建minidump了:

可以使用Windows的SEH的API:SetUnhandledExceptionFilter,設定一個異常過濾器,這樣每當一個SEH發生時,如果系統找不到合适的處理代碼,就會調用設定的過濾函數,進行處理。

在我們設定的異常處理函數中,我們就可以使用上面的minidump,儲存目前程式的dump檔案。

繼續閱讀