Windows用戶態程序高效排錯 -- 異常(Exception)和通知(Debug Event)

 理解操作系統對程序的反饋:異常(Exception)和通知(Debug Event)

本小結首先介紹異常的原理和相關資料,再舉例說明異常跟崩潰和調試是如何緊密聯繫在一起的。最後說明如何利用工具來監視異常,獲取準確的信息。

2.3.1  異常(Exception)的方方面面和一篇字字珠璣的文章

異常是CPU,操作系統和應用程序控制代碼流程的一種機制。正常情況下,代碼是順序執行的,比如下面兩行:

*p=11;

printf(“%d”,*p);

這裏應該會打印出11。 但若p指向的地址是無效地址呢?那麼這裏對*p賦值的時候,也就是CPU向對應地址做寫操作的時候,CPU就會觸發無效地址訪問的異常,接下來的printf很可能就不會執行了。

從這個簡單的例子可以看到,當程序行爲跟預期相左的時候,很可能就是異常的發生改變了程序的執行邏輯。在很多案例中,抓準異常的原因,其實就解決了問題。

異常發生的時候,由於操作系統在內核掛接了對應的CPU異常處理函數,CPU就會跳轉去執行操作系統提供的處理函數,所以printf就不一定會被執行了。在操作系統的處理函數裏面,如果檢測到發生在用戶態的程序的異常,操作系統會再把異常信息發送給用戶態進程對應的處理函數,讓用戶態程序有處理異常的機會。

用戶態程序處理完了異常,代碼會繼續執行,不過執行的次序可以是緊接着的下一個指令,比如printf,也可以跳到另外的地址開始執行,比如catch block,或者重新執行一次出錯的指令。這些都是用戶態的異常處理函數可以控制的。

如果用戶態程序沒有處理這個異常,那操作系統的默認行爲就是中止程序的執行,然後用戶可以看到給Microsoft發送錯誤報告的界面,或者乾脆就是一個紅色的框框,說某某地址上的指令在訪問某某地址的時候遭遇了訪問違例的錯誤。

除了上面的非預期異常,也可以手動觸發異常來控制執行順序,C++/C# 中的throw關鍵字就可以觸發異常。手動觸發異常需要依賴於編譯器和操作系統API來實現。

異常的類型,是通過異常代碼來標識的。比如訪問無效地址的號碼是0xc0000005,而C++異常的號碼是0xe06d7363。其他很多看似跟異常無關的東西,其實都是跟異常聯繫在一起的,比如調試的時候設置斷點,或者單步執行,都有通過break point exception來實現的。越權指令,堆棧溢出的處理也依靠異常。在Windbg幫助文件的Controlling Exceptions and Events主題裏面,有一張常用異常代碼表。

程序的行爲跟預期的不一樣,直接原因是代碼執行次序跟預期的不一樣。異常改變了代碼執行次序,比如代碼中從來都沒有什麼函數跳一個紅框框出來,說某某地址上的指令在訪問某某地址的時候遭遇了訪問違例。弄清楚異常發生的時間、地址、導致異常的指令和異常導致的結果對排錯是至關重要的。

異常如此重要,所以操作系統提供了對應的調試功能,可以使用調試器來檢視異常。異常發生後,操作系統在調用用戶態程序的異常處理函數前,會檢查當前用戶態程序是否有調試器加載。如果有,那麼操作系統會首先把異常信息發送給調試器,讓調試器有觀察異常的第一次機會,所以也叫做first chance exception,調試器處理完畢後,操作系統才讓用戶態程序來處理。

如果用戶態程序處理了這個異常,就沒調試器什麼事了。否則,程序在unhandled exception崩潰前,操作系統會給調試器第二次觀察異常的機會,所以也叫做second chance exception。

請注意,這裏的1st chance, 2nd chance是針對調試器來說的。雖然C++異常處理的時候也會有first phrase find exception handler, second phrase unwind stack這樣的概念,但是兩者是不一樣的。

操作系統提供的異常處理功能叫做 Structrued Exception Handle(SEH),C++和其他高級語言的異常處理機制都是建立在SEH上的。如果要直接使用SEH,可以在C/C++中使用__try,__except關鍵字。

關於異常處理的詳細信息,所有的來龍去脈,操作系統做了些什麼事情,C++編譯器做了些什麼事情,SEH和C++異常處理的關係,以及調試器是如何參與的,下面幾篇文章有非常詳細的介紹。

A Crash Course on the Depths of Win32™ Structured Exception Handling

http://www.microsoft.com/msj/0197/Exception/Exception.aspx

這篇文章出來後,沒見人寫第二篇了。深入淺出,字字珠璣。

RaiseException

http://msdn.microsoft.com/library/default.asp?url=/library/en-us/debug/base/raiseexception.asp

注意,上面鏈接中,remark section詳細介紹了異常處理函數是如何被分發的。

案例分析:如何讓C++像C#一樣打印出函數調用棧(callstack)

如果用C#或者Java,在異常發生後,可以獲取異常發生時刻的call stack。但是對於C++,除非使用調試器,否則是看不到的。現在用戶想儘可能少地修改代碼,讓C++程序在異常崩潰後,能夠打印出call stack,有什麼方法呢?

我的解法是直接使用SEH,加上局部變量析構函數在異常發生時候會被執行的特點來完成。這個例子當時使用VC6在Windows 2003上調試通過。當重新整理這個例子的時候,發現這段代碼在VC2005+Windows 2003 SP1上有奇怪的現象發生。如果用debug模式編譯,運行正常。如果用release模式編譯,程序會在沒有任何異常報告的情況下悄然退出。關於整個源代碼和對應的分析,請參考:

SEH,DEP, Compiler,FS:[0] and PE format

2.3.2  Adplus,抓取dump的方便工具

前面提到了dump文件能保存進程狀態,方便分析。由於dump文件記錄的是進程某一時刻的具體信息,所以保存dump的時機非常重要。比如程序崩潰,dump應該選在引發崩潰的指令執行時(也就是1st chance exception發生的時候)獲取,這樣分析dump的時候就能夠看到問題的直接原因。

Adplus是跟Windbg在同一個目錄的VBS腳本。Adplus主要是用來抓取dump文件。 詳細的信息,可以參考Windbg幫助文件中關於adplus的幫助。有下面一些常見用法:

假設我們的目標程序是test.exe:

假設test.exe運行一段時間崩潰,在test.exe啓動後崩潰前的這個時間段,運行下面的命令監視:

Adplus –crash –pn test.exe –o C:/dumps

當test.exe發生2nd chance exception崩潰的時候,adplus在C:/dumps生成full dump文件。當發生1st chance AV exception, 或者1st chance breakpoint exception的時候,adplus在C:/dumps生成mini dump文件。

也可以用:

Adplus –crash –pn test.exe –fullonfirst –o C:/dumps

差別在於,加上-fullonfirst參數後,無論是1st chance exception還是2nd chance exception,都會生成full dump文件。

假如test.exe發生deadlock,或者memory leak,並不是crash,需要獲取任意時刻的一個dump,可以用下面的命令:

Adplus –hang –pn test.exe –o C:/dumps

該命令立刻把test.exe的full dump 抓到C:/dumps下。

Adplus更靈活的方法就是用-c參數帶配置文件。在配置文件裏面,可以選擇exception發生的時間,生成的dump是mini dump還是full dump,還可以設定斷點等等。對於adplus各項參數的選用原則,在最後一章還會作進一步介紹。

案例分析:華生醫生(Dr. Watson)在什麼情況下不能記錄Dump文件

問題描述

客戶聲稱用VC開發的程序偶爾會崩潰。爲了獲取詳細信息,客戶激活了Dr. Watson,以便程序崩潰的時候可以自動獲取dump文件。但是問題再次發生後,Dr. Watson並沒有記錄dump文件。

背景知識

dump文件包含的是內存鏡像信息。在Windows系統上,dump文件分爲內核dump和用戶態dump兩種。前者一般用來分析內核相關的問題,比如驅動程序;後者一般用來分析用戶態程序的問題。如果不作說明,本書後面所指的dump都表示用戶態dump。用戶態的dump又分成mini dump和full dump。前者尺寸小,只記錄一些常用信息;後者則是把目標進程用戶態的所有內容都記錄下來。Windows提供了MiniDumpWriteDump API可供程序調用來生成mini dump。通過調試器和相關工具,可以抓取目標程序的full dump。拿到dump後,可以通過調試器檢查dump中的內容,比如call stack,memory,exception等等。關於dump和調試器的更詳細信息,後面會有更多介紹。跟Dr. Watson相關的文檔是:

Description of the Dr. Watson for Windows (Drwtsn32.exe) Tool

http://support.microsoft.com/?id=308538

Specifying the Debugger for Unhandled User Mode Exceptions

http://support.microsoft.com/?id=121434

INFO: Choosing the Debugger That the System Will Spawn

http://support.microsoft.com/?id=103861

也就是說,通過設定註冊表中的AeDebug項,可以在程序崩潰後,選擇調試器進行調試。選擇Dr. Watson就可以直接生成dump文件。

問題分析

回到這個問題,客戶並沒有獲取到dump文件,可能性有兩個:

1.         Dr. Watson工作不正常。

2.         客戶的程序根本沒有崩潰,不過是正常退出而已。

爲了測試第1點,提供瞭如下的代碼給客戶測試:

int *p=0;

*p=0;

測試上面的代碼,Dr. Watson成功地獲取了dump文件。也就是說,Dr. Watson工作是正常的。那看來客戶聲稱的崩潰可能並不是unhandled exception導致的。說不定在非預料情況下調用了ExitProcess,被客戶誤認爲是崩潰。所以,抓取信息不應該侷限於unhandled exception,而應該檢查進程退出的原因。

當程序在Windbg調試器中退出的時候,系統會觸發調試器的進程退出消息,可以在這個時候抓取dump來分析進程退出的原因。

如果讓客戶每次都先啓動Windbg,然後用Windbg啓動程序,操作起來很複雜。最好有一個自動的方法。Windows提供了讓指定程序隨調試器啓動的選項。設定註冊表後,當設定的進程啓動的時候,系統先啓動指定的調試器,然後把目標進程的地址和命令行作爲參數傳遞給調試器,調試器再啓動目標進程調試。這個選項在無法手動從調試器中啓動程序的時候特別有用,比如調試先於用戶登錄而啓動Windows Service程序,就必須使用這個方法:

How to debug Windows services

http://support.microsoft.com/?kbid=824344

有趣的是,好多惡意程序也通過這個方法來達到加載進程的目的。很多人把這個方法叫做IFEO 劫持(Image File Execution Option Hacking)。

在Windbg目錄下,有一個叫做adplus.vbs的腳本可以方便地調用Windbg來獲取dump文件。所以這裏可以借用這個腳本:

How to use ADPlus to troubleshoot "hangs" and "crashes"

http://support.microsoft.com/kb/286350/EN-US/

腳本的詳細說明可以參考adplus /?的幫助。

新的做法

結合上面的信息,具體做法是:

1.         在客戶機器的Image File Execution Options註冊表下面創建跟問題程序同名的鍵。

2.         在這個鍵的下面創建Debugger字符串類型子鍵。

3.         設定Debugger= C:/Debuggers/autodump.bat。

4.         編輯C:/Debuggers/autodump.bat文件的內容爲如下:

cscript.exe C:/Debuggers/adplus.vbs -crash -o C:/dumps -quiet -sc %1

通過上面的設置,當程序啓動的時候,系統自動運行cscript.exe來執行adplus.vbs腳本。Adplus.vbs腳本的-sc參數指定需要啓動的目標進程路徑(路徑作爲參數又系統傳入,bat文件中的%1代表這個參數),-crash參數表示監視進程退出,-o參數指定dump文件路徑,-quiet參數取消額外的提示。可以用notepad.exe作爲小白鼠做一個實驗,看看關閉notepad.exe的時候,是否有dump產生。

根據上面的設定,問題再次發生後,C:/dumps目錄生成了兩個dump文件。文件名分別是:

PID-0__Spawned0__1st_chance_Process_Shut_Down__full_178C_DateTime_0928.dmp

PID-0__Spawned0__2nd_chance_CPlusPlusEH__full_178C_2006-06-21_DateTime_0928.dmp

注意看第二個的名字,這個名字表示發生2nd chance的C++ exception!打開這個dump後找到了對應的call stack,發現的確是客戶忘記了catch潛在的C++異常。修改代碼添加對應的catch後,問題解決。

問題解決了,可是爲什麼華生醫生(Dr. Watson)抓不到dump呢

當然疑問並沒有隨着問題的解決而結束。既然是unhandled exception導致的crash,爲什麼Dr. Watson抓不到呢?首先創建兩個不同的程序來測試Dr. Watson的行爲:

int _tmain(int argc, _TCHAR* argv[])

{

  throw 1; 

  return 0;

}

int _tmain(int argc, _TCHAR* argv[])

{

  int *p=0;

  *p=0;

  return 0;

}

果然,對於第一個程序,Dr. Watson並沒有保存dump文件。對於第二個,Dr. Watson工作正常。看來的確跟異常類型相關。

仔細回憶一下。當AeDebug下的Auto設定爲0的時候,系統會彈出前面提到的紅色框框。對於上面這兩個程序,框框的內容是不一樣的。

在我這裏,看到的對話框分別是(對話框出現的時候用Ctrl+C保存的信息):

---------------------------

Microsoft Visual C++ Debug Library

---------------------------

Debug Error!

Program: d:/xiongli/today/exceptioninject/debug/exceptioninject.exe

This application has requested the Runtime to terminate it in an unusual way.

Please contact the application's support team for more information.

(Press Retry to debug the application)

---------------------------

Abort   Retry   Ignore  

---------------------------

---------------------------

exceptioninject.exe - Application Error

---------------------------

The instruction at "0x00411908" referenced memory at "0x00000000". The memory could not be "written".

Click on OK to terminate the program

Click on CANCEL to debug the program

---------------------------

OK   Cancel  

---------------------------

兩者行爲完全不一樣!如果做更多的測試,會發現對話框的細節還跟編譯模式release/debug 相關。

程序可以通過SetUnhandledExceptionFilter函數來修改unhanded exception的默認處理函數。這裏,C++運行庫在初始化CRT(C Runtime)的時候,傳入了CRT的處理函數 (msvcrt!CxxUnhandledExceptionFilter)。如果發生unhandled exception,該函數會判斷異常的號碼,如果是C++異常,就會彈出第一個對話框,否則就交給系統默認的處理函數(kernel32!UnhandledExceptionFilter)處理。第一種情況的call stack 如下:

USER32!MessageBoxA

MSVCR80D!__crtMessageBoxA

MSVCR80D!__crtMessageWindowA

MSVCR80D!_VCrtDbgReportA

MSVCR80D!_CrtDbgReportV

MSVCR80D!_CrtDbgReport

MSVCR80D!_NMSG_WRITE

MSVCR80D!abort

MSVCR80D!terminate

MSVCR80D!__CxxUnhandledExceptionFilter

kernel32!UnhandledExceptionFilter

MSVCR80D!_XcptFilter

第二種情況CRT交給系統處理。Callstack如下:

ntdll!KiFastSystemCallRet

ntdll!ZwRaiseHardError+0xc

kernel32!UnhandledExceptionFilter+0x4b4

release_crash!_XcptFilter+0x2e

release_crash!mainCRTStartup+0x1aa

release_crash!_except_handler3+0x61

ntdll!ExecuteHandler2+0x26

ntdll!ExecuteHandler+0x24

ntdll!KiUserExceptionDispatcher+0xe

release_crash!main+0x28

release_crash!mainCRTStartup+0x170

kernel32!BaseProcessStart+0x23

詳細的信息可以參考:

SetUnhandledExceptionFilter

http://msdn.microsoft.com/library/default.asp?url=/library/en-us/debug/base/setunhandledexceptionfilter.asp

UnhandledExceptionFilter

http://msdn.microsoft.com/library/default.asp?url=/library/en-us/debug/base/unhandledexceptionfilter.asp

上面觀察到的信息能解釋Dr. Watson的行爲嗎?看起來似乎有關係。爲了進一步確認這個問題,可以通過下面的測試,使用Windbg代替Dr. Watson,看看是否可以獲取dump。如果僅僅換一個調試器就可以獲取dump,那說明問題是跟調試器相關,跟程序拋出的異常無關。具體做法是:

1.         運行drwtsn32.exe –i註冊Dr. Watson。

2.         打開AeDebug註冊表,找到Debugger項,裏面應該是drwtsn32 -p %ld -e %ld -g。

3.         修改Debugger爲: C:/debuggers/windbg.exe -p %ld -e %ld -c ".dump /mfh C:/myfile.dmp ;q"。

當unhanded exception發生後,系統會啓動windbg.exe作爲調試器加載到目標進程。但是windbg.exe不會自動獲取dump,所以需要用-c參數來指定初始命令。命令之間可以用分開分割。這裏的.dump /mfh C:/myfile.dmp命令就是用來生成dump文件的。接下來的q命令是讓windbg.exe在dump生成完畢後自動退出。用這個方法,對於unhandled C++ exception,windbg.exe是可以獲取dump文件的。所以我認爲Dr. Watson這個工具在獲取dump的時候是有缺陷的。研究的發現在:

2.3.3  通知(Debug Event)是操作系統跟調試器交流的一種方法

通知,也叫做調試信息(Debug Events),是操作系統在某些事件發生的時候,通知調試器的一個手段。跟異常處理相似,操作系統在某些事件發生的時候,會檢查當前進程是否有調試器加載。如果有,就會給調試器發送對應的消息,以便使用調試器進行觀察。跟異常不一樣的地方就是,只有調試器纔會得到通知,應用程序本身是得不到的。同時調試器得到通知後不需要做什麼處理,沒有1st /2nd chance的差別。在Windbg幫助文件的Controlling Exceptions and Events主題裏面,可以看到關於通知的所有代號。常見的通知有:DLL的加載、卸載,線程的創建、退出等。

案例分析:VB6的版本問題

客戶用VB6開發的程序,在VB6 IDE調試的時候無法訪問Access 2003創建的數據庫,訪問Access 97的數據庫卻是好的。如果換一臺開發機,測試就一切正常。

這個問題的思路非常簡單,既然只有一臺機器有問題,說明是環境的原因。既然訪問Access 97沒問題,或許跟Access客戶端文件,也就是DAO的版本有關。通過工具Windbg目錄下的tlist工具檢查進程中加載的DLL,發現有問題的機器加載的是dao350.dll,沒有問題的機器加載的是dao360.dll。下一步就需要知道爲什麼加載的是dao350.dll?

DAO是一個COM對象,很有可能是通過COM對象加載的方法完成的。那麼,可以採取1.2節中ShellExecute同時打開兩個文件的處理方法,從創建COM的API: CoCreateInstanceEx開始,用wt命令跟蹤整個函數的執行,保存下來後比較兩種不同情況的異同。通過這個方法肯定是可以找出原因的,不過要想用wt命令一直跟蹤到LoadLibrary函數加載這個DLL,可能需要執行一整天。所以,應該找一個可操作性更強一點的方法來檢查。既然最後要追蹤到LoadLibrary爲止,那何不在這個函數上設置斷點,觀察檢查DAO350.DLL加載起來的情況?

在LoadLibrary上設定斷點並不是一個很好的方法。因爲:

1.         加載DLL不一定要調用LoadLibrary的。可以直接調用Native API,比如ntdll!LdrLoadDll。

2.         假設有幾十個DLL要加載,如果每次LoadLibrary都斷下來,操作起來也是很麻煩的事情。雖然可以通過條件斷點判斷LoadLibrary的參數來決定是否斷下來,但是設定條件斷點也是很麻煩的。

最好的方法,就是使用通知,在moudle load的時候,系統給調試器發送通知。由於Windbg在收到moudle load通知的時候,可以使用通配符來判斷 DLL的名字,操作起來就簡單多了。首先,在Windbg中用sxe ld:dao*.dll設置截獲Moudle Load的通知,當文件名是dao*.dll的時候,Windbg就會停下來。(關於Windbg的詳細信息,以及這裏使用到的命令,後面都有章節詳細介紹)。看到的結果就是:

0:008> sxe ld:dao*.dll

ModLoad: 1b740000 1b7c8000   C:/Program Files/Common Files/Microsoft Shared/DAO/DAO360.DLL

eax=00000001 ebx=00000000 ecx=0013e301 edx=00000000 esi=7ffdf000 edi=20000000

eip=7c82ed54 esp=0013e300 ebp=0013e344 iopl=0         nv up ei pl zr na po nc

cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246

ntdll!KiFastSystemCallRet:

7c82ed54 c3               ret

ntdll!KiFastSystemCallRet                            

ntdll!NtMapViewOfSection                             

ntdll!LdrpMapViewOfDllSection                        

ntdll!LdrpMapDll                                     

ntdll!LdrpLoadDll                                     

ntdll!LdrLoadDll                                     

0013e9c4 776ab4d0 0013ea40 00000000 00000008 kernel32!LoadLibraryExW

ole32!CClassCache::CDllPathEntry::LoadDll            

ole32!CClassCache::CDllPathEntry::Create_rl          

ole32!CClassCache::CClassEntry::CreateDllClassEntry_rl

ole32!CClassCache::GetClassObjectActivator           

ole32!CClassCache::GetClassObject                    

ole32!CServerContextActivator::GetClassObject        

ole32!ActivationPropertiesIn::DelegateGetClassObject 

ole32!CApartmentActivator::GetClassObject            

ole32!CProcessActivator::GCOCallback                 

ole32!CProcessActivator::AttemptActivation           

ole32!CProcessActivator::ActivateByContext           

ole32!CProcessActivator::GetClassObject              

ole32!ActivationPropertiesIn::DelegateGetClassObject 

ole32!CClientContextActivator::GetClassObject        

ole32!ActivationPropertiesIn::DelegateGetClassObject 

ole32!ICoGetClassObject                              

ole32!CComActivator::DoGetClassObject                

ole32!CoGetClassObject                               

VB6!VBCoGetClassObject                               

VB6!_DBErrCreateDao36DBEngine                        

通過檢查LoadLibraryExW的參數,可以看到:

0:000> du 0013ea40

0013ea40  "C:/Program Files/Common Files/Mi"

0013ea80  "crosoft Shared/DAO/DAO360.DLL"

從上面的信息可以看到:

1.         DAO360不是通過CoCreateInstanceEx加載進來的,而是另外一個COM API: CoGetClassObject。所以如果對CoCreateInstanceEx做想當然的跟蹤,就浪費時間了。

2.         COM調用的發起者是VB6!_DBErrCreateDao36DBEngine這個函數。應該仔細檢查這個函數。

有了前面DLL HELL 案例的教訓,在檢查這個函數前,首先檢查VB6.EXE的版本。發現正常情況下的版本是6.00.9782,有問題的機器上的版本是6.00.8176。 在有問題的機器上安裝Visual Studio 6,SP6升級VB6版本後,問題解決。

2.3.4  題外話和相關討論

錯過第一現場後還從dump中分析出線索嗎

前面介紹了用Windbg截取1st chance exception進行分析的方法。

但是好多情況下,程序並沒有運行在調試器下。崩潰發生後留在桌面上的是紅色的框框,這時候已經錯過了第一現場,但還是有機會找到對應exception的信息。

前面介紹過,紅色的框框是通過UnhandledExceptionFilter函數顯示出來的,而UnhandledExceptionFilter的參數就包含了異常信息。這個時候檢查UnhandledExceptionFilter的參數,就可以找到異常信息和異常上下文的地址,然後通過.exr和.cxr就可以在Windbg中把對應信息打印出來。

(注意:在Vista和Windows 2008中,系統改良了Error Reporting功能。程序崩潰後,系統會在Error Reporting的時候從內核直接掛起出錯的進程。這個時候如果用調試器檢查,會看到出錯進程就停在發生問題的指令上,不再需要在調試器中手動恢復exception context。

詳細信息可以參考:

Inside the Windows Vista Kernel: Part 3
http://www.microsoft.com/technet/technetmag/issues/2007/04/vistakernel/default.aspx?loc=en

拿案例2中的第2個例子做一個實驗。直接運行,崩潰後看到彈出的框框。這個時候不要點擊確定,而是啓動Windbg,attach到這個進程,然後用kb命令打印出call stack,找到UnhandledExceptionFilter的參數:

0:000> kb

ChildEBP RetAddr  Args to Child             

0012f74c 7c821b74 77e999ea d0000144 00000004 ntdll!KiFastSystemCallRet

0012f750 77e999ea d0000144 00000004 00000000 ntdll!ZwRaiseHardError+0xc

0012f9bc 004339be 0012fa08 7ffdd000 0044c4d8 kernel32!UnhandledExceptionFilter+0x4b4

第一個參數0012fa08保存的就是異常信息和異常上下文的地址:

0:000> dd 0x0012fa08

0012fa08  0012faf4 0012fb10 0012fa34 7c82eeb2

接下來用.exr加上異常信息地址打印出異常的信息:

0:000> .exr 0012faf4

ExceptionAddress: 0041a5a8 (release_crash!main+0x00000028)

   ExceptionCode: c0000005 (Access violation)

  ExceptionFlags: 00000000

NumberParameters: 2

   Parameter[0]: 00000001

   Parameter[1]: 00000000

Attempt to write to address 00000000

然後可以用.cxr加上異常上下文地址來切換上下文:

0:000> .cxr 0012fb10

eax=00000000 ebx=7ffde000 ecx=00000000 edx=00000001 esi=00000000 edi=0012fedc

eip=0041a5a8 esp=0012fddc ebp=0012fedc iopl=0         nv up ei pl nz na po nc

cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010206

release_crash!main+0x28:

0041a5a8 c60000           mov     byte ptr [eax],0x0      ds:0023:00000000=??

上下文切換完成後,可以用kb命令重新打印出該上下文上的call stack,就可以看到異常發生時候的狀態:

0:000> kb

  *** Stack trace for last set context - .thread/.cxr resets it

ChildEBP RetAddr  Args to Child             

0012fedc 00427c90 00000001 00361748 003617d0 release_crash!main+0x28 [c:/documents and settings/lixiong/desktop/amobrowser/release_crash.cpp @ 51]

0012ffc0 77e523cd 00000000 00000000 7ffde000 release_crash!mainCRTStartup+0x170

0012fff0 00000000 00418b18 00000000 78746341 kernel32!BaseProcessStart+0x23

這裏可以直接看到問題發生在release_crash.cpp文件的第51行。

Adplus,天天都用的工具

如果要捕獲崩潰時候的詳細信息,通常可以在調試器下運行程序,或者使用更方便的adplus來自動獲取異常產生時候的dump文件。可以參考:

How to use ADPlus to troubleshoot "hangs" and "crashes"

未處理異常發生後的主動退出

在某些特殊情況下,程序員爲了需要,會在發生未處理異常後主動退出,而不是等到崩潰被動發生。使用這種技術的有COM+,ASP.NET,還有淘寶旺旺客戶端。

這樣做的好處是:

1.         可以自定義接口。

2.         可以把發生異常時候的詳細信息保存下來以便後繼分析。

3.         可以防止調試器帶來的不必要干擾,保證發生崩潰的程序能立刻被系統回收,同時可以進行必要的挽救工作,比如重新啓動發生錯誤的進程繼續服務。

實現方法非常簡單。一種方法是在程序的main函數,或者關鍵函數中,使用SEH的__try和__except語句捕獲所有的異常。在__except語句中做相應的操作後(比如顯示UI,保存信息)直接退出程序。

另外一種方法是使用SetUnhandledExceptionFilter。有很多程序有崩潰後發送異常報告的功能。淘寶旺旺客戶端就是這樣的一個例子,可以參考:

http://eparg.spaces.msn.com/blog/cns!59BFC22C0E7E1A76!817.entry

根據我的分析,淘寶旺旺客戶端這裏用了SetUnhandledExceptionFilter這個函數來定義自己的異常處理函數,在異常處理函數中通過MiniDumpWriteDump API實現dump的捕獲。

使用這個技術的缺點就是調試器無法接收到2nd chance exception了,給調試增加了難度。比如要獲取COM+程序上crash的信息,頗費一番周折,還需要使用上面提到的.exr/.cxr命令:

How To Obtain a Userdump When COM+ Failfasts

http://support.microsoft.com/?id=287643

How to find the faulting stack in a process dump file that COM+ obtains

http://support.microsoft.com/?id=317317

如何調試UnhandledExceptionFilter

根據MSDN的描述,UnhandledExceptionFilter在沒有debugger attach的時候纔會被調用。所以,SetUnhandledExceptionFilter函數還有一個妙用,就是讓某些敏感代碼避開debugger的追蹤。比如你想把一些代碼保護起來,避免調試器的追蹤,可以採用的方法:

1.         在代碼執行前調用IsDebuggerPresent來檢查當前是否有調試器加載上來。如果有,就退出。

2.         把代碼放到SetUnhandledExceptionFilter設定的函數裏面。通過人爲觸發一個unhandled exception來執行。由於設定的UnhandledExceptionFilter函數只有在調試器沒有加載的時候纔會被系統調用,這裏巧妙地使用了系統的這個功能來保護代碼。

第一鍾方法很容易被繞過。看看IsDebuggerPresent的實現:

0:000> uf kernel32!IsDebuggerPresent

kernel32!IsDebuggerPresent:

  281 77e64860 64a118000000     mov     eax,fs:[00000018]

  282 77e64866 8b4030           mov     eax,[eax+0x30]

  282 77e64869 0fb64002         movzx   eax,byte ptr [eax+0x2]

  283 77e6486d c3               ret

IsDebuggerPresent是通過返回FS寄存器上記錄的地址的一些偏移量來實現的。([FS: [18]]:30保存的其實是當前進程的PEB地址)。在debugger中可以任意操作當前進程內存地址上的值,所以只需要用調試器把[[FS:[18]]:30]:2的值修改成0,IsDebuggerPresent就會返回false,導致方法1失效。

對於第二種方法,使用[[FS:[18]]:30]:2的欺騙方法就沒用了。因爲UnhandledExceptionFilter是否調用取決於系統內核的判斷。用戶態的調試器要想改變這個行爲,要破費一番腦筋了。

Kwan Hyun Kim提供了一種欺騙系統的方法:

How to debug UnhandleExceptionHandler

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章