

记一次 .NET 某新能源汽车锂电池检测程序 UI挂死分析
source link: https://www.cnblogs.com/huangxincheng/p/15245554.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

更多高质量干货:参见我的 GitHub: dotnetfly
1. 讲故事
这世间事说来也奇怪,近两个月有三位朋友找到我,让我帮忙分析下他的程序hangon现象,这三个dump分别涉及: 医疗,新能源,POS系统。截图如下:
那这篇为什么要拿其中的 新能源
说事呢? 因为这位朋友解决的最顺利,在提供的一些线索后比较顺利的找出了问题代码。
说点题外话,我本人对 winform 是不熟的,又奈何它三番五次的出现在我的视野里,所以我决定写一篇文章好好的总结下,介于没有太多的参考资料,能力有限,只能自己试着解读。
二: Windbg 分析
1. 程序现象
开始之前先吐槽一下,这几位大佬抓的dump文件都是 wow64
,也就是用64bit任务管理器抓了32bit的程序,见如下输出:
wow64cpu!CpupSyscallStub+0x9:
00000000`756d2e09 c3 ret
所以就不好用 windbg preview
来分析了,首先要用 !wow64exts.sw
将 64bit 转为 32bit ,本篇用的是 windbg10,好了,既然是UI卡死,首当其冲就是要看一下UI线程到底被什么东西卡住了,可以用命令 !clrstack
看一下。
0:000:x86> !clrstack
OS Thread Id: 0x1d90 (0)
Child SP IP Call Site
0019ee6c 0000002b [HelperMethodFrame_1OBJ: 0019ee6c] System.Threading.WaitHandle.WaitOneNative(System.Runtime.InteropServices.SafeHandle, UInt32, Boolean, Boolean)
0019ef50 6c4fc7c1 System.Threading.WaitHandle.InternalWaitOne(System.Runtime.InteropServices.SafeHandle, Int64, Boolean, Boolean)
0019ef68 6c4fc788 System.Threading.WaitHandle.WaitOne(Int32, Boolean)
0019ef7c 6e094e7e System.Windows.Forms.Control.WaitForWaitHandle(System.Threading.WaitHandle)
0019efbc 6e463b96 System.Windows.Forms.Control.MarshaledInvoke(System.Windows.Forms.Control, System.Delegate, System.Object[], Boolean)
0019efc0 6e09722b [InlinedCallFrame: 0019efc0]
0019f044 6e09722b System.Windows.Forms.Control.Invoke(System.Delegate, System.Object[])
0019f078 6e318556 System.Windows.Forms.WindowsFormsSynchronizationContext.Send(System.Threading.SendOrPostCallback, System.Object)
0019f090 6eef65a8 Microsoft.Win32.SystemEvents+SystemEventInvokeInfo.Invoke(Boolean, System.Object[])
0019f0c4 6eff850c Microsoft.Win32.SystemEvents.RaiseEvent(Boolean, System.Object, System.Object[])
0019f110 6eddb134 Microsoft.Win32.SystemEvents.OnUserPreferenceChanged(Int32, IntPtr, IntPtr)
0019f130 6f01f0b0 Microsoft.Win32.SystemEvents.WindowProc(IntPtr, Int32, IntPtr, IntPtr)
0019f134 001cd246 [InlinedCallFrame: 0019f134]
0019f2e4 001cd246 [InlinedCallFrame: 0019f2e4]
0019f2e0 6dbaefdc DomainBoundILStubClass.IL_STUB_PInvoke(MSG ByRef)
0019f2e4 6db5e039 [InlinedCallFrame: 0019f2e4] System.Windows.Forms.UnsafeNativeMethods.DispatchMessageW(MSG ByRef)
0019f318 6db5e039 System.Windows.Forms.Application+ComponentManager.System.Windows.Forms.UnsafeNativeMethods.IMsoComponentManager.FPushMessageLoop(IntPtr, Int32, Int32)
0019f31c 6db5dc49 [InlinedCallFrame: 0019f31c]
0019f3a4 6db5dc49 System.Windows.Forms.Application+ThreadContext.RunMessageLoopInner(Int32, System.Windows.Forms.ApplicationContext)
0019f3f4 6db5dac0 System.Windows.Forms.Application+ThreadContext.RunMessageLoop(Int32, System.Windows.Forms.ApplicationContext)
0019f420 6db4a7b1 System.Windows.Forms.Application.Run(System.Windows.Forms.Form)
0019f434 003504a3 xxx.Program.Main()
0019f5a8 6f191366 [GCFrame: 0019f5a8]
从调用栈上看,代码是由于 Microsoft.Win32.SystemEvents.OnUserPreferenceChanged
被触发,然后在 System.Windows.Forms.Control.WaitForWaitHandle
处被卡死,从前者的名字上就能看到,OnUserPreferenceChanged(用户首选项)
是一个系统级别的 Microsoft.Win32.SystemEvents
事件,那到底是什么导致了这个系统事件被触发,为此我查了下资料,大概是说:如果应用程序的 Control 注册了这些系统级事件,那么当windows发出 WM_SYSCOLORCHANGE, WM_DISPLAYCHANGED, WM_THEMECHANGED
(主题,首选项,界面显示) 消息时,这些注册了系统级事件的 Control 的handle将会被执行,比如刷新自身。
觉得文字比较拗口的话,我试着画一张图来阐明一下。
从本质上来说,它就是一个观察者模式,但这和UI卡死没有半点关系,充其量就是解决问题前需要了解的背景知识,还有一个重要概念没有说,那就是: WindowsFormsSynchronizationContext
。
2. 理解 WindowsFormsSynchronizationContext
为什么一定要了解 WindowsFormsSynchronizationContext 呢?理解了它,你就搞明白了为什么会卡死,我们知道 winform 的UI线程是一个 STA 模型,它的一个特点就是单线程,其他线程想要更新Control,都需要调度到UI线程的Queue队列中,不存在也不允许并发更新Control的情况,参考如下:
0:000:x86> !t
ThreadCount: 207
UnstartedThread: 0
BackgroundThread: 206
PendingThread: 0
DeadThread: 0
Hosted Runtime: no
Lock
ID OSID ThreadOBJ State GC Mode GC Alloc Context Domain Count Apt Exception
0 1 1d90 003e2430 2026020 Preemptive 00000000:00000000 003db8b8 0 STA
2 2 2804 003f0188 2b220 Preemptive 00000000:00000000 003db8b8 0 MTA (Finalizer)
Winform 还有一个特点:它会给那些创建 Control 的线程配一个 WindowsFormsSynchronizationContext 同步上下文,也就是说如果其他线程想要更新那个 Control,那就必须将更新的值通过 WindowsFormsSynchronizationContext 调度到那个创建它的线程上,这里的线程不仅仅是 UI 线程哦,有了这些基础知识后,再来分析下为什么会被卡死。
3. 卡死的真正原因
再重新看下主线程的调用栈,它的走势是这样的: OnUserPreferenceChanged -> WindowsFormsSynchronizationContext.Send -> Control.MarshaledInvoke -> WaitHandle.WaitOneNative
,哈哈,有看出什么问题吗???
眼尖的朋友会发现,为什么主线程会调用 WindowsFormsSynchronizationContext.Send
方法呢? 难道那个注册 handler的 Control 不是由主线程创建的吗?要想回答这个问题,需要看一下 WindowsFormsSynchronizationContext 类的 destinationThreadRef 字段值,源码如下:
public sealed class WindowsFormsSynchronizationContext : SynchronizationContext, IDisposable
{
private Control controlToSendTo;
private WeakReference destinationThreadRef;
}
可以用 !dso
命令把线程栈上的 WindowsFormsSynchronizationContext 给找出来,简化输出如下:
0:000:x86> !dso
OS Thread Id: 0x1d90 (0)
ESP/REG Object Name
0019ED70 027e441c System.Windows.Forms.WindowsFormsSynchronizationContext
0019EDC8 112ee43c Microsoft.Win32.SafeHandles.SafeWaitHandle
0019F078 11098b74 System.Windows.Forms.WindowsFormsSynchronizationContext
0019F080 1107487c Microsoft.Win32.SystemEvents+SystemEventInvokeInfo
0019F08C 10fa386c System.Object[] (System.Object[])
0019F090 1107487c Microsoft.Win32.SystemEvents+SystemEventInvokeInfo
0019F0AC 027ebf60 System.Object
0019F0C0 10fa386c System.Object[] (System.Object[])
0019F0C8 027ebe3c System.Object
0019F0CC 10fa388c Microsoft.Win32.SystemEvents+SystemEventInvokeInfo[]
...
0:000:x86> !do 11098b74
Name: System.Windows.Forms.WindowsFormsSynchronizationContext
Fields:
MT Field Offset Type VT Attr Value Name
6dbd8f30 4002567 8 ...ows.Forms.Control 0 instance 11098c24 controlToSendTo
6c667c2c 4002568 c System.WeakReference 0 instance 11098b88 destinationThreadRef
0:000:x86> !do 11098b88
Name: System.WeakReference
Fields:
MT Field Offset Type VT Attr Value Name
6c66938c 4000705 4 System.IntPtr 1 instance 86e426c m_handle
0:000:x86> !do poi(86e426c)
Name: System.Threading.Thread
Fields:
MT Field Offset Type VT Attr Value Name
6c663cc4 40018a5 24 System.Int32 1 instance 2 m_Priority
6c663cc4 40018a6 28 System.Int32 1 instance 7 m_ManagedThreadId
6c66f3d8 40018a7 2c System.Boolean 1 instance 1 m_ExecutionContextBelongsToOuterScope
果然不出所料, 从卦象上看 Thread=7
线程上有 Control 注册了系统事件,那 Thread=7 到底是什么线程呢? 可以通过 !t
查看。
0:028:x86> !t
ThreadCount: 207
UnstartedThread: 0
BackgroundThread: 206
PendingThread: 0
DeadThread: 0
Hosted Runtime: no
Lock
ID OSID ThreadOBJ State GC Mode GC Alloc Context Domain Count Apt Exception
0 1 1d90 003e2430 2026020 Preemptive 00000000:00000000 003db8b8 0 STA
2 2 2804 003f0188 2b220 Preemptive 00000000:00000000 003db8b8 0 MTA (Finalizer)
28 7 27f0 0b29cd30 3029220 Preemptive 00000000:00000000 003db8b8 0 MTA (Threadpool Worker)
从卦象上看: ID=7 是一个线程池线程,而且是 MTA 模式,按理说它应该将创建控件的逻辑调度给UI线程,而不是自己创建,所以UI线程一直在 WaitOneNative 处等待 7号线程消息泵响应,所以导致了无限期等待。
4. 7号线程到底创建了什么控件
这又是一个考验底层知识的问题,也困扰着我至今,太难了,我曾今尝试着把 UserPreferenceChangedEventHandler
事件上的所有 handles 捞出来,写了一个脚本大概如下:
"use strict";
// 32bit
let arr = ["xxxx"];
function initializeScript() { return [new host.apiVersionSupport(1, 7)]; }
function log(str) { host.diagnostics.debugLog(str + "\n"); }
function exec(str) { return host.namespace.Debugger.Utility.Control.ExecuteCommand(str); }
function invokeScript() {
for (var address of arr) {
var commandText = ".printf \"%04x\", poi(poi(poi(poi(" + address + "+0x4)+0xc)+0x4))";
var output = exec(commandText).First();
if (parseInt(output) == 0) continue; //not exists thread info
commandText = ".printf \"%04x\", poi(poi(poi(poi(poi(" + address + "+0x4)+0xc)+0x4))+0x28)";
output = exec(commandText).First();
//thread id
var tid = parseInt(output);
if (tid > 1) log("Thread=" + tid + ",systemEventInvokeInfo=" + address);
}
}
输出结果:
||2:2:438> !wow64exts.sw
Switched to Guest (WoW) mode
Thread=7,systemEventInvokeInfo=1107487c
从输出中找到了 7号线程
对应的处理事件 systemEventInvokeInfo ,然后对其追查如下:
0:028:x86> !do 1107487c
Name: Microsoft.Win32.SystemEvents+SystemEventInvokeInfo
Fields:
MT Field Offset Type VT Attr Value Name
6c65ae34 4002e9f 4 ...ronizationContext 0 instance 11098b74 _syncContext
6c6635ac 4002ea0 8 System.Delegate 0 instance 1107485c _delegate
0:028:x86> !DumpObj /d 1107485c
Name: Microsoft.Win32.UserPreferenceChangedEventHandler
Fields:
MT Field Offset Type VT Attr Value Name
6c66211c 40002b0 4 System.Object 0 instance 110747bc _target
6c66211c 40002b1 8 System.Object 0 instance 00000000 _methodBase
6c66938c 40002b2 c System.IntPtr 1 instance 6ebdc00 _methodPtr
6c66938c 40002b3 10 System.IntPtr 1 instance 0 _methodPtrAux
6c66211c 40002bd 14 System.Object 0 instance 00000000 _invocationList
6c66938c 40002be 18 System.IntPtr 1 instance 0 _invocationCount
0:028:x86> !DumpObj /d 110747bc
Name: DevExpress.LookAndFeel.Design.UserLookAndFeelDefault
从输出中可以看到,最后的控件是 DevExpress.LookAndFeel.Design.UserLookAndFeelDefault
,我以为找到了答案,拿着这个结果去 google,结果 devExpress 踢皮球,截图如下:
咳,到这里貌似就查不下去了,有其他资料上说 Control 在跨线程注册 handler 时会经过 MarshalingControl
,所以在这个控件设置bp断点是能够抓到的,参考命令如下:
bp xxx ".echo MarshalingControl creation detected. Callstack follows.;!clrstack;.echo
这里我就没法验证了。
虽然知道这三起事故都是由于非UI线程创建Control所致,但很遗憾的是我尽了最大的知识边界还没有找到最重要的罪魁祸首,不过值得开心的是基于现有线索有一位朋友终于找到了问题代码,真替他开心???,解决办法也很简单,将 创建控件
通过 Invoke 调度到 UI线程 执行。截图如下:
通过这个案例,我发现高级调试真的是一场苦行之旅,且调且珍惜!

Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK