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

打印 上一主题 下一主题

主题 2091|帖子 2091|积分 6273

一:背景

1. 讲故事

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


  • ILSpy:观察原生代码
  • 日志: 多看harmony日志,即方法上加盖 HarmonyDebug 特性。
  • DeepSeek:大模型是一个非常好的助手,公道利用定会效率加倍。
否则遇到稍微复杂一点的,真的难搞。。。
二:有趣的IL编织案例

1. 怎样将Sub中的加法改成减法

为了方便演示,我们先上一段代码,实现一个简单的 a+b 操纵,代码如下:
  1.     internal class Program
  2.     {
  3.         static void Main(string[] args)
  4.         {
  5.             var num = MyMath.Sub(40, 30);
  6.             Console.WriteLine($"Result: {num}");
  7.             Console.ReadLine();
  8.         }
  9.     }
  10.     public class MyMath
  11.     {
  12.         public static int Sub(object a, object b)
  13.         {
  14.             var num1 = Convert.ToInt32(a);
  15.             var num2 = Convert.ToInt32(b);
  16.             var num = num1 + num2;
  17.             return num;
  18.         }
  19.     }
复制代码
上面卦中的 Sub 方法的 IL 代码如下:
  1.         .method public hidebysig static
  2.                 int32 Sub (
  3.                         object a,
  4.                         object b
  5.                 ) cil managed
  6.         {
  7.                 .custom instance void [System.Runtime]System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = (
  8.                         01 00 01 00 00
  9.                 )
  10.                 // Method begins at RVA 0x20b0
  11.                 // Header size: 12
  12.                 // Code size: 25 (0x19)
  13.                 .maxstack 2
  14.                 .locals init (
  15.                         [0] int32 num1,
  16.                         [1] int32 num2,
  17.                         [2] int32 sum,
  18.                         [3] int32
  19.                 )
  20.                 IL_0000: nop
  21.                 IL_0001: ldarg.0
  22.                 IL_0002: call int32 [System.Runtime]System.Convert::ToInt32(object)
  23.                 IL_0007: stloc.0
  24.                 IL_0008: ldarg.1
  25.                 IL_0009: call int32 [System.Runtime]System.Convert::ToInt32(object)
  26.                 IL_000e: stloc.1
  27.                 IL_000f: ldloc.0
  28.                 IL_0010: ldloc.1
  29.                 IL_0011: add
  30.                 IL_0012: stloc.2
  31.                 IL_0013: ldloc.2
  32.                 IL_0014: stloc.3
  33.                 IL_0015: br.s IL_0017
  34.                 IL_0017: ldloc.3
  35.                 IL_0018: ret
  36.         } // end of method MyMath::Sub
复制代码
因为Sub怎么大概是a+b,所以现在我的需求就是将 num1 + num2 改成 num1 - num2,从 il 的角度就是将 IL_0011: add 改成 IL_0011: sub 即可,怎样做到呢?用 harmony 的 CodeMatcher 类去更换IL代码即可,完整的代码如下:
  1. namespace Example_20_1_1
  2. {
  3.     internal class Program
  4.     {
  5.         static void Main(string[] args)
  6.         {
  7.             // 应用Harmony补丁                                                               
  8.             var harmony = new Harmony("com.example.patch");
  9.             harmony.PatchAll();
  10.             var num = MyMath.Sub(40, 30);
  11.             Console.WriteLine($"Result: {num}"); // 原应输出70,补丁后输出10                                                               
  12.             Console.ReadLine();
  13.         }
  14.     }
  15.     public class MyMath
  16.     {
  17.         public static int Sub(object a, object b)
  18.         {
  19.             var num1 = Convert.ToInt32(a);
  20.             var num2 = Convert.ToInt32(b);
  21.             var num = num1 + num2; // 此行将被Transpiler修改为减法                                                               
  22.             return num;
  23.         }
  24.     }
  25.     [HarmonyPatch(typeof(MyMath), "Sub")]
  26.     [HarmonyDebug]
  27.     public static class MyMathPatch
  28.     {
  29.         static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
  30.         {
  31.             var codeMatcher = new CodeMatcher(instructions);
  32.             codeMatcher.MatchStartForward(new CodeMatch(OpCodes.Add))     // 匹配加法操作 (add 指令)       
  33.                        .ThrowIfInvalid("Could not find add instruction")
  34.                        .SetOpcodeAndAdvance(OpCodes.Sub);                 // 将 add 指令替换为 sub 指令       
  35.             return codeMatcher.Instructions();
  36.         }
  37.     }
  38. }
复制代码

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


  • MatchStartForward:这个就是游标,定位到 OpCodes.Add 行。
  • ThrowIfInvalid: 如果没有定位到就抛出非常。
  • SetOpcodeAndAdvance:更换 IL中的add为sub,并向下移动一行,可以明白成 i++。
由于在 MyMathPatch 上加了一个 [HarmonyDebug] 特性,打开 harmony.log.txt 的输出结果,乐成看到了更换后的sub,参考如下:
  1. ### Patch: static System.Int32 Example_20_1_1.MyMath::Sub(System.Object a, System.Object b)
  2. ### Replacement: static System.Int32 Example_20_1_1.MyMath::Example_20_1_1.MyMath.Sub_Patch0(System.Object a, System.Object b)
  3. IL_0000: Local var 0: System.Int32
  4. IL_0000: Local var 1: System.Int32
  5. IL_0000: Local var 2: System.Int32
  6. IL_0000: Local var 3: System.Int32
  7. IL_0000: // start original
  8. IL_0000: nop
  9. IL_0001: ldarg.0
  10. IL_0002: call       static System.Int32 System.Convert::ToInt32(System.Object value)
  11. IL_0007: stloc.0
  12. IL_0008: ldarg.1
  13. IL_0009: call       static System.Int32 System.Convert::ToInt32(System.Object value)
  14. IL_000E: stloc.1
  15. IL_000F: ldloc.0
  16. IL_0010: ldloc.1
  17. IL_0011: sub
  18. IL_0012: stloc.2
  19. IL_0013: ldloc.2
  20. IL_0014: stloc.3
  21. IL_0015: br =>      Label0
  22. IL_001A: Label0
  23. IL_001A: ldloc.3
  24. IL_001B: // end original
  25. IL_001B: ret
  26. DONE
复制代码
2. 怎样给Sub加业务逻辑

上面的例子本质上是IL代码的原地更换,接下来我们看下怎样对IL代码进行删增操纵,我的业务需求是这样的,想将 num1 + num2 改成 num1 - num2 - num3,我想要最终的 C# 代码变为这样:
  1.     public class MyMath
  2.     {
  3.         public static int Sub(object a, object b)
  4.         {
  5.             var num1 = Convert.ToInt32(a);
  6.             var num2 = Convert.ToInt32(b);
  7.             var num3 = Convert.ToInt32("20");   // 新增的代码
  8.             var num = num1 - num2 - num3;
  9.             return num;
  10.         }
  11.     }
复制代码
接下来用Transpiler进行编织,代码如下:
  1.     [HarmonyPatch(typeof(MyMath), "Sub")]
  2.     [HarmonyDebug]
  3.     public static class MyMathPatch
  4.     {
  5.         public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions, ILGenerator generator)
  6.         {
  7.             var codeMatcher = new CodeMatcher(instructions, generator)
  8.                 .MatchStartForward(                     // 匹配模式:ldloc.0, ldloc.1, add
  9.                     new CodeMatch(OpCodes.Ldloc_0),
  10.                     new CodeMatch(OpCodes.Ldloc_1),
  11.                     new CodeMatch(OpCodes.Add)
  12.                 )
  13.                 .ThrowIfInvalid("Could not find add operation pattern")
  14.                 // 移除原来的三条指令
  15.                 .RemoveInstructions(3)
  16.                 // 插入新的指令序列
  17.                 .InsertAndAdvance(
  18.                                     new CodeInstruction(OpCodes.Ldloc_0),
  19.                                     new CodeInstruction(OpCodes.Ldloc_1),
  20.                                     new CodeInstruction(OpCodes.Sub),
  21.                                     new CodeInstruction(OpCodes.Ldstr, "20"),
  22.                                     new CodeInstruction(OpCodes.Call, typeof(Convert).GetMethod(
  23.                                                                       nameof(Convert.ToInt32),
  24.                                                                       new[] { typeof(string) })),
  25.                                     new CodeInstruction(OpCodes.Sub)
  26.                 );
  27.             return codeMatcher.InstructionEnumeration();
  28.         }
  29.     }
复制代码
代码的逻辑非常简单,先在IL代码中定位到 num1 + num2,然后删除再写入 num1 - num2 - num3。

3. 怎样添加try catch

末了我们来一个比力实用的修改,即在 Sub 中增加try catch,理想的代码如下:
  1.     public class MyMath
  2.     {
  3.         public static int Sub(object a, object b)
  4.         {
  5.             try
  6.             {
  7.                 var num1 = Convert.ToInt32(a);
  8.                 var num2 = Convert.ToInt32(b);
  9.                 var num = num1 - num2;
  10.                 return num;
  11.             }
  12.             catch (Exception ex)
  13.             {
  14.                 Console.WriteLine(ex.Message);
  15.                 return 0;
  16.             }
  17.         }
  18.     }
复制代码
接下来就要开始编织了,这是从0开始的代码段,完整代码如下:
  1. 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();        }    }
  2.     public class MyMath
  3.     {
  4.         public static int Sub(object a, object b)
  5.         {
  6.             try
  7.             {
  8.                 var num1 = Convert.ToInt32(a);
  9.                 var num2 = Convert.ToInt32(b);
  10.                 var num = num1 - num2;
  11.                 return num;
  12.             }
  13.             catch (Exception ex)
  14.             {
  15.                 Console.WriteLine(ex.Message);
  16.                 return 0;
  17.             }
  18.         }
  19.     }
  20.     [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)); // 返回0            newInstructions.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调试训练营 的学员们功力提升添砖加瓦!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

尚未崩坏

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表