一:背景
1.講故事
前段時間有位朋友找到我,說他的程序在客戶的機器上跑著跑著會出現偶發卡死,然后就崩掉了,但在本地怎么也沒復現,dump也抓到了,讓我幫忙看下到底怎么回事,其實崩潰類的dump也有簡單的,也有非常復雜的,因為大多情況下都是非托管層面出現的各種故障,非常考驗對 C, C++, Win32 API 以及 匯編 的理解,所以能不能解決看運氣吧, 不管怎么說,先上 WinDbg。
二:WinDbg分析
1. 查找崩潰點
WinDbg 非常牛的地方在于它擁有一個自動化崩潰分析命令 !analyze -v
,它的輸出信息非常有參考價值,所以嘗試一下看看。
0:136>?!analyze?-v
*******************************************************************************
*?????????????????????????????????????????????????????????????????????????????*
*????????????????????????Exception?Analysis???????????????????????????????????*
*?????????????????????????????????????????????????????????????????????????????*
*******************************************************************************
CONTEXT:??(.ecxr)
eax=00000000?ebx=00000000?ecx=00000000?edx=00000000?esi=00000003?edi=00000003
eip=777cf04c?esp=22dfd678?ebp=22dfd808?iopl=0?????????nv?up?ei?pl?nz?na?pe?nc
cs=0023??ss=002b??ds=002b??es=002b??fs=0053??gs=002b?????????????efl=00000206
ntdll!NtWaitForMultipleObjects+0xc:
777cf04c?c21400??????????ret?????14h
Resetting?default?scopeEXCEPTION_RECORD:??(.exr?-1)
ExceptionAddress:?0174ba6dExceptionCode:?00000000ExceptionFlags:?00000000
NumberParameters:?0PROCESS_NAME:??xxx.exeSTACK_TEXT:??
22dfd808?75b23b10?????00000003?22dfdc68?00000001?ntdll!NtWaitForMultipleObjects+0xc
22dfd808?75b23a08?????00000003?22dfdc68?00000000?KERNELBASE!WaitForMultipleObjectsEx+0xf0
22dfd824?672ff11a?????00000003?22dfdc68?00000000?KERNELBASE!WaitForMultipleObjects+0x18
22dfdca4?672ff3ac?????672dd150?672d0000?00000003?Faultrep!WerpReportFaultInternal+0x59e
22dfdcc4?672dd17d?????22dfdcec?708d0479?22dfdd60?Faultrep!WerpReportFault+0x9e
22dfdccc?708d0479?????22dfdd60?00000000?22dfdd60?Faultrep!ReportFault+0x2d
22dfdcec?708d07e9?????ec030e28?1c8f7728?00000003?clr!DoReportFault+0x43
22dfdd44?709f3c7e?????00000003?22dfe140?2e954594?clr!WatsonLastChance+0x19a
22dfe090?709f3d90?????ec0333f0?22dfe140?2e954594?clr!DoWatsonForUserBreak+0xc2
22dfe120?6fdc690f?????00000000?00000000?00000000?clr!DebugDebugger::Break+0xc9
22dfe148?0174ba6d?????00000000?00000000?00000000?mscorlib_ni!System.Diagnostics.Debugger.Break+0x57
WARNING:?Frame?IP?not?in?any?known?module.?Following?frames?may?be?wrong.
22dfe194?0174b58b?????00000000?00000000?00000000?0x174ba6d
22dfe1e8?0174b525?????00000000?00000000?00000000?mscorlib_ni!System.Runtime.CompilerServices.AsyncTaskMethodBuilder.Start<<xxxAsync>d__10>+0x43
22dfe1e8?0174b525?????00000000?00000000?00000000?mscorlib_ni!System.Runtime.CompilerServices.AsyncTaskMethodBuilder.Start<<xxxAsync>d__10>+0x43
22dfe22c?0174b3bd?????00000000?00000000?00000000?0x174b525
22dfe27c?0174b33b?????00000000?00000000?00000000?0x174b3bd
22dfe2d0?0174b2d5?????00000000?00000000?00000000?0x174b33b
...SYMBOL_NAME:??faultrep!WerpReportFaultInternal+59eMODULE_NAME:?FaultrepIMAGE_NAME:??Faultrep.dllSTACK_COMMAND:??~136s;?.ecxr?;?kb...
從卦中的調用棧看,有二個非常重要的信息。
Debugger.Break()
這個是 C# 對 int 3
?的封裝,即 斷點中斷異常
,目的就是將程序的所有線程中斷。
Faultrep!ReportFault()
這個是 WER 2.0
,全稱為 Windows Error Reporting Service
,用來抓崩潰dump的,前身是 Waston 醫生,在 Windows 服務列表中可以看到。

還有一點, Faultrep.dll
是 WER 的一個組件,會在抓取過程中自動加載,我們用 lm 觀察進程中的 dll 列表。
0:136>?lm
start????end????????module?name
00fe0000?01034000???xxx?C?(service?symbols:?CLR?Symbols?without?PDB)????????
0c100000?0c123000???WINMMBASE???(deferred)?????????????
662d0000?662ef000???clrcompression???(deferred)???????????????????
672d0000?67327000???Faultrep???(pdb?symbols)??????????c:\mysymbols\FaultRep.pdb\E16126C7FB9849A8B9AC57D8D62CABB01\FaultRep.pdb
...
匯總以上信息,大概就能推測出代碼中用了 Debugger.Break()
函數,因為無catch處理,Windows 啟動了 WER 2.0,程序代碼在 ntdll!NtWaitForMultipleObjects
處等待第三方組件處理完畢,因為各種原因出現了問題導致無法返回最后崩潰。
通過卦中的信息我們大概知道了前因后果,但代碼中為什么會出現 Debugger.Break()
呢?這就需要我們繼續深挖。
2. 為什么會有 Debugger.Break()
剛才的輸出中有這么一段話: STACK_COMMAND: ~136s; .ecxr ; kb
,它可以讓我們找到異常前的調用棧,為了能看到托管棧,這里將 kb
改成 !clrstack
。
0:136>??~136s;?.ecxr?;?!clrstackOS?Thread?Id:?0x13ec?(136)
Child?SP???????IP?Call?Site
22dfe0ac?777cf04c?[HelperMethodFrame:?22dfe0ac]?System.Diagnostics.Debugger.BreakInternal()
22dfe128?6fdc690f?System.Diagnostics.Debugger.Break()?[f:\dd\ndp\clr\src\BCL\system\diagnostics\debugger.cs?@?65]
22dfe150?0174ba6d?xxx.xxx+d__10.MoveNext()
22dfe19c?0174b58b?System.Runtime.CompilerServices.AsyncTaskMethodBuilder.Start[[xxx.xxx+d__10,?xxx.Abstractions]](d__10?ByRef)?[f:\dd\ndp\clr\src\BCL\system\runtime\compilerservices\AsyncMethodBuilder.cs?@?316]
22dfe1f0?0174b525?xxx.xxxAsync(System.String,?System.String)
22dfe238?0174b3bd?xxx.xxxProducer+d__7.MoveNext()
22dfe284?0174b33b?System.Runtime.CompilerServices.AsyncTaskMethodBuilder.Start[[xxx.xxx+d__7,?xxx.Abstractions]](d__7?ByRef)
22dfe2d8?0174b2d5?xxx.xxx.xxx(System.String,?System.String)
從卦中看,貌似是在一個異步方法中手工調用了 Deubgger.Break()
方法,接下來我們觀察下源碼,由于比較隱私,這里就簡化一下。
internal?async?Task?xxxAsync(string?x1,?string?x2)
{if?(string.IsNullOrEmpty(x1)){Debugger.Break();return;}if?(string.IsNullOrEmpty(x2)){Debugger.Break();return;}...
}
這代碼果然有意思,在防御性編程中居然用 Debugger.Break()
來處理,比較少見。
找到了問題源頭,解決方法就簡單了,大概有兩種做法。
去掉
Debugger.Break()
語句關閉 WER 2.0 服務

3. 對 Debugger.Break() 的題外話
在 clr 源碼中有對 Debugger.Break()
非常詳細的注釋。
//?This?does?a?user?break,?triggered?by?System.Diagnostics.Debugger.Break,?or?the?IL?opcode?for?break.
//
//?Notes:
//????If?a?managed?debugger?is?attached,?this?should?send?the?managed?UserBreak?event.
//????Else?if?a?native?debugger?is?attached,?this?should?send?a?native?break?event?(kernel32!DebugBreak)
//????Else,?this?should?invoke?Watson.
//
//?Historical?trivia:
//?-?In?whidbey,?this?would?still?invoke?Watson?if?a?native-only?debugger?is?attached.
//?-?In?arrowhead,?the?managed?debugging?pipeline?switched?to?be?built?on?the?native?pipeline.
FCIMPL0(void,?DebugDebugger::Break)
{...
}
FCIMPLEND
注釋文本:Else, this should invoke Watson
中的 Watson 其實就是本篇聊到的 WER
,觀察反匯編其實就是對 int 3
的封裝。
0:136>?uf?kernelBase!DebugBreak
KERNELBASE!DebugBreak:
75ba5e40?8bff????????????mov?????edi,edi
75ba5e42?cc??????????????int?????3
75ba5e43?c3??????????????ret
在很多反調試機制中,經常會用 int 3
來檢測當前程序是否被附加了調試器,參考如下 C++ 代碼。
#include?<iostream>int?isAttach?=?0;int?main()
{__try{__asm?{int?3}isAttach?=?1;}__except(1){isAttach?=?0;}if?(isAttach)?{printf("不好,發現有調試器?...");}else?{printf("哈哈,一切正常!");}getchar();
}
如果你用 WinDbg 附加上去, 就會被程序檢測到,截圖如下:

如果是正常運行,會是如下界面

可以在 C# 中通過 Pinvoke 引入,這種動態方式,反反調試會有不小的難度。
三:總結
這次事故是朋友在開發過程中為了方便調試,使用了 Debugger.Break()
方法,但在生產環境下并沒有刪除,導致在某些客戶機器上因為 WER 的開啟,被 Waston 捕獲導致的事故。
本次教訓是:發給客戶的版本,內含的調試信息該關閉的一定要關閉,以免生出此亂。