.NET外挂系列:7. harmony在高级调试中的一些实战案例
一:背景
1. 讲故事
如果你读完前六篇,我相信你对 harmony 的简单使用应该是没什么问题了,现在你处于手拿锤子看谁都是钉子的情况,那这篇我就找高级调试里非常经典的 3个钉子
让大家捶一锤。
二:三大故障案例
1. ConcurrentBag 大集合问题
在高级调试中经常会遇到一类问题就是托管内存暴涨
,最终在托管堆上发现了超大的一个集合,windbg 输出如下:
0:014> !gcroot 028266c9ff30
HandleTable:
0000028262d51328 (strong handle)
-> 0282675459a0 System.Object[]
-> 0282675459c8 System.Threading.ThreadLocal<System.Collections.Concurrent.ConcurrentBag<Example_20_1_1.Student>+WorkStealingQueue>+LinkedSlotVolatile[]
-> 028267545a00 System.Threading.ThreadLocal<System.Collections.Concurrent.ConcurrentBag<Example_20_1_1.Student>+WorkStealingQueue>+LinkedSlot
-> 028267545a30 System.Collections.Concurrent.ConcurrentBag<Example_20_1_1.Student>+WorkStealingQueue
-> 028267fe0198 Example_20_1_1.Student[]
-> 028266c9ff30 Example_20_1_1.Student
0:014> !dumpobj /d 28267545a30
Name: System.Collections.Concurrent.ConcurrentBag`1+WorkStealingQueue[[Example_20_1_1.Student, Example_20_1_1]]
File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.13\System.Collections.Concurrent.dll
Fields:
MT Field Offset Type VT Attr Value Name
00007fff8fc31188 4000017 18 System.Int32 1 instance 0 _headIndex
00007fff8fc31188 4000018 1c System.Int32 1 instance 1000000 _tailIndex
00007fff8fe76168 4000019 8 System.__Canon[] 0 instance 0000028267fe0198 _array
...
从windbg的输出中可以看到ConcurrentBag
中有100w条记录,现在我就特别想知道,这个ConcurrentBag
的变量是什么,谁在不断的Add操作?这刚好是 harmony 的大显神威之处,由于引用类型的泛型参数统一由__Canon
替代,这里我就使用它的基类 object,参考代码如下:
namespace Example_20_1_1
{
internal class Program
{
static void Main(string[] args)
{
var harmony = new Harmony("com.example.threadhook");
harmony.PatchAll();
RunWork();
Console.ReadLine();
}
static void RunWork()
{
ConcurrentBag<Student> studentBags = new ConcurrentBag<Student>();
studentBags.Add(new Student() { Id = 1 });
studentBags.Add(new Student() { Id = 2 });
ConcurrentBag<Person> personBags = new ConcurrentBag<Person>();
personBags.Add(new Person() { Id = 1 });
}
}
[HarmonyPatch(typeof(ConcurrentBag<object>), "Add", new Type[] { typeof(object) })]
public class ConcurrentBagHook
{
public static void Prefix(object __instance) { }
public static void Postfix(object __instance, object __0)
{
var count = Traverse.Create(__instance).Property("Count").GetValue<int>();
Console.WriteLine($"泛型参数:{__0.GetType()},当前Count={count}");
Console.WriteLine(Environment.StackTrace);
}
}
public class Student { public int Id { get; set; } }
public class Person { public int Id { get; set; } }
}
从卦中可以看到不同类型的 ConcurrentBag
的集合元素数,以及对应的上层调用栈,根据调用栈自然就能找到问题,即使它是在第三方sdk中。
2. 非主线程创建UI控件导致卡死
这个问题是 wpf/winform
常遇到的经典问题,介绍的再多也不为过,凡是遇到这种经典都会有这样的调用栈。
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)
...
底层原理我在 https://www.cnblogs.com/huangxincheng/p/18668388
这一篇中跟大家详细聊过,这里就不细说了,在这里我只要追踪到那个不该出生的control 就算赢了,即Application下的内部类 MarshalingControl,参考代码如下:
namespace WindowsFormsApp1
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
var harmony = new Harmony("com.example.marshalingcontrolhook");
harmony.PatchAll();
}
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();
}
}
[HarmonyPatch]
public class MarshalingControlHook
{
[HarmonyTargetMethod]
static MethodBase TargetMethod()
{
var methodInfo = AccessTools.Inner(typeof(Application), "MarshalingControl").Constructor();
return methodInfo;
}
public static void Prefix()
{
Debug.WriteLine("----------------------------");
Debug.WriteLine($"控件创建线程:{Thread.CurrentThread.ManagedThreadId}");
Debug.WriteLine(Environment.StackTrace);
Debug.WriteLine("----------------------------");
}
}
}
从卦中可以轻松的看到,原来是用户代码 backgroundWorker1_DoWork
创建的 MarshalingControl 类,自此真相大白。
3. 孤儿锁问题
在大家的潜意识中都会认为lock锁都是有进有出
,但在真实的场景下也会存在 有进没出
的情况,那是什么场景呢?对,就是 lock 处理非托管代码的时候,如果非托管代码意外让当前线程退出,就会遇到这种经典的 孤儿锁
现象,参考代码如下:
internal class Program
{
[DllImport("Example_20_1_5", CallingConvention = CallingConvention.Cdecl)]
public extern static void dowork();
public static object lockMe = new object();
static void Main(string[] args)
{
var harmony = new Harmony("com.example.monitorhook");
harmony.PatchAll();
for (int i = 0; i < 3; i++)
{
Task.Run(() =>
{
lock (lockMe)
{
Console.WriteLine("1. 调用 C++ 代码...");
dowork();
Console.WriteLine("2. C++ 代码执行完毕...");
}
});
}
Console.ReadLine();
}
}
代码中的 dowork 是由 C 实现的,参考如下:
extern "C"
{
_declspec(dllexport) void dowork();
}
#include "iostream"
#include <Windows.h>
using namespace std;
void dowork()
{
ExitThread(0);
}
启动程序后,你会发现 !syncblk
中对object的持有线程丢了。。。一旦丢失,就会污染 object
的对象头,导致其他线程一直等待 持有线程
的释放,最终引发程序卡死的灾难后果,参考如下:
0:008> !syncblk
Index SyncBlock MonitorHeld Recursion Owning Thread Info SyncBlock Owner
1 02D562D4 5 1 02D4D400 0 XXX 0504c1b0 System.Object
-----------------------------
Total 2
CCW 0
RCW 0
ComClassFactory 0
Free 0
上面的XXX
就是丢失的持有线程
,接下来的问题就是洞察到底是哪个线程持有锁之后意外退出了。。。这也是 harmony 的强项,我们对 lock 的底层 Monitor.Enter
进行监控,通过 object
的内存地址观察当初是谁调用的,修改后的完整代码如下:
internal class Program
{
[DllImport("Example_20_1_5", CallingConvention = CallingConvention.Cdecl)]
public extern static void dowork();
public static object lockMe = new object();
static void Main(string[] args)
{
var harmony = new Harmony("com.example.monitorhook");
harmony.PatchAll();
for (int i = 0; i < 3; i++)
{
Task.Run(() =>
{
lock (lockMe)
{
Console.WriteLine("1. 调用 C++ 代码...");
dowork();
Console.WriteLine("2. C++ 代码执行完毕...");
}
});
}
Console.ReadLine();
}
}
[HarmonyPatch]
public class MonitorHook
{
[HarmonyTargetMethod]
static MethodBase TargetMethod()
{
var enterMethodInfo = AccessTools.Method(typeof(Monitor), "Enter", new[] { typeof(object), typeof(bool).MakeByRefType() });
return enterMethodInfo;
}
public static unsafe void Postfix(object obj)
{
void** ptr = (void**)Unsafe.AsPointer(ref obj);
//注意:不要使用带 lock 的底层方法,否则会导致 死循环,建议将内容通过 c++ 写入。
Debug.WriteLine("-----------------------");
Debug.WriteLine($"对象引用地址: 0x{(long)(*ptr):X8} , tid={Thread.CurrentThread.ManagedThreadId}, 调用栈:\n {Environment.StackTrace}");
Debug.WriteLine("-----------------------");
}
}
程序执行后,观察 output 和 windbg 的输出信息,参考如下:
-----------------------
对象引用地址: 0x057CCFD8 , tid=4, 调用栈:
at System.Environment.get_StackTrace()
at Example_20_1_1.MonitorHook.Postfix(Object obj) in D:\skyfly\20.20250116\src\Example\Example_20_1_1\Program.cs:line 61
at System.Threading.Monitor.Enter_Patch1(Object obj, Boolean& lockTaken)
at Example_20_1_1.Program.<>c.<Main>b__2_0() in D:\skyfly\20.20250116\src\Example\Example_20_1_1\Program.cs:line 31
at System.Threading.Tasks.Task.InnerInvoke()
at System.Threading.Tasks.Task.<>c.<.cctor>b__281_0(Object obj)
at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state)
at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot, Thread threadPoolThread)
at System.Threading.Tasks.Task.ExecuteEntryUnsafe(Thread threadPoolThread)
at System.Threading.Tasks.Task.ExecuteFromThreadPool(Thread threadPoolThread)
at System.Threading.ThreadPoolWorkQueue.Dispatch()
at System.Threading.PortableThreadPool.WorkerThread.WorkerThreadStart()
at System.Threading.Thread.StartCallback()
-----------------------
0:008> !syncblk
Index SyncBlock MonitorHeld Recursion Owning Thread Info SyncBlock Owner
5 0AE90184 5 1 035EC578 0 XXX 057ccfd8 System.Object
-----------------------------
Total 6
CCW 0
RCW 0
ComClassFactory 0
Free 0
根据上面调用栈的输出结果,原来这个 057ccfd8
的 object 是由 b__2_0
方法调用的,在真实场景中可能有多处,不过此时我们把范围已经缩小到了极致。
这里还有一个告警点,即我用了 Debug.WriteLine 而没有使用 Console.WriteLine 是因为后者本身就带有锁,使用的话就直接死循环了,建议大家写一个C的导出函数来输出内容。
三:总结
本篇列出的3个案例在.NET高级调试
领域中还是非常经典的,如果用的合适,相信对你找出程序的疑难杂症
事半功倍。
.NET外挂系列:7. harmony在高级调试中的一些实战案例的更多相关文章
- .NET高级调试系列-Windbg调试入门篇
Windbg是.NET高级调试领域中不可或缺的一个工具和利器,也是日常我们分析解决问题的必备.准备近期写2篇精华文章,集中给大家分享一下如果通过Windbg进行.NET高级调试. 今天我们来一篇入门的 ...
- 【ABAP系列】SAP ABAP 高级业务应用程序编程(ABAP)
公众号:SAP Technical 本文作者:matinal 原文出处:http://www.cnblogs.com/SAPmatinal/ 原文链接:[ABAP系列]SAP ABAP 高级业务应用程 ...
- 给Source Insight做个外挂系列之三--构建外挂软件的定制代码框架
上一篇文章介绍了“TabSiPlus”是如何进行代码注入的,本篇将介绍如何构建一个外挂软件最重要的部分,也就是为其扩展功能的定制代码.本文前面提到过,由于windows进程管理的限制,扩展代码必须以动 ...
- 给Source Insight做个外挂系列之一--发现Source Insight
一提到外挂程序,大家肯定都不陌生,QQ就有很多个版本的去广告外挂,很多游戏也有用于扩展功能或者作弊的工具,其中很多也是以外挂的形式提供的.外挂和插件的区别在于插件通常依赖于程序的支持,如果程序不支持插 ...
- [Android Studio 权威教程]断点调试和高级调试
好了开始写一个简单的调试程序,我们先来一个for循环 ? 1 2 3 4 5 6 7 8 <code class="language-java hljs ">for ( ...
- ###Android 断点调试和高级调试###
转自:http://www.2cto.com/kf/201506/408358.html 有人说Android 的调试是最坑的,那我只能说是你不会用而已,我可以说Android Studio的调试是我 ...
- sed修炼系列(三):sed高级应用之实现窗口滑动技术
html { font-family: sans-serif } body { margin: 0 } article,aside,details,figcaption,figure,footer,h ...
- 《Visual C++ 2010入门教程》系列六:VC2010常见调试技术
<Visual C++ 2010入门教程>系列六:VC2010常见调试技术 犹豫了好久,最终还是决定开始这一章,因为我不清楚到底有没有必要写这样的一章,是应该在这里说明一些简单的调试方 ...
- Android Stuido中断点调试和高级调试
写一个简单的调试程序 import android.os.Bundle; import android.support.v7.app.AppCompatActivity; public class M ...
- Linux高级调试与优化——gdb调试命令
番外 2019年7月26日至27日,公司邀请<软件调试>和<格蠹汇编——软件调试案例集锦>两本书的作者张银奎老师进行<Linux高级调试与优化>培训,有幸聆听张老师 ...
随机推荐
- Ansible - [08] 模块应用
firewalld 模块 使用firewalld模块可以配置防火墙策略 [root@control ~]# cat ~/ansible/firewall.yml --- - hosts: agent ...
- win11 输入法自定义短语输出日期时间变量
自定义短语中输入%yyyy%-%MM%-%dd% %HH%:%mm%:%ss%
- 掌握 K8s Pod 基础应用 (一)
Pod 介绍 Pod结构 每个Pod中都可以包含一个或者多个容器,这些容器可以分为两类: 用户程序所在的容器,数量可多可少 Pause容器,这是每个Pod都会有的一个根容器,它的作用有两个: 可以以它 ...
- 万字长文详解SIFT特征提取
本文对 SIFT 算法进行了详细梳理.SIFT即尺度不变特征变换(Scale-Invariant Feature Transform),是一种用于检测和描述图像局部特征的算法.该算法对图像的尺度和旋转 ...
- 泛型(Generics)
Java中的泛型(Generics)是JDK 5引入的一种特性,它使得类.接口和方法能够以一种类型参数化的方式进行定义和使用.泛型的主要目的是增强代码的类型安全性和可读性,同时减少类型转换(cast) ...
- 【SpringCloud】Zookeeper服务注册与发现
Zookeeper服务注册与发现 Eureka停止更新了,你怎么办 https://github.com/Netflix/eureka/wiki SpringCloud整合Zookeeper替代Eur ...
- # 🤖 **DeepSeek 深度解析 PasteForm:一个让管理端开发爽到飞起的全栈解决方案**
DeepSeek 深度解析 PasteForm:一个让管理端开发爽到飞起的全栈解决方案 各位开发者注意啦!今天我要带大家全方位解剖 PasteForm 这个神奇框架--不仅介绍核心思想,更要重点展示它 ...
- nodejs目录与文件遍历
路径相关函数 path.basename('/foo/bar/baz/asdf/quux.html'); // Returns: 'quux.html' path.basename('/foo/bar ...
- 说说 Java 的执行流程?
Java 的执行流程 Java 的执行流程包括多个阶段,从源码编写到最终程序的执行,涉及到编译.类加载.字节码执行.垃圾回收等多个环节.下面将详细介绍 Java 程序的执行流程. 1. 编写源代码 开 ...
- Quill自定义工具栏
<div id="toolbar"> <button class="ql-bold"></button> <butto ...