記一次 .NET某新能源檢測系統 崩潰分析

2023-10-16 18:02:09

一:背景

1. 講故事

前幾天有位朋友微信上找到我,說他的程式會偶發性崩潰,一直找不到原因,讓我幫忙看一下怎麼回事,對於這種崩潰類的程式,最好的辦法就是丟dump過來看一下便知,話不多說,上windbg說話。

二:WinDbg 分析

1. 到底是哪裡的崩潰

對於一個崩潰類的dump,尋找崩潰點非常重要,常用的命令就是 !analyze -v,輸出如下:


0:006> !analyze -v
CONTEXT:  6fbdee65 -- (.cxr 0x6fbdee65)
eax=55d2ebff ebx=5e5f04c0 ecx=e8c434e8 edx=cf8bc35b esi=83008b05 edi=75880000
eip=3d83f98b esp=ce8b0774 ebp=5756ec8b iopl=0 vip     ov up ei pl nz na po nc
cs=4040  ss=0010  ds=81f8  es=00e1  fs=4e8b  gs=ffdb             efl=08758b00
4040:3d83f98b ??              ???
Resetting default scope

EXCEPTION_RECORD:  049bfbd0 -- (.exr 0x49bfbd0)
ExceptionAddress: 00000000
   ExceptionCode: 049bfbf8
  ExceptionFlags: 6f9b6c38
NumberParameters: 8752248
   Parameter[0]: 00000000
   Parameter[1]: 6f9c92a0
   Parameter[2]: 049bfbdc
   Parameter[3]: 00000008
   Parameter[4]: 00000000
   Parameter[5]: 049bfc34
   Parameter[6]: 6f9b6d0d
   Parameter[7]: a2cc713a
   Parameter[8]: 6f9b6c40
   Parameter[9]: 00000000
   Parameter[10]: 00844c80
   Parameter[11]: a2cc712a
   Parameter[12]: 00000000
   Parameter[13]: 049bfd2c
   Parameter[14]: 049bfc00

PROCESS_NAME:  xxxx.exe

ERROR_CODE: (NTSTATUS) 0x80000004 - {    }

EXCEPTION_CODE_STR:  80000004

FAULTING_THREAD:  ffffffff

從卦中的崩潰點來看,很奇怪,怎麼 cs:eip 所處的地址沒有機器碼? 先不管了,看下異常狀態 80000004,在微軟的官方檔案查一查:

從圖中資訊看,應該是 F11 這種單步跟蹤造成的,這就很奇葩了,分析了200+ 的dump,這種崩潰還是第一次遇到,無語,一下子陷入了迷茫。

2. 還有突破口嗎

雖然 windbg 的自動化分析給出的資訊很不盡如意,但沒關係,根據強大的臨場經驗,我們直接切到異常前的狀態,看看異常前的上下文有沒有什麼新的線索,刪減後如下:


0:006> .ecxr
eax=00000000 ebx=049bec60 ecx=027e1374 edx=0b8024a8 esi=00000000 edi=049bebf8
eip=09fb48b1 esp=049beb98 ebp=049bebe0 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
xxx!xxx.Program.CurrentDomain_UnhandledException+0x29:
09fb48b1 cc              int     3
0:006> k
  *** Stack trace for last set context - .thread/.cxr resets it
 # ChildEBP RetAddr      
00 049bebe0 6f962546     xxx!xxx.Program.CurrentDomain_UnhandledException+0x29
...
11 049bf114 77a48962     clr!_except_handler4+0x29
12 049bf138 77a48934     ntdll!ExecuteHandler2+0x26
13 049bf200 77a34f86     ntdll!ExecuteHandler+0x24
14 049bf6f0 77a32b2c     ntdll!KiUserExceptionDispatcher+0x26
15 049bf6f0 76698d7a     ntdll!NtClose+0xc
16 049bf6f0 6f96287d     KERNELBASE!CloseHandle+0x4a
...
20 049bf970 78a1887b     clr!SafeHandle::Finalize+0x7a
21 049bf978 78a187e4     mscorlib_ni!System.Runtime.InteropServices.SafeHandle.Dispose+0x1b [f:\dd\ndp\clr\src\BCL\system\runtime\interopservices\safehandle.cs @ 263] 
22 049bf998 6f98df99     mscorlib_ni!System.Runtime.InteropServices.SafeHandle.Finalize+0x24 [f:\dd\ndp\clr\src\BCL\system\runtime\interopservices\safehandle.cs @ 199] 
23 049bf9ec 6f98e0a7     clr!FastCallFinalize+0x6d
24 049bfa10 6f98de5c     clr!MethodTable::CallFinalizer+0x150
25 049bfa78 6f98ded3     clr!CallFinalizer+0xa6
26 049bfa78 6f9c9263     clr!FinalizerThread::DoOneFinalization+0x132
27 049bfaa8 6f9c9343     clr!FinalizerThread::FinalizeAllObjects+0xa1
28 049bfad4 6f973b24     clr!FinalizerThread::FinalizerThreadWorker+0xbe
29 049bfaec 6f973b9b     clr!ManagedThreadBase_DispatchInner+0x71
2a 049bfb74 6f973c4b     clr!ManagedThreadBase_DispatchMiddle+0x8f
2b 049bfbd0 6f9b6c38     clr!ManagedThreadBase_DispatchOuter+0x6d
2c 049bfbf8 6f9b6d0d     clr!ManagedThreadBase::FinalizerBase+0x33
2d 049bfc34 6f98eb34     clr!FinalizerThread::FinalizerThreadStart+0xe2
2e 049bfcd0 76cdfcc9     clr!Thread::intermediateThreadProc+0x58
2f 049bfce0 77a27b1e     kernel32!BaseThreadInitThunk+0x19
30 049bfd3c 77a27aee     ntdll!__RtlUserThreadStart+0x2f
31 049bfd4c 00000000     ntdll!_RtlUserThreadStart+0x1b

從卦中的執行緒棧資訊來看,邏輯還是非常清楚的,終端子執行緒解構一個C#的 SafeWaitHandle 物件時,在閘道器函數 ntdll!NtClose 中丟擲了異常,這個函數再往下就是 核心層 了。

執行緒既然拋了異常,那 C# 層面有沒有接到呢?可以用 !t 觀察下。


0:006> !t
ThreadCount:      10
UnstartedThread:  0
BackgroundThread: 8
PendingThread:    0
DeadThread:       1
Hosted Runtime:   no
                                                                         Lock  
       ID OSID ThreadOBJ    State GC Mode     GC Alloc Context  Domain   Count Apt Exception
   0    1 4b9c 0085f088     a6028 Preemptive  0B7FF79C:00000000 00858c78 0     STA 
   6    2 5068 008a3708     ab228 Preemptive  0B8024B8:00000000 00858c78 0     MTA (Finalizer) System.Runtime.InteropServices.SEHException 0b800c88
  11    3 293c 0092c0a8   10a9228 Preemptive  00000000:00000000 00858c78 0     MTA (Threadpool Worker) 
  12    4 2eb0 0602ed48   8029228 Preemptive  00000000:00000000 00858c78 0     MTA (Threadpool Completion Port) 
XXXX    5    0 07de70a8   1039820 Preemptive  00000000:00000000 00858c78 0     Ukn (Threadpool Worker) 
  13    6 7e0c 0a7ada58   102a228 Preemptive  00000000:00000000 00858c78 0     MTA (Threadpool Worker) 
  14    7 7c60 0a773950   1029228 Preemptive  00000000:00000000 00858c78 0     MTA (Threadpool Worker) 
  15    8 5c24 0a775f68   10a9228 Preemptive  0B7EB8CC:00000000 00858c78 0     MTA (Threadpool Worker) 
  16    9 698c 008d5b40   1029228 Preemptive  00000000:00000000 00858c78 0     MTA (Threadpool Worker) 
  17   10 7de4 008dea10   1029228 Preemptive  0B7ECE80:00000000 00858c78 0     MTA (Threadpool Worker) 
0:006> !PrintException /d 0b800c88
Exception object: 0b800c88
Exception type:   System.Runtime.InteropServices.SEHException
Message:          外部元件發生異常。
InnerException:   <none>
StackTrace (generated):
    SP       IP       Function
    00000000 00000000 mscorlib_ni!Microsoft.Win32.Win32Native.CloseHandle(IntPtr)+0x1
    049BF760 78ADF5FE mscorlib_ni!Microsoft.Win32.SafeHandles.SafeWaitHandle.ReleaseHandle()+0xe
    00000000 00000001 mscorlib_ni!System.Runtime.InteropServices.SafeHandle.InternalFinalize()+0xffffffff90656c91
    049BF978 78A1887B mscorlib_ni!System.Runtime.InteropServices.SafeHandle.Dispose(Boolean)+0x1b
    049BF980 78A187E4 mscorlib_ni!System.Runtime.InteropServices.SafeHandle.Finalize()+0x24

StackTraceString: <none>
HResult: 80004005

從卦中資訊看,果然是在解構 SafeHandle.Finalize 時異常了,但這個異常資訊 Message:外部元件發生異常 對我們來說一點作用都沒有,到這裡貌似又進行不下去了。

3. 從 handle 上突破

託管層沒法挖了,那就繼續挖非託管層,也就是異常前的最後一個函數 ntdll!NtClose,這個函數其實沒什麼特別的,也就是釋放控制程式碼,這個函數一般來說固若金湯,不會有異常的,不管怎麼說,先把控制程式碼值找出來看看,簽名如下:


NTSTATUS NTAPI NtClose(
  HANDLE Handle
);

如何提取出 handle 呢?非常簡單,用 kb 即可。


0:006> kb 
  *** Stack trace for last set context - .thread/.cxr resets it
 # ChildEBP RetAddr      Args to Child    
...
13 049bf200 77a34f86     049bf218 049bf268 049bf218 ntdll!ExecuteHandler+0x24
14 049bf6f0 77a32b2c     00000664 049bf730 008a3708 ntdll!KiUserExceptionDispatcher+0x26
15 049bf6f0 76698d7a     00000664 049bf6fc 049bf72c ntdll!NtClose+0xc
16 049bf6f0 6f96287d     00000664 049bf730 008a3708 KERNELBASE!CloseHandle+0x4a
...

0:006> !handle 00000664 f
Handle 00000664
  Type         	<Error retrieving type>
unable to query object information
unable to query object information
  No object specific information available

我去,卦中顯示這個 handle=664 控制程式碼值居然不在程序中,難怪呼叫 ntdll!NtClose 會報錯,接下來的問題就是這個 handle 到底怎麼了?要找到這個答案,需要從執行緒棧上把 _EXCEPTION_RECORD 結構體給提取出來,它的內部記錄了 ExceptionCode ,而且剛好執行緒棧上的 ntdll!ExecuteHandler 方法的第一個引數就是這個結構體。


0:006> dt _EXCEPTION_RECORD 049bf218
VCRUNTIME140_CLR0400!_EXCEPTION_RECORD
   +0x000 ExceptionCode    : 0xc0000008
   +0x004 ExceptionFlags   : 0
   +0x008 ExceptionRecord  : (null) 
   +0x00c ExceptionAddress : 0x74a70daa Void
   +0x010 NumberParameters : 0
   +0x014 ExceptionInformation : [15] 0

接下來就是找下 ExceptionCode=0xc0000008 代表什麼意思,這個簡單,網上搜一下便知,截圖如下:

到這裡就很好理解了,然來是在釋放一個已經釋放的控制程式碼,這個肯定會報錯的,據朋友所說,他們的程式是 C# 和 C++ 混合程式設計的,那大概率就是 handle=664 被 C++ 給提前釋放了。

有些朋友肯定要問了,那我怎麼找到釋放這個 handle 的程式碼呢?要尋找這個答案,需要通過 perfview 對 handle 進行全程監控,參見:https://www.cnblogs.com/huangxincheng/p/17559370.html

三:總結

這個崩潰還是挺有意思的,需要你對 Windows 層面的知識有一定的瞭解,否則很難找出前因後果,所以請善待做大工控的朋友。

圖片名稱