一个超经典 WinForm,WPF 卡死问题的终极反思

news/2025/1/13 12:23:38/文章来源:https://www.cnblogs.com/huangxincheng/p/18668388

一:背景

1. 讲故事

写这篇文章起源于训练营里一位朋友最近在微信聊到他对这个问题使用了一种非常切实可行,简单粗暴的方式,并且也成功解决了公司里几个这样的卡死dump,如今在公司已是灵魂级人物,让我也尝到了什么叫反哺!对,这个东西叫 Harmony, github网址: https://github.com/pardeike/Harmony,一个非常牛逼的C#程序函数修改器。

二:卡死问题的回顾

1. 故障成因

为了方便讲述,先把 WinForm/WPF 程序故障的调用堆栈给大家呈现一下。


0:000:x86> !clrstack
OS Thread Id: 0x4eb688 (0)
Child SP       IP Call Site
002fed38 0000002b [HelperMethodFrame_1OBJ: 002fed38] System.Threading.WaitHandle.WaitOneNative(System.Runtime.InteropServices.SafeHandle, UInt32, Boolean, Boolean)
002fee1c 5cddad21 System.Threading.WaitHandle.InternalWaitOne(System.Runtime.InteropServices.SafeHandle, Int64, Boolean, Boolean)
002fee34 5cddace8 System.Threading.WaitHandle.WaitOne(Int32, Boolean)
002fee48 538d876c System.Windows.Forms.Control.WaitForWaitHandle(System.Threading.WaitHandle)
002fee88 53c5214a System.Windows.Forms.Control.MarshaledInvoke(System.Windows.Forms.Control, System.Delegate, System.Object[], Boolean)
002fee8c 538dab4b [InlinedCallFrame: 002fee8c] 
002fef14 538dab4b System.Windows.Forms.Control.Invoke(System.Delegate, System.Object[])
002fef48 53b03bc6 System.Windows.Forms.WindowsFormsSynchronizationContext.Send(System.Threading.SendOrPostCallback, System.Object)
002fef60 5c774708 Microsoft.Win32.SystemEvents+SystemEventInvokeInfo.Invoke(Boolean, System.Object[])
002fef94 5c6616ec Microsoft.Win32.SystemEvents.RaiseEvent(Boolean, System.Object, System.Object[])
002fefe8 5c660cd4 Microsoft.Win32.SystemEvents.OnUserPreferenceChanged(Int32, IntPtr, IntPtr)
002ff008 5c882c98 Microsoft.Win32.SystemEvents.WindowProc(IntPtr, Int32, IntPtr, IntPtr)
...

这个程序之所以被卡死,底层原因到底大概是这样的。

  1. 程序在t1时间,有非主线程创建了控件。
  2. 程序在t2时间,用户主动或被动做了 远程连接,Windows主题色刷新 等操作,这种系统级操作Windows需要同步刷新给所有UI控件。
  3. 那些非主线程控件由于没有 MessageLoop 机制,导致主线程给这些UI发消息时得不到响应,最终引发悲剧。

t2时间的卡死是由于t1时间的错误创建导致,要想在dump中反向追溯目前是无法做到的,所以要想找到祸根需要监控t1,即MarshalingControl到底是谁创建的,为此我也写过两篇文章来仔细分析此事。

  • (一个超经典 WinForm 卡死问题的再反思 )[https://www.cnblogs.com/huangxincheng/p/16868486.html]
  • (一个超经典 WinForm 卡死问题的最后一次反思)[https://www.cnblogs.com/huangxincheng/p/17654394.html]

第一种方式是启动 windbg 对 System_Windows_Forms_ni System.Windows.Forms.Application+MarshalingControl..ctor 进行拦截,说实话这种方式很多程序员搞不定,原因在于windbg的使用门槛较高,现实中很多程序员连CURD都没摸明白,所以可想而知了。。。

第二种方式是启动 perfview 对 winform/wpf 程序进行监控,直到程序出现卡死停止收集。最后在录播中寻找 MarshalingControl..ctor 的调用栈,这种方式也有不可行的时候,如果说卡死发生在程序启动的10天后,那这个录播文件将会超级超级大,或者有更极端的情况发生。

所以这两种方案都有各自的优缺点,现实可行性虽然有,但不高。。。今天作为终结篇,必须把这个问题安排掉,继续提供两种切实可行的方案。

三:两种修改方案

1. 使用 Harmony 注入

Harmony作为一款运行时C#方法修改器,借助它我完全可以将一些逻辑注入到 MarshalingControl..ctor 中,比如记录下初始化该方法的 堆栈信息 ,是不是就可以轻松找到这个非主线程控件到底是谁?对不对,有了思路,我们在 nuget 上引用 Lib.Harmony ,上代码说话。

public partial class Form1 : Form{public Form1(){InitializeComponent();var harmony = new Harmony("一线码农聊技术");Type applicationType = typeof(Application);Type marshalingControlType = applicationType.GetNestedType("MarshalingControl", BindingFlags.NonPublic);ConstructorInfo constructor = marshalingControlType.GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, Type.EmptyTypes, null);var prefix = typeof(HookMarshalingControl).GetMethod("OnActionExecuting");harmony.Patch(constructor, new HarmonyMethod(prefix));}private void Form1_Load(object sender, EventArgs e){}private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e){Button btn = new Button();var query = btn.Handle;}private void button1_Click(object sender, EventArgs e){backgroundWorker1.RunWorkerAsync();}}/// <summary>/// Hook MarshalingControl 的描述类/// </summary>public class HookMarshalingControl{/// <summary>/// 原生方法之前执行的 action/// </summary>public static void OnActionExecuting(){Console.WriteLine("----------------------------");Console.WriteLine($"控件创建线程:{Thread.CurrentThread.ManagedThreadId}");Console.WriteLine(Environment.StackTrace);Console.WriteLine("----------------------------");}}

卦中的代码逻辑我就不详述了,核心就是将 OnActionExecuting 方法注入到 MarshalingControl..ctor 构造函数里,把程序运行起来后观察 output 窗口,截图如下:

终于是一个卧槽,祸根居然是一个 tid=3 的线程初始化了 new Button() 控件。。。

2. 使用 DnSpy

Harmony 作为一款修改器,它对程序的侵入性是非常高的,目前还是有一些bug,比如对 .NET7 的支持还不是很好,但相对 perfviewwindbg 的方式已经非常轻量级了,极大的降低了使用门槛。

问题来了,那有没有一种对程序无侵入,可行性超高的方式呢?当然是有的,dnspy 此时可以闪亮登场,用过 dnspy 的朋友应该知道它是一款轻量级,免安装绿色的调试器,当然除了调试器功能,它还是一款程序集修改器,可以实现 Harmony 的所有功能,在实践中我们可以将 dnspy copy 到客户机使用 启动调试 或者 附加进程 的方式对程序进行干预。

如何使用 dnspy 对 MarshalingControl..ctor 进行干预呢?可以使用 断点日志 的功能,日志信息如下:

控件创建线程:{Environment.CurrentManagedThreadId} \n $CALLSTACK

有些人可能要问了 $CALLSTACK 是什么东西?很显然是堆栈信息,除了这个关键词还有很多,具体可以看后面的 问号面板

接下来把程序跑起来,观察 output面板。

从面板中可以清楚的看到,原来有个 tid=3 的线程创建了一个 Button 控件,这就是我们要找的祸根。

到这里,可能有些人要说,dnspy 启动 exe 的方式因为各种原因在我们这边行不通,有没有其他的方式呢? 当然是有的,我们还可以在程序启动之后以 进程附加 的方式注入,同样也是一种非常可行且低侵入的方式。

为了能够更早的介入,可以在 Form1 初始化之前弹一个MessageBox,有更好的方式大家也可以说一下,感谢。参考代码如下:

public partial class Form1 : Form{public Form1(){MessageBox.Show("开启你的注入吧...");InitializeComponent();}}

弹框之后,使用 dnspy 的进程附加。

附加好了之后关闭弹框让程序继续运行,点击 buttton 按钮,可以看到 output 上的输出。


11:20:01.548 控件创建线程:<<<当线程位于不安全状态时无法对表达式进行求值。按步调试或运行直到触发断点。>>>  
11:20:01.550   	System.Windows.Forms.Application.MarshalingControl.MarshalingControl
11:20:01.551 	System.Windows.Forms.Application.ThreadContext.MarshalingControl.get
11:20:01.552 	System.Windows.Forms.WindowsFormsSynchronizationContext.WindowsFormsSynchronizationContext
11:20:01.553 	System.Windows.Forms.WindowsFormsSynchronizationContext.InstallIfNeeded
11:20:01.553 	System.Windows.Forms.Control.Control
11:20:01.554 	System.Windows.Forms.ButtonBase.ButtonBase
11:20:01.554 	System.Windows.Forms.Button.Button
11:20:01.554 	WindowsFormsApp1.Form1.backgroundWorker1_DoWork
11:20:01.555 	System.ComponentModel.BackgroundWorker.OnDoWork
11:20:01.555 	System.ComponentModel.BackgroundWorker.WorkerThreadStart
11:20:01.556 	System.Runtime.Remoting.Messaging.StackBuilderSink.AsyncProcessMessage
11:20:01.556 	System.Threading.ExecutionContext.RunInternal
11:20:01.557 	System.Threading.ExecutionContext.Run
11:20:01.557 	System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem
11:20:01.557 	System.Threading.ThreadPoolWorkQueue.Dispatch
11:20:01.558 	[本机到托管的转换]
11:20:01.558 	  

这里稍微提醒一下,tid 在这里没有显示出来,大家可以换成问号面板上的关键词 $TID 即可,不过TID不是最重要的,最重要的是调用栈给弄出来了。

四:总结

作为一名专业的 .NET高级调试师,在这个经典卡死的问题溯源上一直没有提供非常好的解决方案,还是有些内疚的,在我的高级调试之旅中还是会不间断的收到类似dump,相信这篇文章之后,不再有人被它所困扰!
图片名称

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/868641.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

nginx 简单实践:静态资源部署、URL 重写【nginx 实践系列之一】

本文为 nginx 简单实践系列文章之一,主要简单实践了两个内容:静态资源部署、重写,仅供参考。〇、前言 本文为 nginx 简单实践系列文章之一,主要简单实践了两个内容:静态资源部署、重写,仅供参考。 关于 Nginx 基础,以及安装和配置详解,可以参考博主过往文章: https://…

题解:AT_abc353_f [ABC353F] Tile Distance

[ABC353F] Tile Distance 题解 cnblogs 题目传送门:洛谷,Atcoder Solution 很恶心人的分类讨论题。 很显然走大格子大概率比走小格子快。 对终点和起点向上下左右枚举大格子,我们就把问题转化为给两个大格子 \((a,b)\)、\((c,d)\),求怎样走最快。 对角的大格子可以通过 \(2…

数字化转型中的项目管理优化:协作工具的优势与应用

一、企业数字化转型的背景与挑战 1.1 数字化转型的驱动力数字化转型是指企业通过采用数字技术、创新流程和业务模式,提升运营效率、创造新价值并优化客户体验。随着云计算、大数据、人工智能和物联网等技术的不断发展,数字化转型已成为企业实现长期竞争力和持续增长的重要战略…

rk3568屏幕抖动问题

问题描述:有时候操作屏幕界面,发现屏幕有抖动的情况。经跟RK原厂沟通,此问题跟给ddr供电的vdd_logic有关系。vdd_logic默认定义:vdd_logic: DCDC_REG1 {regulator-always-on;regulator-boot-on;regulator-min-microvolt = <500000>;regulator-max-microvolt = <13…

B@se-还原错误字母表转码的base64编码

题目: 密文:MyLkTaP3FaA7KOWjTmKkVjWjVzKjdeNvTnAjoH9iZOIvTeHbvD== JASGBWcQPRXEFLbCDIlmnHUVKTYZdMovwipatNOefghq56rs****kxyz012789+/oh holy shit, something is missing... 第一行是密文,有明显的Base64编码特征(等号结尾) 第二行是大小写字母、数字、+、/,有明显的…

打开浏览器Chrome跳转指定页面并全屏打开

办法来源于https://blog.csdn.net/shaofengzong/article/details/119928096 主要用于大屏数据可视化的项目,设置电脑自启动后,打开浏览器的同时默认跳转指定页面并全屏打开。、 办法通过增加谷歌浏览器的启动参数进行实现。 两种方式实现,需要根据需求进行选择默认全屏打开指…

板栗看板:让供应商全生命周期管理变得如此简单

供应商全生命周期管理(Supplier Lifecycle Management,SLM)是一个结构化、全流程的供应商闭环管理过程,旨在优化供应商关系,提高供应链的整体效率和可持续性。以下是对供应商全生命周期管理的详细阐述供应商全生命周期管理(Supplier Lifecycle Management,SLM)是一个结…

协作管理工具在多部门协作中的优势与应用

一、跨职能团队协作的挑战 跨职能团队的协作面临多个方面的挑战,这些挑战往往会影响团队的工作效率、项目的推进速度以及最终的项目质量。 1.1 信息传递不畅在跨职能团队中,成员来自不同的部门,各自拥有不同的背景、职责和目标。因此,团队成员之间的沟通可能不够顺畅,信息…

side channel

‌Side Channel‌,中文称为“边信道”,是指通过加密软件或硬件运行时产生的各种泄漏信息来获取密文信息的攻击方式。在狭义上,边信道攻击特指针对密码算法的非侵入式攻击,通过分析加密电子设备在运行过程中的边信道信息泄露来破解密码算法。常见的边信道攻击包括计时攻击、…

RocketMQ工具的使用方法

RocketMQ简介 启动rocketmq-Dashboard项目 输入 http://localhost:8888/#/ 即可到rocketmq界面 整体横向菜单分为八个部分:OPS(运维):主要是设置nameserver和配置vipchannel Dashboard(驾驶舱):控制台的dashboard,可以分别按broker和主题来查看消息的数量和趋势。 Cluster(…

【docker】docker desktop换国内源时 apply按钮为灰色or换源失败 解决方法

配docker环境时复制进去国内镜像源后,发现apply按钮为灰色,点不了,如下图解决方法:往下滑,找到下图圈住的选项打勾再回到Docker Engine界面,发现可以点apply按钮了在文本框中添加"registry-mirrors": ["http://mirrors.ustc.edu.cn","http://mi…

ElasticSearch在Windows环境搭建测试

引子 也持续关注大数据相关内容一段时间,大数据内容很多。想了下还是从目前项目需求侧出发,进行相关学习。Elasticsearch(ES)是位于 Elastic Stack(ELK stack) 核心的分布式搜索和分析引擎。Logstash 和 Beats 有助于收集、聚合和丰富您的数据并将其存储在 Elasticsearch…