记一次 .NET 某医院门诊软件 卡死分析
一:背景
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 ,看下有没有同病相怜的人,结果网上一搜一堆,比如下面这样:


不管怎么说,这一块如果处理不好容易出现死锁和卡死的情况,那是不是正如图中所说 waveOutReset 和 waveOutClose 没有匹配造成的呢?
这就取决于代码中的 _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,让 waveOutReset 和 waveOutClose 方法没有匹配出现,造成了卡死现象。

记一次 .NET 某医院门诊软件 卡死分析的更多相关文章
- 记一次 .NET 某药品仓储管理系统 卡死分析
一:背景 1. 讲故事 这个月初,有位朋友wx上找到我,说他的api过一段时间后,就会出现只有请求,没有响应的情况,截图如下: 从朋友的描述中看样子程序是被什么东西卡住了,这种卡死的问题解决起来相对简 ...
- 记一次 .NET 某金融企业 WPF 程序卡死分析
一:背景 1. 讲故事 前段时间遇到了一个难度比较高的 dump,经过几个小时的探索,终于给找出来了,在这里做一下整理,希望对大家有所帮助,对自己也是一个总结,好了,老规矩,上 WinDBG 说话. ...
- 记一次 .NET 某数控机床控制程序 卡死分析
一:背景 1. 讲故事 前段时间有位朋友微信上找到我,说它的程序出现了卡死,让我帮忙看下是怎么回事? 说来也奇怪,那段时间求助卡死类的dump特别多,被迫训练了一下对这类问题的洞察力 ,再次声明一下, ...
- “深度评测官”——记2020BUAA软工软件案例分析作业
项目 内容 这个作业属于哪个课程 2020春季计算机学院软件工程(罗杰 任建) 这个作业的要求在哪里 个人博客作业-软件案例分析 我在这个课程的目标是 完成一次完整的软件开发经历并以博客的方式记录开发 ...
- 软件产品案例分析--K米
软件产品案例分析--K米 第一部分 调研,评测 评测 个人第一次上手体验 使用的第一款点歌软件,以为就是个遥控而已,使用后发现功能还挺多,能点挺久.觉得很方便,不用挤成一堆点歌了.K米的脸蛋(UI)好 ...
- 团队项目2.0软件改进分析MathAPP
软件改进分析 在此基础上,进行软件的改进. 首先,我们把这个软件理解成一个投入市场的.帮助小朋友进行算术运算练习的APP. 从质量保证的角度,有哪些需要改进的BUG? 从用户的角度(把自己当成小学生或 ...
- [团队项目2.0]软件改进分析MathAPP
软件改进分析 在此基础上,进行软件的改进. 首先,我们把这个软件理解成一个投入市场的.帮助小朋友进行算术运算练习的APP. 从质量保证的角度,有哪些需要改进的BUG? 从用户的角度(把自己当成小学生或 ...
- 第五周课后作业——热门软件创新分析+附加题1&附加题3
鉴于我们寝室都热衷于手游,所以本次热门软件创新分析我就来分析一下几款热门的抽卡型手游. 阴阳师(后文简称YYS)——剧情画风唯美,配音引人入胜 作为网易公司研发的一款3D日式和风回合制游戏,YYS ...
- 福州大学软件工程1816 | W班 第10次作业[个人作业——软件产品案例分析]
作业链接 个人作业--软件产品案例分析 评分细则 本次个人项目分数由两部分组成(课堂得分(老师/助教占比60%,学生占比40%)满分40分+博客分满分60分) 课堂得分和博客得分表 评分统计图 千帆竞 ...
- 书评第003篇:《0day安全:软件漏洞分析技术(第2版)》
本书基本信息 丛书名:安全技术大系 作者:王清(主编),张东辉.周浩.王继刚.赵双(编著) 出版社:电子工业出版社 出版时间:2011-6-1 ISBN:9787121133961 版次:1 页数:7 ...
随机推荐
- .bat 脚本替换文件内容
rem 定义变量延迟环境,关闭回显 @echo off&setlocal enabledelayedexpansion rem 读取a.txt所有内容 for /f "eol=* t ...
- JMeter压测脚本实例:单接口
新建测试计划 添加线程组 添加HTTP请求 配置该请求相关参数 1.请求头部信息 ①HTTP请求同级线程组下添加HTTP信息头部管理器 ②填充该请求所需的头部信息 2.请求体 选中之前增加的HTTP请 ...
- 开源不易、安全慎行,中国软件如何走向文明?丨RTE 技术环境月报 202205
各位开发者小伙伴: 这里是 2022 年第 5 期的 RTE<技术环境月报>--致力于成为对大家"有用"的 Highlight 看板--每月初通过 RTC 开发者社区( ...
- Docke的使用与详解2 --RabbitMQ安装与使用
一.RabbitMQ安装与使用 1.RabbitMQ安装 --RabbbitMQ官网 1>使用docker pull rabbitmq拉取镜像,默认拉取最新版本: 这时你会想,我要拉取指定版本该 ...
- 使用golang+antlr4构建一个自己的语言解析器(一)
Antlr4 简介 ANTLR(全名:ANother Tool for Language Recognition)是基于LL(*)算法实现的语法解析器生成器(parser generator),用Ja ...
- springboot jpa---->总结一下遇到的问题
Native Query throw exception dto code import lombok.Value; @Value public class IdsOnly { Integer id; ...
- 2023年数字化经济与管理科学国际学术会议(CDEMS2023)
基本信息 大会官网:www.cdems.org 大会时间:2023年4月21-23日 大会地点:中国开封 截稿时间:2023年4月16日(此处有变动) 接受/拒稿通知:投稿后1周 收录检索:CPCI, ...
- vue:路由守卫
路由守卫 作用:对路由进行权限控制 配置路由守卫应在暴露前配置 分类:全局守卫.独享守卫.组件内守卫 首先先给需要鉴权的路由设置好meta配置项. meta配置项:是vue-router中的一个对象, ...
- Linux服务器MySQL操作总结
目录 1. Navicat连接服务器MySQL 2. 如何查看MySQL用户名和密码 3. 修改MySQL的登录密码 4. 安装MySQL开发包(Centos7版) 错误:error 1045 (28 ...
- day96:flask:flask-migrate&flask-session&蓝图Blueprint&蓝图的运行机制&基于flask仿照django进行项目架构
目录 1.flask-migrate 2.flask-session 3.蓝图:Blueprint 4.蓝图的运行机制 5.基于flask仿照django进行项目架构 1.准备工作 2.加载配置文件 ...