記一次 .NET 某工控自動化控制系統 卡死分析

来源:https://www.cnblogs.com/huangxincheng/archive/2022/08/02/16544462.html
-Advertisement-
Play Games

一:背景 1. 講故事 前段時間遇到了好幾起關於窗體程式的 進程載入鎖 引發的 程式卡死 和 線程暴漲 問題,這種 dump 分析難度較大,主要涉及到 Windows操作系統 和 C++ 的基礎知識,所以有必要簡單整理和大家分享一下,上 windbg 說話。 二:WinDbg 分析 1. 主線程此時 ...


一:背景

1. 講故事

前段時間遇到了好幾起關於窗體程式的 進程載入鎖 引發的 程式卡死線程暴漲 問題,這種 dump 分析難度較大,主要涉及到 Windows操作系統 和 C++ 的基礎知識,所以有必要簡單整理和大家分享一下,上 windbg 說話。

二:WinDbg 分析

1. 主線程此時在做什麼

窗體程式的卡死,入口分析點在 主線程 上,使用 ~0s; k 命令即可。


0:000> ~0s; k
ntdll!NtWaitForSingleObject+0x14:
00007ffc`6010e614 c3              ret
 # Child-SP          RetAddr               Call Site
00 0000008c`107fe5d8 00007ffc`5cda4313     ntdll!NtWaitForSingleObject+0x14
01 0000008c`107fe5e0 00007ffc`257b2fe8     KERNELBASE!WaitForSingleObjectEx+0x93
02 0000008c`107fe680 00007ffc`257b2f9e     clr!CLREventWaitHelper2+0x3c
03 0000008c`107fe6c0 00007ffc`257b2efc     clr!CLREventWaitHelper+0x1f
04 0000008c`107fe720 00007ffc`256beed2     clr!CLREventBase::WaitEx+0x71
05 0000008c`107fe7b0 00007ffc`25687e44     clr!WKS::GCHeap::WaitUntilGCComplete+0x2e
06 0000008c`107fe7e0 00007ffc`25688092     clr!Thread::RareDisablePreemptiveGC+0x18f
07 0000008c`107fe880 00007ffc`255d44f4     clr!JIT_RareDisableHelperWorker+0x42
08 0000008c`107fe9d0 00007ffc`22544314     clr!JIT_RareDisableHelper+0x14
09 0000008c`107fea10 00007ffc`22525f32     WindowsBase_ni+0x184314
0a 0000008c`107fead0 00007ffc`22520298     WindowsBase_ni+0x165f32
0b 0000008c`107feb10 00007ffc`2251edaf     WindowsBase_ni+0x160298
0c 0000008c`107feba0 00007ffc`202b6421     WindowsBase_ni+0x15edaf
...

從卦象中的 WaitUntilGCComplete 函數看,此時的主線程正在等待 GC完成,那到底誰觸發了 GC 呢? 接下來用 !t 命令查看下 GC 標記。


0:000> !t
ThreadCount:      58
UnstartedThread:  9
BackgroundThread: 39
PendingThread:    9
DeadThread:       5
Hosted Runtime:   no
  42   41  cd8 000001ec5f7f7c90  202b220 Preemptive  0000000000000000:0000000000000000 000001ec3353c710 0     MTA 
  43   34 1160 000001ec5f7f4db0    21220 Preemptive  0000000000000000:0000000000000000 000001ec3353c710 0     Ukn 
  44   33 218c 000001ec5f7f5580    2b220 Cooperative 0000000000000000:0000000000000000 000001ec3353c710 1     MTA (GC) 
  45   36 1110 000001ec5f7f8460  202b220 Preemptive  0000000000000000:0000000000000000 000001ec3353c710 0     MTA 
  48   32 26a8 000001ec545813e0    2b220 Preemptive  0000000000000000:0000000000000000 000001ec3353c710 0     MTA 
  49   31  4b4 000001ec54581bb0    2b220 Preemptive  0000000000000000:0000000000000000 000001ec3353c710 0     MTA 

從卦中看,當前的 44 號線程觸發了 GC,接下來看下它的線程棧情況。


0:000> ~~[218c]s
ntdll!NtWaitForSingleObject+0x14:
00007ffc`6010e614 c3              ret
0:044> k
 # Child-SP          RetAddr               Call Site
00 0000008c`0a0bd9b8 00007ffc`5cda4313     ntdll!NtWaitForSingleObject+0x14
01 0000008c`0a0bd9c0 00007ffc`257b2fe8     KERNELBASE!WaitForSingleObjectEx+0x93
02 0000008c`0a0bda60 00007ffc`257b2f9e     clr!CLREventWaitHelper2+0x3c
03 0000008c`0a0bdaa0 00007ffc`257b2efc     clr!CLREventWaitHelper+0x1f
04 0000008c`0a0bdb00 00007ffc`256c821d     clr!CLREventBase::WaitEx+0x71
05 0000008c`0a0bdb90 00007ffc`256c8120     clr!standalone::`anonymous namespace'::CreateSuspendableThread+0x10c
06 0000008c`0a0bdc50 00007ffc`257b9e4c     clr!GCToEEInterface::CreateThread+0x170
07 0000008c`0a0bde40 00007ffc`257b8543     clr!WKS::gc_heap::prepare_bgc_thread+0x4c
08 0000008c`0a0bde70 00007ffc`256be9f7     clr!WKS::gc_heap::garbage_collect+0xfbb37
09 0000008c`0a0bdeb0 00007ffc`256c0c47     clr!WKS::GCHeap::GarbageCollectGeneration+0xef
0a 0000008c`0a0bdf00 00007ffc`255dc7b3     clr!WKS::GCHeap::Alloc+0x29c
0b 0000008c`0a0bdf50 00007ffb`c631853d     clr!JIT_New+0x339

從線程棧看,GC 在觸發的過程中準備使用 CreateThread 函數創建線程,可能有些朋友不太理解,GC觸發還有創建線程的操作??? 哈哈,這就涉及到一點 CLR 的基礎知識,workstation 的 bgc 模式會有一個專門的 後臺線程, 而這個後臺線程是在運行時的某個時刻創建和銷毀的,所以從線程棧看,GC 正在等待 bgc 線程初始化完畢。

很奇怪的是,我從多個卡死狀態下的 dump 看,發現 GC 都卡在這個 CreateThread 函數上,言外之意線程在這裡無限期等待了。

2. CreateThread 為什麼不能初始化完成?

如果大家玩過 C++ 的話,應該知道 C++ 的 dll 會有一個 DllMain 方法,它的意義和 Main 方法一致,代碼骨架圖如下:


// dllmain.cpp : Defines the entry point for the DLL application.
#include "pch.h"

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

從 switch 中的枚舉參數來看,就是 dll 載入和卸載,線程創建和銷毀,有此 DllMain 方法的 dll 都會收到通知,在進入到這個 DllMain 之前會首先獲取到一個全局的 進程載入鎖(LdrpLoaderLock)

既然 GC 過程中不能創建 CreateThread,那必然有人在持有這個 LdrpLoaderLock 鎖,接下來的問題就是如何找到 哪個線程正在持有 LdrpLoaderLock ? 這就涉及到 windows 操作系統的 基礎知識了。

3. 誰在持有 LdrpLoaderLock 鎖?

LdrpLoaderLock 變數是在 ntdll.dll 用戶態網關dll中,可以用 x ntdll!LdrpLoaderLock 命令檢索,然後看下是作為哪個臨界區持有的。


0:044>  x ntdll!LdrpLoaderLock
00007ffc`601cf4f8 ntdll!LdrpLoaderLock = <no type information>

0:044> dt _RTL_CRITICAL_SECTION  00007ffc`601cf4f8
atl100!_RTL_CRITICAL_SECTION
   +0x000 DebugInfo        : 0x00007ffc`601cf978 _RTL_CRITICAL_SECTION_DEBUG
   +0x008 LockCount        : 0n-2
   +0x00c RecursionCount   : 0n1
   +0x010 OwningThread     : 0x00000000`0000138c Void
   +0x018 LockSemaphore    : (null) 
   +0x020 SpinCount        : 0x4000000

從卦中看,當前 138c 號線程持有了這個臨界區,接下來切到這個線程看下它的線程棧即可。


0:044> ~~[138c]s
win32u!NtUserMessageCall+0x14:
00007ffc`5c891184 c3              ret
0:061> k
 # Child-SP          RetAddr               Call Site
00 0000008c`00ffec68 00007ffc`5f21bfbe     win32u!NtUserMessageCall+0x14
01 0000008c`00ffec70 00007ffc`5f21be38     user32!SendMessageWorker+0x11e
02 0000008c`00ffed10 00007ffc`124fd4af     user32!SendMessageW+0xf8
03 0000008c`00ffed70 00007ffc`125e943b     cogxImagingDevice!DllUnregisterServer+0x3029f
04 0000008c`00ffeda0 00007ffc`125e9685     cogxImagingDevice!DllUnregisterServer+0x11c22b
05 0000008c`00ffede0 00007ffc`600b50e7     cogxImagingDevice!DllUnregisterServer+0x11c475
06 0000008c`00ffee20 00007ffc`60093ccd     ntdll!LdrpCallInitRoutine+0x6f
07 0000008c`00ffee90 00007ffc`60092eef     ntdll!LdrpProcessDetachNode+0xf5
08 0000008c`00ffef60 00007ffc`600ae319     ntdll!LdrpUnloadNode+0x3f
09 0000008c`00ffefb0 00007ffc`600ae293     ntdll!LdrpDecrementModuleLoadCountEx+0x71
0a 0000008c`00ffefe0 00007ffc`5cd7c00e     ntdll!LdrUnloadDll+0x93
0b 0000008c`00fff010 00007ffc`5d47cf78     KERNELBASE!FreeLibrary+0x1e
0c 0000008c`00fff040 00007ffc`5d447aa3     combase!CClassCache::CDllPathEntry::CFinishObject::Finish+0x28 [onecore\com\combase\objact\dllcache.cxx @ 3420] 
0d 0000008c`00fff070 00007ffc`5d4471a9     combase!CClassCache::CFinishComposite::Finish+0x4b [onecore\com\combase\objact\dllcache.cxx @ 3530] 
0e 0000008c`00fff0a0 00007ffc`5d3f1499     combase!CClassCache::FreeUnused+0xdd [onecore\com\combase\objact\dllcache.cxx @ 6547] 
0f 0000008c`00fff650 00007ffc`5d3f13c7     combase!CoFreeUnusedLibrariesEx+0x89 [onecore\com\combase\objact\dllapi.cxx @ 117] 
10 (Inline Function) --------`--------     combase!CoFreeUnusedLibraries+0xa [onecore\com\combase\objact\dllapi.cxx @ 74] 
11 0000008c`00fff690 00007ffc`6008a019     combase!CDllHost::MTADllUnloadCallback+0x17 [onecore\com\combase\objact\dllhost.cxx @ 929] 
12 0000008c`00fff6c0 00007ffc`6008bec4     ntdll!TppTimerpExecuteCallback+0xa9
13 0000008c`00fff710 00007ffc`5f167e94     ntdll!TppWorkerThread+0x644
14 0000008c`00fffa00 00007ffc`600d7ad1     kernel32!BaseThreadInitThunk+0x14
15 0000008c`00fffa30 00000000`00000000     ntdll!RtlUserThreadStart+0x21

可以看到,cogxImagingDevice 發起了一個 user32!SendMessageW 同步方法,導致程式徹底死鎖,可能有些朋友有點懵,我簡單羅列下。

  1. 44 號線程使用 CreateThread 創建線程,但必須要先獲取 載入鎖,所以一直在等待 61 號線程釋放載入鎖。
  2. 61 號線程用同步的方式 user32!SendMessageW 給 主線程的 WndProc 網關函數打同步消息,等待 主線程給予響應,而此時主線程正在等待 GC 完成。
  3. 主線程 在無限期的 等待 GC 結束。

綜合來看,只要主線程不響應 44 號線程, 44號線程就不會釋放 載入鎖,這個 載入鎖 不釋放,就會導致很多的線程都無法初始化完畢,這個在它的 dump 中也反應出來了,代碼如下:


  70  Id: 300.4f0 Suspend: 0 Teb: 0000008c`102e1000 Unfrozen
 # Child-SP          RetAddr               Call Site
00 0000008c`0ecff388 00007ffc`6008902d     ntdll!NtWaitForSingleObject+0x14
01 0000008c`0ecff390 00007ffc`600b29a7     ntdll!LdrpDrainWorkQueue+0x15d
02 0000008c`0ecff3d0 00007ffc`600e76d5     ntdll!LdrpInitializeThread+0x8b
03 0000008c`0ecff4b0 00007ffc`600e7633     ntdll!_LdrpInitialize+0x89
04 0000008c`0ecff550 00007ffc`600e75de     ntdll!LdrpInitialize+0x3b
05 0000008c`0ecff580 00000000`00000000     ntdll!LdrInitializeThunk+0xe

  71  Id: 300.1c88 Suspend: 0 Teb: 0000008c`102e5000 Unfrozen
 # Child-SP          RetAddr               Call Site
00 0000008c`0f4ff268 00007ffc`6008902d     ntdll!NtWaitForSingleObject+0x14
01 0000008c`0f4ff270 00007ffc`600b29a7     ntdll!LdrpDrainWorkQueue+0x15d
02 0000008c`0f4ff2b0 00007ffc`600e76d5     ntdll!LdrpInitializeThread+0x8b
03 0000008c`0f4ff390 00007ffc`600e7633     ntdll!_LdrpInitialize+0x89
04 0000008c`0f4ff430 00007ffc`600e75de     ntdll!LdrpInitialize+0x3b
05 0000008c`0f4ff460 00000000`00000000     ntdll!LdrInitializeThunk+0xe

  72  Id: 300.15c0 Suspend: 0 Teb: 0000008c`102e7000 Unfrozen
 # Child-SP          RetAddr               Call Site
00 0000008c`0f8ff278 00007ffc`6008902d     ntdll!NtWaitForSingleObject+0x14
01 0000008c`0f8ff280 00007ffc`600b29a7     ntdll!LdrpDrainWorkQueue+0x15d
02 0000008c`0f8ff2c0 00007ffc`600e76d5     ntdll!LdrpInitializeThread+0x8b
03 0000008c`0f8ff3a0 00007ffc`600e7633     ntdll!_LdrpInitialize+0x89
04 0000008c`0f8ff440 00007ffc`600e75de     ntdll!LdrpInitialize+0x3b
05 0000008c`0f8ff470 00000000`00000000     ntdll!LdrInitializeThunk+0xe

  73  Id: 300.764 Suspend: 0 Teb: 0000008c`102ef000 Unfrozen
 # Child-SP          RetAddr               Call Site
00 0000008c`0fcff388 00007ffc`6008902d     ntdll!NtWaitForSingleObject+0x14
01 0000008c`0fcff390 00007ffc`600b29a7     ntdll!LdrpDrainWorkQueue+0x15d
02 0000008c`0fcff3d0 00007ffc`600e76d5     ntdll!LdrpInitializeThread+0x8b
03 0000008c`0fcff4b0 00007ffc`600e7633     ntdll!_LdrpInitialize+0x89
04 0000008c`0fcff550 00007ffc`600e75de     ntdll!LdrpInitialize+0x3b
05 0000008c`0fcff580 00000000`00000000     ntdll!LdrInitializeThunk+0xe

可以看到,有很多的線程都卡死在 ntdll!LdrpInitializeThread+0x8b 處無法進行下去,那這個方法到底在做什麼呢?可以看下 反彙編代碼


0:000> u ntdll!LdrpInitializeThread+0x8b
ntdll!LdrpInitializeThread+0x8b:
00007ffc`600b29a7 e874a50000      call    ntdll!LdrpAcquireLoaderLock (00007ffc`600bcf20)
00007ffc`600b29ac 90              nop
00007ffc`600b29ad 488b1d1c2a1200  mov     rbx,qword ptr [ntdll!PebLdr+0x10 (00007ffc`601d53d0)]
00007ffc`600b29b4 488d05152a1200  lea     rax,[ntdll!PebLdr+0x10 (00007ffc`601d53d0)]
00007ffc`600b29bb 483bd8          cmp     rbx,rax
00007ffc`600b29be 0f84c5000000    je      ntdll!LdrpInitializeThread+0x16d (00007ffc`600b2a89)
....

從彙編中可以清晰的看到,都卡在獲取載入鎖 ntdll!LdrpAcquireLoaderLock 函數上。

三:總結

這是一個由 cogxImagingDevice.dll引發的程式死鎖,查了下百度是一個商業版的 視覺圖像庫,對此我也無法解決,只能建議朋友。

  1. 熟悉下這個 dll 的配置,如果不是配置造成建議提交官方解決。

  2. 爭取做到 C# 和 C++ 的進程級隔離,或者乾脆替換掉 cogxImagingDevice.dll ,雖然這個難度有點大。

這個 dump 給我們的教訓是: 當 C# 和 C++ 混在一起,爭取做到最大可能的隔離,一旦出現問題非常考驗你對 windows 底層知識的理解,分析排錯門檻很高

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • numpy.linalg 模塊包含線性代數的函數。使用這個模塊,可以計算逆矩陣、求特征值、解線性方程組以及求解行列式等。一、計算逆矩陣 線性代數中,矩陣A與其逆矩陣A ^(-1)相乘後會得到一個單位矩陣I。該定義可以寫為A *A ^(-1) =1。numpy.linalg 模塊中的 inv 函數可以 ...
  • 多商戶商城系統,也稱為B2B2C(BBC)平臺電商模式多商家商城系統。可以快速幫助企業搭建類似拼多多/京東/天貓/淘寶的綜合商城。 多商戶商城系統支持商家入駐加盟,同時滿足平臺自營、旗艦店等多種經營方式。平臺可以通過收取商家入駐費,訂單交易服務費,提現手續費,簡訊通道費等多手段方式,實現整體盈利。 ...
  • 面試最常問的問題 1、equals比較的什麼? 2、有沒有重寫過equals? 3、有沒有重寫過hashCode? 4、什麼情況下需要重寫equals()和hashCode()? 1) equals源碼 **目標:**如果不做任何處理(可能絕大大大多數場景的對象都是這樣的),jvm對同一個對象的判斷 ...
  • 前言 😋 嗨嘍,大家好呀~這裡是愛看美女的茜茜吶 技術賦能,用科技提升每個人獨特的幸福感。 在快上,用戶可以用照片和短視頻記錄自己的生活點滴,也可以通過直播與粉絲實時互動。 快的內容覆蓋生活的方方面面,用戶遍佈全國各地。 在這裡,人們能找到自己喜歡的內容,找到自己感興趣的人,看到更真實有趣的世界, ...
  • 兄弟們,今天我們來試試用Python輸出指定時間間隔內的日期~ 涉及知識點 文件讀寫 基礎語法 字元串處理 迴圈遍歷 代碼展示 使用的模塊 import platform import datetime # 我還給大家準備了這些資料:2022最新Python視頻教程、Python電子書10個G(涵蓋 ...
  • Apache Maven,是一個項目管理及自動構建的工具,有Apache軟體基金會所提供。 Maven是用Java語言編寫的,是一款可以跨平臺的軟體。 Maven解決了軟體構建的兩方面問題:一是軟體是如何構建的,二是軟體的依賴關係。 Maven是以基於插件的架構構建的,這使其能夠使用任何能用標準輸入... ...
  • 隨著線上購物成為了人們的主要消費之一,搭建商城系統也成為一大熱門的發展方向,在現在的電商市場中,經營的主體規模非常龐大,各種各樣的電商系統琳琅滿目,但是只要仔細觀察就會發現,有很大一部分的商城系統風格很相似,同質化現象非常嚴重,容易讓用戶產生審美疲勞,要是選擇進行頁面二次開發,需要花費的精力和成本又 ...
  • 很多時候,我們在本地開發過程中程式運行很正常,但是發佈到線上之後由於環境的原因,可能會有一些異常。通常我們會通過日誌來分析問題,除了日誌還有一種常用的調試手段就是:附加進程。 VS中的附加進程非常強大,目前提供了9種常用的附加方式。 在當前.Net Core支持跨平臺的大背景下,其中Linux環境和 ...
一周排行
    -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中,預設只支持固定左側列,這跟大家習慣性操作列放最後不符,今天就來介紹一種簡單的方式實現固定右側列。(這裡的實現方式參考的大佬 ...