記一次 .NET 某自動化採集軟體 崩潰分析

2022-11-21 09:00:42

一:背景

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 scope

EXCEPTION_RECORD:  (.exr -1)
ExceptionAddress: 0174ba6d
   ExceptionCode: 00000000
  ExceptionFlags: 00000000
NumberParameters: 0

PROCESS_NAME:  xxx.exe

STACK_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+59e

MODULE_NAME: Faultrep

IMAGE_NAME:  Faultrep.dll

STACK_COMMAND:  ~136s; .ecxr ; kb

...

從卦中的呼叫棧看,有二個非常重要的資訊。

  1. Debugger.Break()

這個是 C# 對 int 3 的封裝,即 斷點中斷異常,目的就是將程式的所有執行緒中斷。

  1. 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 ; !clrstack

OS 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() 來處理,比較少見。

找到了問題源頭,解決方法就簡單了,大概有兩種做法。

  1. 去掉 Debugger.Break() 語句

  2. 關閉 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 捕獲導致的事故。

本次教訓是:發給客戶的版本,內含的偵錯資訊該關閉的一定要關閉,以免生出此亂。

圖片名稱