.NET新能源汽车锂电池检测程序UI挂死问题分析

2023-12-08 0 321
目录
  • 一:背景
    • 1. 讲故事
  • 二: Windbg 分析
    • 1. 程序现象
    • 2. 理解 WindowsFormsSynchronizationContext
    • 3. 卡死的真正原因
    • 4. 7号线程到底创建了什么控件
  • 三:总结

    一:背景

    1. 讲故事

    这世间事说来也奇怪,近两个月有三位朋友找到我,让我帮忙分析下他的程序hangon现象,这三个dump分别涉及: 医疗,新能源,POS系统。截图如下:

    .NET新能源汽车锂电池检测程序UI挂死问题分析

    .NET新能源汽车锂电池检测程序UI挂死问题分析

    .NET新能源汽车锂电池检测程序UI挂死问题分析

    那这篇为什么要拿其中的新能源说事呢? 因为这位朋友解决的最顺利,在提供的一些线索后比较顺利的找出了问题代码。

    说点题外话,我本人对 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将会被执行,比如刷新自身。

    觉得文字比较拗口的话,我试着画一张图来阐明一下。

    .NET新能源汽车锂电池检测程序UI挂死问题分析

    从本质上来说,它就是一个观察者模式,但这和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.swSwitched to Guest (WoW) modeThread=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 踢皮球,截图如下:

    .NET新能源汽车锂电池检测程序UI挂死问题分析

    咳,到这里貌似就查不下去了,有其他资料上说 Control 在跨线程注册 handler 时会经过MarshalingControl,所以在这个控件设置bp断点是能够抓到的,参考命令如下:

    bp xxx \”.echo MarshalingControl creation detected. Callstack follows.;!clrstack;.echo

    这里我就没法验证了。

    三:总结

    虽然知道这三起事故都是由于非UI线程创建Control所致,但很遗憾的是我尽了最大的知识边界还没有找到最重要的罪魁祸首,不过值得开心的是基于现有线索有一位朋友终于找到了问题代码,真替他开心😘😘😘,解决办法也很简单,将创建控件通过 Invoke 调度到 UI线程 执行。截图如下:

    .NET新能源汽车锂电池检测程序UI挂死问题分析

    通过这个案例,我发现高级调试真的是一场苦行之旅,且调且珍惜!

    以上就是.NET新能源汽车锂电池检测程序UI挂死问题分析 的详细内容,更多关于.NET锂电池UI挂死的资料请关注悠久资源网其它相关文章!

    您可能感兴趣的文章:

    • .Net Core使用layui多文件上传
    • ASP.NET Core中的通用主机HostBuilder
    • ASP.NET Core MVC中Required与BindRequired用法与区别介绍
    • ASP.NET Core托管模型CreateDefaultBuilder()方法
    • .NET跨平台应用MAUI介绍
    • .Net 6中WebApplicationBuilder介绍和用法

    收藏 (0) 打赏

    感谢您的支持,我会继续努力的!

    打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
    点赞 (0)

    悠久资源 ASP.NET .NET新能源汽车锂电池检测程序UI挂死问题分析 https://www.u-9.cn/biancheng/aspnet/128794.html

    常见问题

    相关文章

    发表评论
    暂无评论
    官方客服团队

    为您解决烦忧 - 24小时在线 专业服务