記一次 .NET 某醫院門診軟體 卡死分析

来源:https://www.cnblogs.com/huangxincheng/archive/2023/05/15/17401330.html
-Advertisement-
Play Games

一:背景 1. 講故事 前幾天有位朋友找到我,說他們的軟體在客戶那邊卡死了,讓我幫忙看下是怎麼回事?我就讓朋友在程式卡死的時候通過 任務管理器 抓一個 dump 下來,雖然預設抓的是 wow64 ,不過用 soswow64.dll 轉還是可以的,參考命令如下: .load C:\soft\soswo ...


一:背景

1. 講故事

前幾天有位朋友找到我,說他們的軟體在客戶那邊卡死了,讓我幫忙看下是怎麼回事?我就讓朋友在程式卡死的時候通過 任務管理器 抓一個 dump 下來,雖然預設抓的是 wow64 ,不過用 soswow64.dll 轉還是可以的,參考命令如下:


    .load C:\soft\soswow64\soswow64.dll
    !wow64exts.sw

接下來就可以分析了哈。

二:WinDbg 分析

1. 為什麼會卡死

首先用 !t 簡單看一下主線程的 COM Apartment 模式,如果是 STA 那就是窗體程式,比如 WPF,WinForm 之類的,輸出如下:


0:000:x86> !t
ThreadCount:      39
UnstartedThread:  0
BackgroundThread: 12
PendingThread:    0
DeadThread:       26
Hosted Runtime:   no
                                                                         Lock  
       ID OSID ThreadOBJ    State GC Mode     GC Alloc Context  Domain   Count Apt Exception
   0    1 1928 01aee0b0   2026020 Preemptive  041D496C:00000000 01ae88a8 2     STA 
   ...

既然是窗體程式那就看主線程吧,使用 ~0s;!clrstack 命令。


0:000:x86> !clrstack
OS Thread Id: 0x1928 (0)
Child SP       IP Call Site
0177dff8 0167e1f8 [HelperMethodFrame_1OBJ: 0177dff8] System.Threading.SynchronizationContext.WaitHelper(IntPtr[], Boolean, Int32)
0177e29c 6a6fc693 System.Windows.Threading.DispatcherSynchronizationContext.Wait(IntPtr[], Boolean, Int32)
0177e2b0 71e36d54 System.Threading.SynchronizationContext.InvokeWaitMethodHelper(System.Threading.SynchronizationContext, IntPtr[], Boolean, Int32) [f:\dd\ndp\clr\src\BCL\system\threading\synchronizationcontext.cs @ 349]
0177e4d8 73220076 [GCFrame: 0177e4d8] 
0177e5f8 73220076 [GCFrame: 0177e5f8] 
0177e6d8 73220076 [GCFrame: 0177e6d8] 
0177e6f4 73220076 [HelperMethodFrame_1OBJ: 0177e6f4] System.Threading.Monitor.ReliableEnter(System.Object, Boolean ByRef)
0177e770 18078b93 System.Speech.Internal.Synthesis.AudioDeviceOut.Abort()
0177e79c 17270698 System.Speech.Internal.Synthesis.VoiceSynthesis.Abort()
0177e7e8 065ec76b System.Speech.Synthesis.SpeechSynthesizer.SpeakAsyncCancelAll()
0177e7f0 065ec728 xxx.xxx.Speek(System.String)
...

從卦中看是一個 語音模塊,還有 Speek 功能,挺有意思。。。 還 Speek 啥呢?可以用 !mdso 看一下。


0:000:x86> !mdso
Thread 0:
Location          Object            Type
------------------------------------------------------------
0177e060  04176eb8  System.IntPtr[]
...
0177e7f8  03be9504  System.String  "請先登錄驗證身份"

哈哈,上面只是花絮,繼續看線程棧會發現代碼卡在 Monitor.ReliableEnter 上,也就是等待 lock 鎖,接下來用 kb 把 鎖對象提取出來,即 clr!JITutil_MonReliableEnter 方法的第一個參數 03be11b4,輸出如下:


0:000:x86> kb
...
17 0177e768 18078b93     03be11b4 00000000 00000000 clr!JITutil_MonReliableEnter+0xb5
18 0177e794 17270698     0177e7bc 73252799 00000000 0x18078b93
19 0177e7e0 065ec76b     0177e808 065ec728 00000000 0x17270698
1a 0177e7e8 065ec728     00000000 03a0b318 03be9504 0x65ec76b
1b 0177e808 1727e09f     00000000 03be0920 04158b98 0x65ec728
1c 0177e824 69181324     04175c04 041199c0 00000001 0x1727e09f
...

0:000:x86> !do 03be11b4
Name:        System.Object
MethodTable: 71f200f4
EEClass:     71a715b0
Size:        12(0xc) bytes
File:        C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
Object
Fields:
None

有了這個對象就可以用 !syncblk 命令觀察同步塊表,到底是哪個線程在持有不釋放?


0:000:x86> !syncblk
Index         SyncBlock MonitorHeld Recursion Owning Thread Info          SyncBlock Owner
   96 11ac3ee0            3         1 11af0f28 35d4  13   03be11b4 System.Object
-----------------------------
Total           931
CCW             39
RCW             19
ComClassFactory 2
Free            802

0:000:x86> ~13s;!clrstack
ntdll_76fc0000!NtWaitForSingleObject+0xc:
7703159c c20c00          ret     0Ch
OS Thread Id: 0x35d4 (13)
Child SP       IP Call Site
17f8f23c 0000002b [InlinedCallFrame: 17f8f23c] 
17f8f238 1adf3269 DomainBoundILStubClass.IL_STUB_PInvoke(IntPtr)
17f8f23c 1adf2e82 [InlinedCallFrame: 17f8f23c] System.Speech.Internal.Synthesis.SafeNativeMethods.waveOutClose(IntPtr)
17f8f26c 1adf2e82 System.Speech.Internal.Synthesis.AudioDeviceOut.End()
17f8f298 187a5cd6 System.Speech.Internal.Synthesis.VoiceSynthesis.SpeakText(System.Speech.Internal.Synthesis.SpeakInfo, System.Speech.Synthesis.Prompt, System.Collections.Generic.List`1<System.Speech.Internal.Synthesis.LexiconEntry>)
17f8f304 17271669 System.Speech.Internal.Synthesis.VoiceSynthesis.ThreadProc()
17f8f3b8 71e3710d System.Threading.ThreadHelper.ThreadStart_Context(System.Object) [f:\dd\ndp\clr\src\BCL\system\threading\thread.cs @ 74]
17f8f3c4 71e640c5 System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean) [f:\dd\ndp\clr\src\BCL\system\threading\executioncontext.cs @ 954]
17f8f430 71e63fd6 System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean) [f:\dd\ndp\clr\src\BCL\system\threading\executioncontext.cs @ 902]
17f8f444 71e63f91 System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object) [f:\dd\ndp\clr\src\BCL\system\threading\executioncontext.cs @ 891]
17f8f45c 71e37068 System.Threading.ThreadHelper.ThreadStart() [f:\dd\ndp\clr\src\BCL\system\threading\thread.cs @ 111]
17f8f5a0 73220076 [GCFrame: 17f8f5a0] 
17f8f784 73220076 [DebuggerU2MCatchHandlerFrame: 17f8f784] 


0:013:x86> !t
  ...
  13   14 35d4 11af0f28     2b220 Preemptive  00000000:00000000 01ae88a8 2     MTA 
  ...

從卦中信息看:13號線程持有了 lock 鎖,並且它非線程池線程,而是通過 new Thread 出來的,從線程棧看都是sdk函數,綜合這些信息,應該是 VoiceSynthesis 創建出來的後臺線程,下麵的圖也可以佐證。

接下來繼續看,從線程棧頂上可以觀察到最後卡在了 System.Speech.Internal.Synthesis.SafeNativeMethods.waveOutClose 方法處,逆向之後的代碼如下:


    // System.Speech.Internal.Synthesis.AudioDeviceOut
    internal override void End()
    {
        if (!_deviceOpen)
        {
            throw new InvalidOperationException();
        }
        lock (_noWriteOutLock)
        {
            _deviceOpen = false;
            CheckForAbort();
            if (_queueIn.Count != 0)
            {
                SafeNativeMethods.waveOutReset(_hwo);
            }
            MMSYSERR mMSYSERR = SafeNativeMethods.waveOutClose(_hwo);
        }
    }

由於這是 Windows 的 VoiceSynthesis 模塊封裝底層函數,經過千錘百煉,理論上出問題的概率會非常小,除非上層有不合理的調用,這種概率會大一些。

2. 是上層不合理的調用嗎

這一塊我也沒玩過,網上搜一下 waveOutReset , waveOutClose ,看下有沒有同病相憐的人,結果網上一搜一堆,比如下麵這樣:


不管怎麼說,這一塊如果處理不好容易出現死鎖和卡死的情況,那是不是正如圖中所說 waveOutResetwaveOutClose 沒有匹配造成的呢?

這就取決於代碼中的 _queueIn 集合,可以觀察這兩個函數的彙編代碼提取出這個變數。


0:013:x86> !U /d 1adf2e82
Normal JIT generated code
System.Speech.Internal.Synthesis.AudioDeviceOut.End()
...
1adf2e30 8bf1            mov     esi,ecx
...
1adf2e69 8b4608          mov     eax,dword ptr [esi+8]
1adf2e6c 83780c00        cmp     dword ptr [eax+0Ch],0
1adf2e70 7408            je      1adf2e7a
...

0:013:x86> !U /d 187a5cd6
Normal JIT generated code
System.Speech.Internal.Synthesis.VoiceSynthesis.SpeakText(System.Speech.Internal.Synthesis.SpeakInfo, System.Speech.Synthesis.Prompt, System.Collections.Generic.List`1<System.Speech.Internal.Synthesis.LexiconEntry>)
...
187a5cc8 8b45d0          mov     eax,dword ptr [ebp-30h]
187a5ccb 8b486c          mov     ecx,dword ptr [eax+6Ch]
187a5cd3 ff5014          call    dword ptr [eax+14h]
>>> 187a5cd6 58              pop     eax
...


0:013:x86> kb 10
 # ChildEBP RetAddr      Args to Child              
...
08 17f8f264 1adf2e82     03be11b4 00000001 00000000 0x1adf3269
09 17f8f290 187a5cd6     187a5e16 03be0d24 043f3520 0x1adf2e82
0a 17f8f2f4 17271669     040efa14 040ef9a4 732515d8 0x187a5cd6
0b 17f8f3b0 71e3710d     03ff3e98 17f8f420 71e640c5 0x17271669
...

仔細觀察上面的彙編代碼:eax 來自於 esi,esi 來自於 ecx,ecx 最終來自於父函數中的 ebp-30h 的位置,串聯起來的命令就是 !do poi(poi(poi(17f8f2f4-30)+6c)+0x8) ,接下來我們 do 一下。


0:000:x86> !do poi(poi(poi(17f8f2f4-30)+6c)+0x8)
Name:        System.Collections.Generic.List`1[[System.Speech.Internal.Synthesis.AudioDeviceOut+InItem, System.Speech]]
MethodTable: 16cf20ec
EEClass:     71af6f8c
Size:        24(0x18) bytes
File:        C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
71f2f320  4001891        4     System.__Canon[]  0 instance 03c74538 _items
71f21bb4  4001892        c         System.Int32  1 instance        0 _size
71f21bb4  4001893       10         System.Int32  1 instance     1900 _version
71f200f4  4001894        8        System.Object  0 instance 00000000 _syncRoot
71f2f320  4001895        4     System.__Canon[]  0   static  <no information>

可以看到此時的 _size=0 ,有可能就是因為上層不合理調用導致這裡的 _queueIn 意外為 0 ,最終引發的卡死現象。

3. 真相大白

一時之間也找不到上層哪裡有不合理的調用,接下來的思路還是自己研讀主線程和13號線程的調用棧,最後發現一個可疑的現象,截圖如下:

通過仔細研讀底層代碼,Speek 會將消息丟到底層的queue隊列中,後臺線程會提取處理,這裡的 SpeakAsyncCancelAll 是完全沒必要的。

有了這個消息,就讓朋友把這個函數去掉觀察下試試,據朋友反饋說沒有問題了。

三:總結

這個案例中去掉了意外的 speech.SpeakAsyncCancelAll(); 語句就搞定了,內部深層邏輯也沒有再探究了,大概率就是意外的 _queueIn 為 0,讓 waveOutResetwaveOutClose 方法沒有匹配出現,造成了卡死現象。

圖片名稱
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本文介紹基於Python中whitebox模塊,對大量長時間序列柵格遙感影像的每一個像元進行忽略NoData值的多時序平均值求取~ ...
  • Lsof 備忘清單 lsof(list open files)是一個列出當前系統打開文件的工具。在linux環境下,任何事物都以文件的形式存在,通過文件不僅僅可以訪問常規數據,還可以訪問網路連接和硬體。在終端下輸入lsof即可顯示系統打開的文件,因為 lsof 需要訪問核心記憶體和各種文件,所以必須以 ...
  • Lua 備忘清單 Lua 是一個小巧的腳本語言。它是巴西里約熱內盧天主教大學(Pontifical Catholic University of Rio de Janeiro)里的一個由Roberto Ierusalimschy、Waldemar Celes 和 Luiz Henrique de F ...
  • PyCharm Professional 是一款針對 Python 編程的集成開發環境 (IDE),由 JetBrains 公司開發和維護。它是 PyCharm 社區版的升級版,提供了更多的功能和工具,旨在提高 Python 開發人員的生產力和效率。 以下是 PyCharm Professional ...
  • 本文屬於 OData 系列文章 引言 OData 是一個開放標準,已經在 oasis 組織標準化,因此我們可以在標準的官網查詢到 OData 的標準請求與返回形式:OData JSON Format Version 4.01 (oasis-open.org) 針對不同的數據類型,輸出返回的格式也不盡 ...
  • Blazor是一種使用.NET和C#構建客戶端Web應用程式的新興技術。它允許開發者在瀏覽器中直接運行.NET代碼,而無需依賴JavaScript。Blazor的技術優點主要表現在以下幾個方面: 單一語言棧:Blazor允許開發者使用C#和.NET進行全棧開發。一種語言用於前端和後端可以大大簡化開發 ...
  • 在VB.NET中,您可以使用Substring方法或Split方法來截取字元串。 Substring方法允許您從字元串中提取一個子字元串,該子字元串從指定的起始索引開始,並繼續到字元串的末尾或指定的長度。以下是使用Substring方法截取字元串的示例: Dim str As String = "H ...
  • JwtBearer簡介 首先要搞清楚什麼是JwtBearer,JwtBearer是ASP.NET Core的OAuth 2.0 JWT Bearer身份驗證提供程式。它提供了對JWT令牌進行驗證的功能,然後允許將令牌中包含的聲明(claims)用於用戶身份驗證和授權控制。 Json Web Toke ...
一周排行
    -Advertisement-
    Play Games
  • 概述:在C#中,++i和i++都是自增運算符,其中++i先增加值再返回,而i++先返回值再增加。應用場景根據需求選擇,首碼適合先增後用,尾碼適合先用後增。詳細示例提供清晰的代碼演示這兩者的操作時機和實際應用。 在C#中,++i 和 i++ 都是自增運算符,但它們在操作上有細微的差異,主要體現在操作的 ...
  • 上次發佈了:Taurus.MVC 性能壓力測試(ap 壓測 和 linux 下wrk 壓測):.NET Core 版本,今天計劃準備壓測一下 .NET 版本,來測試並記錄一下 Taurus.MVC 框架在 .NET 版本的性能,以便後續持續優化改進。 為了方便對比,本文章的電腦環境和測試思路,儘量和... ...
  • .NET WebAPI作為一種構建RESTful服務的強大工具,為開發者提供了便捷的方式來定義、處理HTTP請求並返迴響應。在設計API介面時,正確地接收和解析客戶端發送的數據至關重要。.NET WebAPI提供了一系列特性,如[FromRoute]、[FromQuery]和[FromBody],用 ...
  • 原因:我之所以想做這個項目,是因為在之前查找關於C#/WPF相關資料時,我發現講解圖像濾鏡的資源非常稀缺。此外,我註意到許多現有的開源庫主要基於CPU進行圖像渲染。這種方式在處理大量圖像時,會導致CPU的渲染負擔過重。因此,我將在下文中介紹如何通過GPU渲染來有效實現圖像的各種濾鏡效果。 生成的效果 ...
  • 引言 上一章我們介紹了在xUnit單元測試中用xUnit.DependencyInject來使用依賴註入,上一章我們的Sample.Repository倉儲層有一個批量註入的介面沒有做單元測試,今天用這個示例來演示一下如何用Bogus創建模擬數據 ,和 EFCore 的種子數據生成 Bogus 的優 ...
  • 一、前言 在自己的項目中,涉及到實時心率曲線的繪製,項目上的曲線繪製,一般很難找到能直接用的第三方庫,而且有些還是定製化的功能,所以還是自己繪製比較方便。很多人一聽到自己畫就害怕,感覺很難,今天就分享一個完整的實時心率數據繪製心率曲線圖的例子;之前的博客也分享給DrawingVisual繪製曲線的方 ...
  • 如果你在自定義的 Main 方法中直接使用 App 類並啟動應用程式,但發現 App.xaml 中定義的資源沒有被正確載入,那麼問題可能在於如何正確配置 App.xaml 與你的 App 類的交互。 確保 App.xaml 文件中的 x:Class 屬性正確指向你的 App 類。這樣,當你創建 Ap ...
  • 一:背景 1. 講故事 上個月有個朋友在微信上找到我,說他們的軟體在客戶那邊隔幾天就要崩潰一次,一直都沒有找到原因,讓我幫忙看下怎麼回事,確實工控類的軟體環境複雜難搞,朋友手上有一個崩潰的dump,剛好丟給我來分析一下。 二:WinDbg分析 1. 程式為什麼會崩潰 windbg 有一個厲害之處在於 ...
  • 前言 .NET生態中有許多依賴註入容器。在大多數情況下,微軟提供的內置容器在易用性和性能方面都非常優秀。外加ASP.NET Core預設使用內置容器,使用很方便。 但是筆者在使用中一直有一個頭疼的問題:服務工廠無法提供請求的服務類型相關的信息。這在一般情況下並沒有影響,但是內置容器支持註冊開放泛型服 ...
  • 一、前言 在項目開發過程中,DataGrid是經常使用到的一個數據展示控制項,而通常表格的最後一列是作為操作列存在,比如會有編輯、刪除等功能按鈕。但WPF的原始DataGrid中,預設只支持固定左側列,這跟大家習慣性操作列放最後不符,今天就來介紹一種簡單的方式實現固定右側列。(這裡的實現方式參考的大佬 ...