1. 程式人生 > >VC++ 崩潰處理以及列印呼叫堆疊

VC++ 崩潰處理以及列印呼叫堆疊

我們在程式釋出後總會面臨崩潰的情況,這個時候一般很難重現或者很難定位到程式崩潰的位置,之前有方法在程式崩潰的時候記錄dump檔案然後通過windbg來分析。那種方法對開發人員的要求較高,它需要程式設計師理解記憶體、暫存器等等一系列概念還需要手動載入對應的符號表。Java、Python等等語言在崩潰的時候都會列印一條異常的堆疊資訊並告訴使用者那塊出錯了,根據這個資訊程式設計師可以很容易找到對應的程式碼位置並進行處理,而C/C++則會彈出一個框告訴使用者程式崩潰了,二者對比來看,C++似乎對使用者太不友好了,而且根據它的彈框很難找到對應的問題,那麼有沒有可能使c++像Java那樣列印異常的堆疊呢?這個自然是可能的,本文就是要討論如何在Windows上實現類似的功能

異常處理

一般當程式發生異常時,使用者程式碼停止執行,並將CPU的控制權轉交給作業系統,作業系統接到控制權後,將當前執行緒的環境儲存到結構體CONTEXT中,然後查詢針對此異常的處理函式。系統利用結構EXCEPTION_RECORD儲存了異常描述資訊,它與CONTEXT一同構成了結構體EXCEPTION_POINTERS,一般在異常處理中經常使用這個結構體。
異常資訊EXCEPTION_RECORD的定義如下:

typedef struct _EXCEPTION_RECORD
{
   DWORD ExceptionCode;  //異常碼
   DWORD ExceptionFlags;  //標誌異常是否繼續,標誌異常處理完成後是否接著之前有問題的程式碼
struct _EXCEPTION_RECORD* ExceptionRecord; //指向下一個異常節點的指標,這是一個連結串列結構 PVOID ExceptionAddress; //異常發生的地址 DWORD NumberParameters; //異常附加資訊 ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; //異常的字串 } EXCEPTION_RECORD, *PEXCEPTION_RECORD;

Windows平臺提供的這一套異常處理的機制,我們叫它結構化異常處理(SEH),它的處理過程一般如下:
1. 如果程式是被除錯執行的(比如我們在VS編譯器中除錯執行程式),當異常發生時,系統首先將異常資訊交給除錯程式,如果除錯程式處理了那麼程式繼續執行,否則系統便在發生異常的執行緒棧中查詢可能的處理程式碼。若找到則處理異常,並繼續執行程式
2. 如果線上程棧中沒有找到,則再次通知除錯程式,如果這個時候仍然不能處理這個異常,那麼作業系統會對異常程序預設處理,這個時候一般都是直接彈出一個錯誤的對話方塊然後終止程式。

系統在每個執行緒的堆疊環境中都維護了一個SEH表,表中是使用者註冊的異常型別以及它對應的處理函式,每當使用者在函式中註冊新的異常處理函式,那麼這個資訊會被儲存在連結串列的頭部,也就是說它是採用頭插法來插入新的處理函式,從這個角度上來說,我們可以很容易理解為什麼在一般的高階語言中一般會先找與try塊最近的catch塊,然後在找它的上層catch,由裡到外依次查詢。與try塊最近的catch是最後註冊的,由於採用的是頭插法,自然它會被首先處理。

在Windows中針對異常處理,擴充套件了__try__except 兩個操作符,這兩個操作符與c++中的try和catch非常相似,作用也基本類似,它的一般的語法結構如下:

__try
{
  //do something
}
__except(filter)
{
  //handle
}

使用 __try__except 的時候它主要分為3個部分,分別為:保護程式碼體、過濾表示式、異常處理塊
1. 保護程式碼體一般是try中的語句,它值被保護的程式碼,也就是說我們希望處理那個程式碼塊產生的異常
2. 過濾表示式是 except後面擴號中的值,它只能是3個值中的一個,EXCEPTION_CONTINUE_SEARCH繼續向下查詢異常處理,也就是說這裡的異常處理塊不處理這種異常,EXCEPTION_CONTINUE_EXECUTION表示異常已被處理,這個時候可以繼續執行直線產生異常的程式碼,EXCEPTION_EXECUTE_HANDLER表示異常已被處理,此時直接跳轉到except裡面的程式碼塊中,這種方式下它的執行流程與一般的異常處理的流程類似.
3. 異常處理塊,指的是except下面的擴號中的程式碼塊.

注意:我們說過濾表示式只能是這三個值中的一個,但是沒有說這裡一定得填這三個值,它還支援函式或者其他的表示式型別,只要函式或者表示式的返回值是這三個值中的一個即可。

上述的方式也有他的侷限性,也就是說它只能保護我們指定的程式碼,如果是在 __try 塊之外的程式碼發生了崩潰,可能還是會造成程式被kill掉,而且每個位置都需要寫上這麼些程式碼實在是太麻煩了。其實處理異常還有一種方式,那就是採用 SetUnhandledExceptionFilter來註冊一個全域性的異常處理函式來處理所有未被處理的異常,其實它的主要工作原理就是往異常處理的連結串列頭上新增一個處理函式,函式的原型如下:

LPTOP_LEVEL_EXCEPTION_FILTER WINAPI SetUnhandledExceptionFilter(__in  LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter);

它需要傳入一個函式,以便發生異常的時候呼叫這個函式,這個回撥函式的原型如下:

LONG WINAPI UnhandledExceptionFilter(
  __in  struct _EXCEPTION_POINTERS* ExceptionInfo
);

回撥函式會傳入一個表示當前堆疊和異常資訊的結構體的指標,結構的具體資訊請參考MSDN, 函式會返回一個long型的數值,這個數值為上述3個值中的一個,表示當系統呼叫了這個異常處理函式處理異常之後該如何繼續執行使用者程式碼。

SetUnhandledExceptionFilter 函式返回一個函式指標,這個指標指向連結串列的頭部,如果插入處理函式失敗那麼它將指向原來的連結串列頭,否則指向新的連結串列頭(也就是註冊的這個回撥函式的地址)

而這次要實現這麼一個能列印異常資訊和呼叫堆疊的功能就是要使用這個方法。

列印函式呼叫堆疊

關於列印堆疊的內容,這裡不再多說了,請參考本人之前寫的部落格
windows平臺呼叫函式堆疊的追蹤方法
這裡的主要思路是使用StackWalker來根據當前的堆疊環境來獲取對應的函式資訊,這個資訊需要根據符號表來生成,因此我們需要首先載入符號表,而獲取當前執行緒的環境,我們可以像我部落格中寫的那樣使用GetThreadContext來獲取,但是在異常中就簡單的多了,還記得異常處理函式的原型嗎?異常處理函式本身會帶入一個EXCEPTION_POINTERS結構的指標,而這個結構中就包含了異常堆疊的資訊。

還有一些需要注意的問題,我把它放到實現那塊了,請小心的往下看^_^

實現

實現部分的原始碼我放到了github上,地址

這個專案中主要分為兩個類CBaseException,主要是對異常的一個簡單的封裝,提供了我們需要的一些功能,比如獲取載入的模組的資訊,獲取呼叫的堆疊,以及解析發生異常時的相關資訊。而這些的基礎都在CStackWalker中。
使用上,我把CBaseException中的大部分函式都定義成了virtual 允許進行重寫。因為具體我還沒想好這塊後續會需要進行哪些擴充套件。但是裡面最主要的功能是OutputString函式,這個函式是用來進行資訊輸出的,預設CBaseException是將資訊輸出到控制檯上,後續可以過載這個函式把資料輸出到日誌中。

CBaseException 類

CBaseException 主要是用來處理異常,在程式碼裡面我提供了兩種方式來進行異常處理,第一種是通過 SetUnhandledExceptionFilter 來註冊一個全域性的處理函式,這個函式是類中的靜態函式UnhandledExceptionFilter,在這個函式中我主要根據異常的堆疊環境來初始化了一個CBaseException類,然後簡單的呼叫類的方法顯示異常與堆疊的相關資訊。第二種是通過 _set_se_translator 來註冊一個將SEH轉化為C++異常的方法,在對應的回撥中我簡單的丟擲了一個CBaseException的異常,在具體的程式碼中只要簡單的用c++的異常處理捕獲這麼一個異常即可

CBaseException 類中主要用來解析異常的資訊,裡面提供這樣功能的函式主要有3個

  1. ShowExceptionResoult: 這個函式主要是根據異常碼來獲取到異常的具體字串資訊,比如非法記憶體訪問、除0異常等等
  2. GetLogicalAddress:根據發生異常的程式碼的地址來獲取對應的模組資訊,比如它在PE檔案中屬於第幾個節,節的地址範圍等等,它在實現上首先使用 VirtualQuery來獲取對應的虛擬記憶體資訊,主要是這個模組的首地址資訊,然後解析PE檔案獲取節表的資訊,我們迴圈節表中的每一項,根據節表中的地址範圍來判斷它屬於第幾個節,注意這裡我們根據它在記憶體中的偏移計算了它在PE檔案中的偏移,具體的計算方式請參考PE檔案的相關內容.
    3.ShowRegistorInformation:獲取各個暫存器的值,這個值儲存在CONTEXT結構中,我們只需要簡單列印它就好

CStackWalker類

這個類主要實現一些基礎的功能,它主要提供了初始化符號表環境、獲取對應的呼叫堆疊資訊、獲取載入的模組資訊
在初始化符號表的時候儘可以多的遍歷了常見的幾種符號表的位置並將這些位置中的符號表載入進來,以便能更好的獲取到堆疊呼叫的情況。在獲取到對應的符號表位置後有這樣的程式碼

if (NULL != m_lpszSymbolPath)
{
        m_bSymbolLoaded = SymInitialize(m_hProcess, T2A(m_lpszSymbolPath), TRUE); //這裡設定為TRUE,讓它在初始化符號表的同時載入符號表
}

DWORD symOptions = SymGetOptions();
symOptions |= SYMOPT_LOAD_LINES;
symOptions |= SYMOPT_FAIL_CRITICAL_ERRORS;
symOptions |= SYMOPT_DEBUG;
SymSetOptions(symOptions);

return m_bSymbolLoaded;

這裡將 SymInitialize的最後一個函式置為TRUE,這個引數的意思是是否列舉載入的模組並載入對應的符號表,直接在開始的時候載入上可能會比較浪費記憶體,這個時候我們可以採用動態載入的方式,在初始化的時候先填入FALSE,然後在需要的時候自己列舉所有的模組,然後手動載入所有模組的符號表,手動載入需要呼叫SymLoadModuleEx。這裡需要提醒各位的是,這裡如果填的是FALSE的話,後續一定得自己載入模組的符號表,否則在後續呼叫SymGetSymFromAddr64的時候會得到一堆的487錯誤(也就是地址無效)
我之前就是這個問題困擾了我很久的時間。

在獲取模組的資訊時主要提供了兩種方式,一種是使用CreateToolhelp32Snapshot 函式來獲取程序中模組資訊的快照然後呼叫Module32Next 和 Module32First來列舉模組資訊,還有一種是使用EnumProcessModules來獲取所有模組的控制代碼,然後根據控制代碼來獲取模組的資訊,當然還有另外的方式,其他的方式可以參考我的這篇部落格 列舉程序中的模組

在列舉載入的模組的同時還針對每個模組呼叫了 GetModuleInformation 函式,這個函式主要有兩個功能,獲取模組檔案的版本號和獲取載入的符號表資訊。

接下來就是重頭戲了——獲取呼叫堆疊。獲取呼叫堆疊首先得獲取當前的環境,在程式碼中進行了相應的判斷,如果當前傳入的CONTEXT為NULL,則函式自己獲取當前的堆疊資訊。在獲取堆疊資訊的時候首先判斷是否為當前執行緒,如果不是那麼為了結果準確,需要先停止目標執行緒,然後獲取,否則直接使用巨集來獲取,對應的巨集定義如下:

#define GET_CURRENT_THREAD_CONTEXT(c, contextFlags) \
    do\
    {\
        memset(&c, 0, sizeof(CONTEXT));\
        c.ContextFlags = contextFlags;\
        __asm    call $+5\
        __asm    pop eax\
        __asm    mov c.Eip, eax\
        __asm    mov c.Ebp, ebp\
        __asm    mov c.Esp, esp\
} while (0)

在呼叫StackWalker時只需要關注esp ebp eip的資訊,所以這裡我們也只簡單的獲取這些暫存器的環境,而其他的就不管了。這樣有一個問題,就是我們是在CStackWalker類中的函式中獲取的這個執行緒環境,那麼這個環境裡面會包含CStackWalker::StackWalker,結果自然與我們想要的不太一樣(我們想要的是隱藏這個庫中的相關資訊,而只保留呼叫者的相關堆疊資訊)。這個問題我還沒有什麼好的解決方案。

在獲取到執行緒環境後就是簡單的呼叫StackWalker以及那堆Sym開頭的函式來獲取各種資訊了,這裡就不再詳細說明了。

至此這個功能已經實現的差不多了。庫的具體使用請參考main.cpp這個檔案,相信有這篇博文以及原始碼各位應該很容易就能夠使用它。

據說這些函式不是多執行緒安全的,我自己沒有在多執行緒環境下進行測試,所以具體它在多執行緒環境下表現如何還是個未知數,如果後續我有興趣繼續完善它的話,可能會加入多執行緒的支援。

相關推薦

VC++ 崩潰處理以及列印呼叫堆疊

我們在程式釋出後總會面臨崩潰的情況,這個時候一般很難重現或者很難定位到程式崩潰的位置,之前有方法在程式崩潰的時候記錄dump檔案然後通過windbg來分析。那種方法對開發人員的要求較高,它需要程式設計師理解記憶體、暫存器等等一系列概念還需要手動載入對應的符號表。

VC++ 崩潰處理以及打印調用堆棧

ont address 用戶 工作原理 彈出 語言 path 屬於 lan title: VC++ 崩潰處理以及打印調用堆棧 tags: [VC++, 結構化異常處理, 崩潰日誌記錄] date: 2018-08-28 20:59:54 categories: windo

列印呼叫堆疊

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!        

linux下用gdb實現程式宕機時自動列印呼叫堆疊

linux下程式執行幾天莫名其妙宕機了,不能還原現場,找到宕機原因就很無語了。 一個解決辦法是使用core檔案,但是對於大型伺服器檔案,動輒幾百M的core檔案是在有點傷不起,於是想到程式宕機時自動列印呼叫堆疊。簡單實用。

C++ 程式崩潰呼叫堆疊列印

程式現場釋出,最頭痛程式崩潰 但是日誌又找不出什麼蛛絲馬跡  win32下可以增加如下 可以在release下列印呼叫堆疊資訊   //.h #ifndef _CALL_STACK_TOOL_2017_10_30_ #define _CALL_STACK_TOOL_201

IOS 列印函式呼叫堆疊

源文 http://www.dewen.io/q/8471/Object-c%E4%B8%AD%E5%A6%82%E4%BD%95%E6%89%93%E5%8D%B0%E5%87%BD%E6%95%B0%E8%B0%83%E7%94%A8%E6%A0%88 列印呼叫

嵌入式 linux下利用backtrace追蹤函式呼叫堆疊以及定位段錯誤

一般察看函式執行時堆疊的方法是使用GDB(bt命令)之類的外部偵錯程式,但是,有些時候為了分析程式的BUG,(主要針對長時間執行程式的分析),在程式出錯時打印出函式的呼叫堆疊是非常有用的。在glibc標頭檔案"execinfo.h"中聲明瞭三個函式用於獲取當前執行緒的函式呼

Linux下利用backtrace追蹤函式呼叫堆疊以及定位段錯誤

一般察看函式執行時堆疊的方法是使用GDB(bt命令)之類的外部偵錯程式,但是,有些時候為了分析程式的BUG,(主要針對長時間執行程式的分析),在程式出錯時打印出函式的呼叫堆疊是非常有用的。 在glibc標頭檔案"execinfo.h"中聲明瞭三個函式用於獲取當前執行緒的

利用Xposed Hook列印Java函式呼叫堆疊資訊的幾種方法

在進行Android逆向分析的時候,經常需要進行動態除錯棧回溯,檢視Java函式的呼叫流程,Android的smali動態除錯又不是很方便,因此使用Android的Java Hook的方法,列印Java函式呼叫堆疊資訊輔助靜態分析。package com.xposeddemo

Android函式呼叫堆疊列印方法

RuntimeException e = new RuntimeException("leon is here"); e.fillInStackTrace(); Slog.i(TAG, "xxx" + value, e); 或者是下面的方法: if (D

PHP 列印呼叫函式入口地址(堆疊),方便調式

今天網站出現一個BUG,然後直接在資料庫類裡面寫日誌,看是哪條SQL出了問題,SQL語句到是找到了,但是不知道這條SQL語句來自何處,於是就想啊,如果能有一個辦法,檢視當前正在執行的這個方法是被哪個方法呼叫的,以及上一個方法又是哪個方法呼叫的,以此類推,找到入口地址多好啊。不過以前自己也想過,也在網上去搜過,

VC】window程式崩潰處理

程式設計師有太多的學習點,接觸不到就是一個盲點,今天看到c++在window下的崩潰處理。從此可以在程式崩潰時啟動自己的處理程式,或者友好的告訴使用者了。 #include "stdafx.h" #include <stdio.h> #includ

lua 列印函式呼叫堆疊

 local _trace = debug.traceback local _t_concat = table.concat local _log = print function print_stack(...)     local out = {'[TRACE]'}     local n = selec

Vc++除錯技巧之呼叫堆疊

除錯是程式開發者必備技巧。如果不會除錯,自己寫的程式一旦出問題,往往無從下手。本人總結10年使用VC經驗,對除錯技巧做一個粗淺的介紹。希望對大家有所幫助。 今天簡單的介紹介紹呼叫堆疊。呼叫堆疊在我的專欄的文章VC除錯入門提了一下,但是沒有詳細介紹。 首先介紹一下什麼叫呼叫

記錄程式崩潰時的呼叫堆疊

最近有個使用者遇到程式Crash問題,但我們的機器都不能重現,於是在網上搜了一把,發現有個MSJExceptionHandler類還比較好用,故整理了一下供大家參考。 這個類的使用方法很簡單,只要把這個類加入到你的工程(不管是MFC,com,dll都可以)中一起編譯就可以了

XZ_iOS之崩潰資訊的檢視,呼叫堆疊的除錯技巧

寫一個demo,製造一個崩潰資訊:嘗試給NSArray在第一個位置插入一個空物件;以此為例,查詢崩潰的原因。 下圖中的崩潰資訊有一個標準的名字叫呼叫堆疊,既然是堆疊,那麼先執行的肯定在下面,後執行的在上面。 在真機上只顯示錯誤資訊,不顯示詳細的呼叫堆疊內容

級聯式高壓變頻的單元故障處理以及FPGA的模擬內存

bmf href tex 聲明 man size b2c view ges 級聯式高壓變頻的單元故障處理以及FPGA的模擬內存 2017-04-05 01:34 123人閱讀 評論(0) 收藏 舉報 分類: 程序備忘(42) 算法(39) 高壓變頻(36)

javascript事件兼容處理以及時間冒牌、捕獲

js兼容/cancelBubble阻止事件冒泡 function cancelBubble(ev){ var ev=ev||window.event; if(ev.stopPropagation){

原生js的正負值處理以及添加回調函數

width 並且 urn div 停止 函數實現 等於 || nbsp 今天還是來說昨天的小例子,昨天實現的效果就是點擊按鈕“向前”即往前相反則往後,那麽需要細化的幾個問題就是:代碼可以簡化以及可以用一個變量來控制方向直接來看js: <script>

[離散時間信號處理學習筆記] 12. 連續時間信號的離散時間處理以及離散時間信號的連續時間處理

.cn post 包括 oat text lock let 通過 symbol 連續時間信號與離散時間信號之間的關系 下表為各符號的解釋 Symbol FT DTFT Info $x_c(t)$ $X_c(j\Omega)$ - 連續時間信號 $x[n]$ -