当前位置: 首页 > article >正文

.NET外挂系列:8. harmony 的IL编织 Transpiler

一:背景

1. 讲故事

前面文章所介绍的一些注入技术都是以方法为原子单位,但在一些罕见的场合中,这种方法粒度又太大了,能不能以语句为单位,那这个就是我们这篇介绍的 Transpiler,它可以修改方法的 IL 代码,甚至重构,所以这就非常考验你的 IL 功底,个人建议在写的时候要多借助如下三个工具:

  • ILSpy:观察原生代码
  • 日志: 多看harmony日志,即方法上加盖 HarmonyDebug 特性。
  • DeepSeek:大模型是一个非常好的助手,合理利用定会效率加倍。

否则遇到稍微复杂一点的,真的难搞。。。

二:有趣的IL编织案例

1. 如何将Sub中的加法改成减法

为了方便演示,我们先上一段代码,实现一个简单的 a+b 操作,代码如下:

    internal class Program{static void Main(string[] args){var num = MyMath.Sub(40, 30);Console.WriteLine($"Result: {num}");Console.ReadLine();}}public class MyMath{public static int Sub(object a, object b){var num1 = Convert.ToInt32(a);var num2 = Convert.ToInt32(b);var num = num1 + num2;return num;}}

上面卦中的 Sub 方法的 IL 代码如下:

.method public hidebysig static int32 Sub (object a,object b) cil managed {.custom instance void [System.Runtime]System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = (01 00 01 00 00)// Method begins at RVA 0x20b0// Header size: 12// Code size: 25 (0x19).maxstack 2.locals init ([0] int32 num1,[1] int32 num2,[2] int32 sum,[3] int32)IL_0000: nopIL_0001: ldarg.0IL_0002: call int32 [System.Runtime]System.Convert::ToInt32(object)IL_0007: stloc.0IL_0008: ldarg.1IL_0009: call int32 [System.Runtime]System.Convert::ToInt32(object)IL_000e: stloc.1IL_000f: ldloc.0IL_0010: ldloc.1IL_0011: addIL_0012: stloc.2IL_0013: ldloc.2IL_0014: stloc.3IL_0015: br.s IL_0017IL_0017: ldloc.3IL_0018: ret} // end of method MyMath::Sub

因为Sub怎么可能是a+b,所以现在我的需求就是将 num1 + num2 改成 num1 - num2,从 il 的角度就是将 IL_0011: add 改成 IL_0011: sub 即可,如何做到呢?用 harmony 的 CodeMatcher 类去替换IL代码即可,完整的代码如下:


namespace Example_20_1_1
{internal class Program{static void Main(string[] args){// 应用Harmony补丁								var harmony = new Harmony("com.example.patch");harmony.PatchAll();var num = MyMath.Sub(40, 30);Console.WriteLine($"Result: {num}"); // 原应输出70,补丁后输出10								Console.ReadLine();}}public class MyMath{public static int Sub(object a, object b){var num1 = Convert.ToInt32(a);var num2 = Convert.ToInt32(b);var num = num1 + num2; // 此行将被Transpiler修改为减法								return num;}}[HarmonyPatch(typeof(MyMath), "Sub")][HarmonyDebug]public static class MyMathPatch{static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions){var codeMatcher = new CodeMatcher(instructions);codeMatcher.MatchStartForward(new CodeMatch(OpCodes.Add))     // 匹配加法操作 (add 指令)	.ThrowIfInvalid("Could not find add instruction").SetOpcodeAndAdvance(OpCodes.Sub);                 // 将 add 指令替换为 sub 指令	return codeMatcher.Instructions();}}}

从卦中的输出看,我们修改成功了,这里稍微说一下 CodeMatcher 的方法。

  • MatchStartForward:这个就是游标,定位到 OpCodes.Add 行。
  • ThrowIfInvalid: 如果没有定位到就抛出异常。
  • SetOpcodeAndAdvance:替换 IL中的add为sub,并向下移动一行,可以理解成 i++。

由于在 MyMathPatch 上加了一个 [HarmonyDebug] 特性,打开 harmony.log.txt 的输出结果,成功看到了替换后的sub,参考如下:


### Patch: static System.Int32 Example_20_1_1.MyMath::Sub(System.Object a, System.Object b)
### Replacement: static System.Int32 Example_20_1_1.MyMath::Example_20_1_1.MyMath.Sub_Patch0(System.Object a, System.Object b)
IL_0000: Local var 0: System.Int32
IL_0000: Local var 1: System.Int32
IL_0000: Local var 2: System.Int32
IL_0000: Local var 3: System.Int32
IL_0000: // start original
IL_0000: nop
IL_0001: ldarg.0
IL_0002: call       static System.Int32 System.Convert::ToInt32(System.Object value)
IL_0007: stloc.0
IL_0008: ldarg.1
IL_0009: call       static System.Int32 System.Convert::ToInt32(System.Object value)
IL_000E: stloc.1
IL_000F: ldloc.0
IL_0010: ldloc.1
IL_0011: sub
IL_0012: stloc.2
IL_0013: ldloc.2
IL_0014: stloc.3
IL_0015: br =>      Label0
IL_001A: Label0
IL_001A: ldloc.3
IL_001B: // end original
IL_001B: ret
DONE

2. 如何给Sub加业务逻辑

上面的例子本质上是IL代码的原地替换,接下来我们看下如何对IL代码进行删增操作,我的业务需求是这样的,想将 num1 + num2 改成 num1 - num2 - num3,我想要最终的 C# 代码变为这样:

public class MyMath{public static int Sub(object a, object b){var num1 = Convert.ToInt32(a);var num2 = Convert.ToInt32(b);var num3 = Convert.ToInt32("20");   // 新增的代码var num = num1 - num2 - num3;return num;}}

接下来用Transpiler进行编织,代码如下:

[HarmonyPatch(typeof(MyMath), "Sub")][HarmonyDebug]public static class MyMathPatch{public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions, ILGenerator generator){var codeMatcher = new CodeMatcher(instructions, generator).MatchStartForward(                     // 匹配模式:ldloc.0, ldloc.1, addnew CodeMatch(OpCodes.Ldloc_0),new CodeMatch(OpCodes.Ldloc_1),new CodeMatch(OpCodes.Add)).ThrowIfInvalid("Could not find add operation pattern")// 移除原来的三条指令.RemoveInstructions(3)// 插入新的指令序列.InsertAndAdvance(new CodeInstruction(OpCodes.Ldloc_0),new CodeInstruction(OpCodes.Ldloc_1),new CodeInstruction(OpCodes.Sub),new CodeInstruction(OpCodes.Ldstr, "20"),new CodeInstruction(OpCodes.Call, typeof(Convert).GetMethod(nameof(Convert.ToInt32),new[] { typeof(string) })),new CodeInstruction(OpCodes.Sub));return codeMatcher.InstructionEnumeration();}}

代码的逻辑非常简单,先在IL代码中定位到 num1 + num2,然后删除再写入 num1 - num2 - num3

3. 如何添加try catch

最后我们来一个比较实用的修改,即在 Sub 中增加try catch,理想的代码如下:

public class MyMath{public static int Sub(object a, object b){try{var num1 = Convert.ToInt32(a);var num2 = Convert.ToInt32(b);var num = num1 - num2;return num;}catch (Exception ex){Console.WriteLine(ex.Message);return 0;}}}

接下来就要开始编织了,这是从0开始的代码段,完整代码如下:


namespace Example_20_1_1
{internal class Program{static void Main(string[] args){// 应用Harmony补丁										var harmony = new Harmony("com.example.patch");harmony.PatchAll();// 测试原始方法										var num = MyMath.Sub("a", 30);Console.WriteLine($"异常: {num}");var num2 = MyMath.Sub(50, 30);Console.WriteLine($"正常: {num2}");Console.ReadLine();}}public class MyMath{public static int Sub(object a, object b){try{var num1 = Convert.ToInt32(a);var num2 = Convert.ToInt32(b);var num = num1 - num2;return num;}catch (Exception ex){Console.WriteLine(ex.Message);return 0;}}}[HarmonyPatch(typeof(MyMath), "Sub")][HarmonyDebug]public static class MyMathPatch{static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> originalInstructions, ILGenerator generator){// 定义标签Label tryStart = generator.DefineLabel();Label tryEnd = generator.DefineLabel();Label catchStart = generator.DefineLabel();Label endLabel = generator.DefineLabel();// 声明局部变量var exVar = generator.DeclareLocal(typeof(Exception)); // 用于存储异常的变量var resultVar = generator.DeclareLocal(typeof(int));   // 用于存储返回值的变量var newInstructions = new List<CodeInstruction>();// 1. try 块开始newInstructions.Add(new CodeInstruction(OpCodes.Nop).WithLabels(tryStart));// 2. 添加原始方法体(保持不变)newInstructions.AddRange(originalInstructions);// 3. 存储结果并离开 try 块newInstructions.Add(new CodeInstruction(OpCodes.Stloc, resultVar));newInstructions.Add(new CodeInstruction(OpCodes.Leave, endLabel).WithLabels(tryEnd));// 4. catch 块newInstructions.Add(new CodeInstruction(OpCodes.Stloc, exVar).WithLabels(catchStart));newInstructions.Add(new CodeInstruction(OpCodes.Nop));newInstructions.Add(new CodeInstruction(OpCodes.Ldloc, exVar));newInstructions.Add(new CodeInstruction(OpCodes.Callvirt,typeof(Exception).GetProperty("Message").GetGetMethod()));newInstructions.Add(new CodeInstruction(OpCodes.Call,typeof(Console).GetMethod("WriteLine", new[] { typeof(string) })));newInstructions.Add(new CodeInstruction(OpCodes.Ldc_I4_0)); // 返回0newInstructions.Add(new CodeInstruction(OpCodes.Stloc, resultVar));newInstructions.Add(new CodeInstruction(OpCodes.Leave, endLabel));// 5. 方法结束(加载结果并返回)newInstructions.Add(new CodeInstruction(OpCodes.Ldloc, resultVar).WithLabels(endLabel));newInstructions.Add(new CodeInstruction(OpCodes.Ret));// 添加异常处理generator.BeginExceptionBlock();generator.BeginCatchBlock(typeof(Exception));generator.EndExceptionBlock();return newInstructions;}}
}

哈哈,上面的代码正如我们所料。。。如果不借助 ILSpy 和 DeepSeek,不敢想象得要浪费多少时间。。。门槛太高了。。。

三:总结

这个系列总计8篇,已经全部写完啦!希望对同行们在解决.NET程序疑难杂症相关问题时提供一些资料和灵感,同时也是对.NET调试训练营 的学员们功力提升添砖加瓦!

http://www.lryc.cn/news/2385118.html

相关文章:

  • 基于netty实现视频流式传输和多线程传输
  • 全面指南:使用Node.js和Python连接与操作MongoDB
  • 游戏引擎学习第308天:调试循环检测
  • Java 海康录像机通过sdk下载的视频无法在线预览问题
  • WPF性能优化之延迟加载(解决页面卡顿问题)
  • 移植 FART 到 Android 10 实现自动化脱壳
  • ES的Refresh、Flush、Merge操作对性能的影响? ES如何实现近实时(NRT)搜索? ES聚合查询的Terms和Cardinality区别?
  • WebXR 虚拟现实开发
  • COMPUTEX 2025 | 广和通创新解决方案共筑AI交互新纪元
  • 了解Android studio 初学者零基础推荐(3)
  • Spring 定时器和异步线程池 实践指南
  • 零基础设计模式——创建型模式 - 生成器模式
  • MD编辑器推荐【Obsidian】含下载安装和实用教程
  • LLama-Factory 遇到的问题
  • I-CON: A UNIFYING FRAMEWORK FOR REPRESENTATION LEARNING
  • Missashe线代题型总结
  • 蓝桥杯13届 卡牌
  • 安卓开发用到的设计模式(1)创建型模式
  • 【PalladiumZ2 使用专栏 3 -- 信号值的获取与设置 及 memory dump 与 memory load】
  • flutter dart 函数语法
  • 课外活动:大语言模型Claude的技术解析 与 自动化测试框架领域应用实践
  • 线程的一些基本知识
  • 【Python打卡Day30】模块与包的导入@浙大疏锦行
  • 26考研|高等代数:λ-矩阵
  • 我店模式系统开发打造本地生活生态商圈
  • 数据库练习(3)
  • OpenGL ES 基本基本使用、绘制基本2D图形
  • spark调度系统核心组件SparkContext、DAGSchedul、TaskScheduler、Taskset介绍
  • BU9792驱动段式LCD
  • Springboot通过SSE实现实时消息返回