一:背景

1. 讲故事

前段时间遇到了好几起关于窗体程序的 进程加载锁 引发的 程序卡死线程暴涨 问题,这种 dump 分析难度较大,主要涉及到 Windows操作系统 和 C++ 的基础知识,所以有必要简单整理和大家分享一下,上 windbg 说话。

二:WinDbg 分析

1. 主线程此时在做什么

窗体程序的卡死,入口分析点在 主线程 上,使用 ~0s; k 命令即可。


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

从卦象中的 WaitUntilGCComplete 函数看,此时的主线程正在等待 GC完成,那到底谁触发了 GC 呢? 接下来用 !t 命令查看下 GC 标记。


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

从卦中看,当前的 44 号线程触发了 GC,接下来看下它的线程栈情况。


  1. 0:000> ~~[218c]s
  2. ntdll!NtWaitForSingleObject+0x14:
  3. 00007ffc`6010e614 c3 ret
  4. 0:044> k
  5. # Child-SP RetAddr Call Site
  6. 00 0000008c`0a0bd9b8 00007ffc`5cda4313 ntdll!NtWaitForSingleObject+0x14
  7. 01 0000008c`0a0bd9c0 00007ffc`257b2fe8 KERNELBASE!WaitForSingleObjectEx+0x93
  8. 02 0000008c`0a0bda60 00007ffc`257b2f9e clr!CLREventWaitHelper2+0x3c
  9. 03 0000008c`0a0bdaa0 00007ffc`257b2efc clr!CLREventWaitHelper+0x1f
  10. 04 0000008c`0a0bdb00 00007ffc`256c821d clr!CLREventBase::WaitEx+0x71
  11. 05 0000008c`0a0bdb90 00007ffc`256c8120 clr!standalone::`anonymous namespace'::CreateSuspendableThread+0x10c
  12. 06 0000008c`0a0bdc50 00007ffc`257b9e4c clr!GCToEEInterface::CreateThread+0x170
  13. 07 0000008c`0a0bde40 00007ffc`257b8543 clr!WKS::gc_heap::prepare_bgc_thread+0x4c
  14. 08 0000008c`0a0bde70 00007ffc`256be9f7 clr!WKS::gc_heap::garbage_collect+0xfbb37
  15. 09 0000008c`0a0bdeb0 00007ffc`256c0c47 clr!WKS::GCHeap::GarbageCollectGeneration+0xef
  16. 0a 0000008c`0a0bdf00 00007ffc`255dc7b3 clr!WKS::GCHeap::Alloc+0x29c
  17. 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 方法一致,代码骨架图如下:


  1. // dllmain.cpp : Defines the entry point for the DLL application.
  2. #include "pch.h"
  3. BOOL APIENTRY DllMain( HMODULE hModule,
  4. DWORD ul_reason_for_call,
  5. LPVOID lpReserved
  6. )
  7. {
  8. switch (ul_reason_for_call)
  9. {
  10. case DLL_PROCESS_ATTACH:
  11. case DLL_THREAD_ATTACH:
  12. case DLL_THREAD_DETACH:
  13. case DLL_PROCESS_DETACH:
  14. break;
  15. }
  16. return TRUE;
  17. }

从 switch 中的枚举参数来看,就是 dll 加载和卸载,线程创建和销毁,有此 DllMain 方法的 dll 都会收到通知,在进入到这个 DllMain 之前会首先获取到一个全局的 进程加载锁(LdrpLoaderLock)

既然 GC 过程中不能创建 CreateThread,那必然有人在持有这个 LdrpLoaderLock 锁,接下来的问题就是如何找到 哪个线程正在持有 LdrpLoaderLock ? 这就涉及到 windows 操作系统的 基础知识了。

3. 谁在持有 LdrpLoaderLock 锁?

LdrpLoaderLock 变量是在 ntdll.dll 用户态网关dll中,可以用 x ntdll!LdrpLoaderLock 命令检索,然后看下是作为哪个临界区持有的。


  1. 0:044> x ntdll!LdrpLoaderLock
  2. 00007ffc`601cf4f8 ntdll!LdrpLoaderLock = <no type information>
  3. 0:044> dt _RTL_CRITICAL_SECTION 00007ffc`601cf4f8
  4. atl100!_RTL_CRITICAL_SECTION
  5. +0x000 DebugInfo : 0x00007ffc`601cf978 _RTL_CRITICAL_SECTION_DEBUG
  6. +0x008 LockCount : 0n-2
  7. +0x00c RecursionCount : 0n1
  8. +0x010 OwningThread : 0x00000000`0000138c Void
  9. +0x018 LockSemaphore : (null)
  10. +0x020 SpinCount : 0x4000000

从卦中看,当前 138c 号线程持有了这个临界区,接下来切到这个线程看下它的线程栈即可。


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


  1. 70 Id: 300.4f0 Suspend: 0 Teb: 0000008c`102e1000 Unfrozen
  2. # Child-SP RetAddr Call Site
  3. 00 0000008c`0ecff388 00007ffc`6008902d ntdll!NtWaitForSingleObject+0x14
  4. 01 0000008c`0ecff390 00007ffc`600b29a7 ntdll!LdrpDrainWorkQueue+0x15d
  5. 02 0000008c`0ecff3d0 00007ffc`600e76d5 ntdll!LdrpInitializeThread+0x8b
  6. 03 0000008c`0ecff4b0 00007ffc`600e7633 ntdll!_LdrpInitialize+0x89
  7. 04 0000008c`0ecff550 00007ffc`600e75de ntdll!LdrpInitialize+0x3b
  8. 05 0000008c`0ecff580 00000000`00000000 ntdll!LdrInitializeThunk+0xe
  9. 71 Id: 300.1c88 Suspend: 0 Teb: 0000008c`102e5000 Unfrozen
  10. # Child-SP RetAddr Call Site
  11. 00 0000008c`0f4ff268 00007ffc`6008902d ntdll!NtWaitForSingleObject+0x14
  12. 01 0000008c`0f4ff270 00007ffc`600b29a7 ntdll!LdrpDrainWorkQueue+0x15d
  13. 02 0000008c`0f4ff2b0 00007ffc`600e76d5 ntdll!LdrpInitializeThread+0x8b
  14. 03 0000008c`0f4ff390 00007ffc`600e7633 ntdll!_LdrpInitialize+0x89
  15. 04 0000008c`0f4ff430 00007ffc`600e75de ntdll!LdrpInitialize+0x3b
  16. 05 0000008c`0f4ff460 00000000`00000000 ntdll!LdrInitializeThunk+0xe
  17. 72 Id: 300.15c0 Suspend: 0 Teb: 0000008c`102e7000 Unfrozen
  18. # Child-SP RetAddr Call Site
  19. 00 0000008c`0f8ff278 00007ffc`6008902d ntdll!NtWaitForSingleObject+0x14
  20. 01 0000008c`0f8ff280 00007ffc`600b29a7 ntdll!LdrpDrainWorkQueue+0x15d
  21. 02 0000008c`0f8ff2c0 00007ffc`600e76d5 ntdll!LdrpInitializeThread+0x8b
  22. 03 0000008c`0f8ff3a0 00007ffc`600e7633 ntdll!_LdrpInitialize+0x89
  23. 04 0000008c`0f8ff440 00007ffc`600e75de ntdll!LdrpInitialize+0x3b
  24. 05 0000008c`0f8ff470 00000000`00000000 ntdll!LdrInitializeThunk+0xe
  25. 73 Id: 300.764 Suspend: 0 Teb: 0000008c`102ef000 Unfrozen
  26. # Child-SP RetAddr Call Site
  27. 00 0000008c`0fcff388 00007ffc`6008902d ntdll!NtWaitForSingleObject+0x14
  28. 01 0000008c`0fcff390 00007ffc`600b29a7 ntdll!LdrpDrainWorkQueue+0x15d
  29. 02 0000008c`0fcff3d0 00007ffc`600e76d5 ntdll!LdrpInitializeThread+0x8b
  30. 03 0000008c`0fcff4b0 00007ffc`600e7633 ntdll!_LdrpInitialize+0x89
  31. 04 0000008c`0fcff550 00007ffc`600e75de ntdll!LdrpInitialize+0x3b
  32. 05 0000008c`0fcff580 00000000`00000000 ntdll!LdrInitializeThunk+0xe

可以看到,有很多的线程都卡死在 ntdll!LdrpInitializeThread+0x8b 处无法进行下去,那这个方法到底在做什么呢?可以看下 反汇编代码


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

从汇编中可以清晰的看到,都卡在获取加载锁 ntdll!LdrpAcquireLoaderLock 函数上。

三:总结

这是一个由 cogxImagingDevice.dll引发的程序死锁,查了下百度是一个商业版的 视觉图像库,对此我也无法解决,只能建议朋友。

  1. 熟悉下这个 dll 的配置,如果不是配置造成建议提交官方解决。

  2. 争取做到 C# 和 C++ 的进程级隔离,或者干脆替换掉 cogxImagingDevice.dll ,虽然这个难度有点大。

这个 dump 给我们的教训是: 当 C# 和 C++ 混在一起,争取做到最大可能的隔离,一旦出现问题非常考验你对 windows 底层知识的理解,分析排错门槛很高。

记一次 .NET 某工控自动化控制系统 卡死分析的更多相关文章

  1. .net全栈开发-c#面向对象与工控自动化分拣上位机

    一.前言 开始做了两年web.期间也整了一段时间winform.后来做了两年工控上位机,也就是做工控这两年发现机器跟面向对象真是如此贴切,也是我从处理数据和流程的思维转变为面向对象思维的开始.这对我后 ...

  2. 记一次 .NET 某工控数据采集平台 线程数 爆高分析

    一:背景 1. 讲故事 前几天有位朋友在 B站 加到我,说他的程序出现了 线程数 爆高的问题,让我帮忙看一下怎么回事,截图如下: 说来也奇怪,这些天碰到了好几起关于线程数无缘无故的爆高,不过那几个问题 ...

  3. 记一次 .NET 某工控视觉软件 非托管泄漏分析

    一:背景 1.讲故事 最近分享了好几篇关于 非托管内存泄漏 的文章,有时候就是这么神奇,来求助的都是这类型的dump,一饮一啄,莫非前定.让我被迫加深对 NT堆, 页堆 的理解,这一篇就给大家再带来一 ...

  4. 记一次 .NET 某企业OA后端服务 卡死分析

    一:背景 1.讲故事 前段时间有位朋友微信找到我,说他生产机器上的 Console 服务看起来像是卡死了,也不生成日志,对方也收不到我的httpclient请求,不知道程序出现什么情况了,特来寻求帮助 ...

  5. 记一次 .NET 某物管后台服务 卡死分析

    一:背景 1. 讲故事 这几个月经常被朋友问,为什么不更新这个系列了,哈哈,确实停了好久,主要还是打基础去了,分析 dump 的能力不在于会灵活使用 windbg,而是对底层知识有一个深厚的理解,比如 ...

  6. Wireshark工控协议

    Wireshark是一个强大开源流量与协议分析工具,除了传统网络协议解码外,还支持众多主流和标准工控协议的分析与解码. 序号 协议类型 源码下载 简介 1 Siemens S7 https://git ...

  7. 开源纯C#工控网关+组态软件(七)数据采集与归档

    一.   引子 在当前自动化.信息化.智能化的时代背景下,数据的作用日渐凸显.而工业发展到如今,科技含量和自动化水平均显著提高,但对数据的采集.利用才开始起步. 对工业企业而言,数据采集日益受到重视, ...

  8. 开源纯C#工控网关+组态软件(十)移植到.NET Core

    一.   引子 写这个开源系列已经十来篇了.自从十年前注册博客园以来,关注了张善友.老赵.xiaotie.深蓝色右手等一众大牛,也围观了逗比的吉日嘎啦.精密顽石等形形色色的园友.然而整整十年一篇文章都 ...

  9. 两款工控控件对比评测:Iocomp和ProEssentials

    对于程序员来说,要凭一己之力开发出漂亮逼真的工控仪表和工控图表是非常耗时间和精力的,那么使用专业的第三方控件就是不错的选择,不仅节约开发时间,降低了项目风险,最重要的是第三方控件写的程序更专业,工控图 ...

随机推荐

  1. 联盟链 Hyperledger Fabric 应用场景

    一.说明 本文主要通过一个例子分享以 Hyperledger Fabric 为代表的联盟链应用场景. 关于 Fabric 的相关概念请先参考文章 <Hyperledger Fabric 核心概念 ...

  2. SQL如何用表A更新表B

    文章标题很短,因为问题的描述过于具体,标题就会显得过长. 这个问题更为准确地描述应该是这样:表结构雷同或者有相似字段的两张表A和B,如何用A表的字段数据去更新B表字段的数据? 操作方法: 1 upda ...

  3. 专家PID控制仿真学习

    目录 专家控制 专家系统 专家控制 学习笔记,用于记录学习 资料:<智能控制>(第四版)--刘金琨 专家系统 一.专家系统的定义 专家系统是一类包含知识和推理的智能计算机程序,其内部包含某 ...

  4. jenkins 流水线自动化部署 手动下载安装插件包

    如果有些插件不能通过可选插件安装,可以进行选择高级并上传插件包,插件包链接地址为:http://updates.jenkins-ci.org/download/plugins/ 同时在高级中可以更换下 ...

  5. 将 Ubuntu 16.04 LTS 的 Unity 启动器移动到桌面底部命令

    将 Ubuntu 16.04 LTS 的 Unity 启动器移动到桌面底部命令: gsettings set com.canonical.Unity.Launcher launcher-positio ...

  6. AsList()方法详解

    AsList()方法详解 在Java中,我们应该如何将一个数组array转换为一个List列表并赋初始值?首先想到的肯定是利用List自带的add()方法,先new一个List对象,再使用add()方 ...

  7. 重学ES系列之字符串方面的处理

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  8. 【Parcel 2 + Vue 3】从0到1搭建一款极快,零配置的Vue3项目构建工具

    前言 一周时间,没见了,大家有没有想我啊!哈哈!我知道肯定会有的.言归正传,我们切入正题.上一篇文章中我主要介绍了使用Vite2+Vue3+Ts如何更快的入手项目.那么,今天我将会带领大家认识一个新的 ...

  9. Linux文件的特殊属性

    文件的特殊属性 作用:文件的权限不能显示root用户,为了防止root用户的误操作,所以需要特殊属性来防止root用户的误操作. chattr工具: 可以给文件添加特殊的属性 +i:对这个文件不能修改 ...

  10. Windows 通过本地计算机IP链接Mysql设置

    前言 1.Mysql-1130错误:无法远程连接 错误:ERROR 1130: Host '192.168.1.3' is not allowed to connect to thisMySQL se ...