一:背景

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: nop
IL_0001: ldarg.0
IL_0002: call int32 [System.Runtime]System.Convert::ToInt32(object)
IL_0007: stloc.0
IL_0008: ldarg.1
IL_0009: call int32 [System.Runtime]System.Convert::ToInt32(object)
IL_000e: stloc.1
IL_000f: ldloc.0
IL_0010: ldloc.1
IL_0011: add
IL_0012: stloc.2
IL_0013: ldloc.2
IL_0014: stloc.3
IL_0015: br.s IL_0017 IL_0017: ldloc.3
IL_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, add
new 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)); // 返回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调试训练营 的学员们功力提升添砖加瓦!

.NET外挂系列:8. harmony 的IL编织 Transpiler的更多相关文章

  1. 给Source Insight做个外挂系列之三--构建外挂软件的定制代码框架

    上一篇文章介绍了“TabSiPlus”是如何进行代码注入的,本篇将介绍如何构建一个外挂软件最重要的部分,也就是为其扩展功能的定制代码.本文前面提到过,由于windows进程管理的限制,扩展代码必须以动 ...

  2. 给Source Insight做个外挂系列之一--发现Source Insight

    一提到外挂程序,大家肯定都不陌生,QQ就有很多个版本的去广告外挂,很多游戏也有用于扩展功能或者作弊的工具,其中很多也是以外挂的形式提供的.外挂和插件的区别在于插件通常依赖于程序的支持,如果程序不支持插 ...

  3. 给Source Insight做个外挂系列之五--Insight “TabSiPlus”

    “TabSiPlus 外挂插件”主要有两部分组成,分别是“外挂插件加载器”和“插件动态库”.“插件动态库”完成Source Insight窗口的Hook,显示Tab标签栏,截获Source Insig ...

  4. 给Source Insight做个外挂系列之四--分析“Source Insight”

    外挂的目的就是将代码注入到其它进程中,所以必须要有目标进程才能完成注入,而所谓的目标进程通常是某软件的一部分或者是全部,所以要对目标程序有深入地了解.一般外挂都是针对某个应用程序开发的,其装载.运行都 ...

  5. 给Source Insight做个外挂系列之六--“TabSiPlus”的其它问题

    关于如何做一个Source Insight外挂插件的全过程都已经写完了,这么一点东西拖了一年的时间才写完,足以说明我是一个很懒的人,如果不是很多朋友的关心和督促,恐怕是难以完成了.许多朋友希望顺着本文 ...

  6. 给Source Insight做个外挂系列之二--将本地代码注入到Source Insight进程

    上一篇文章介绍了如何发现正在运行的“Source Insight”窗口,本篇将介绍“TabSiPlus”是如何进行代码注入的.Windows 9x以后的Windows操作系统都对进程空间进行了严格的保 ...

  7. 【转】IL编织 借助PostSharp程序集实现AOP

    ref:   C# AOP实现方法拦截器 在写程序的时候,很多方法都加了.日志信息.比如打印方法开始,方法结束,错误信息,等等. 由于辅助性功能的代码几乎是完全相同的,这样就会令同样的代码在各个函数中 ...

  8. 动态IL织入框架Harmony简单入手

    Harmony是一个开放源代码库,旨在在运行时替换.修饰或修改任何现有C#方法.它的主要用在用Mono语言编写的游戏和插件,但是该技术可以与任何.NET版本一起使用.它还照顾对同一方法的多次更改(它们 ...

  9. C#进阶系列——AOP?AOP!

    前言:今天大阅兵,可是苦逼的博主还得坐在电脑前写博客,为了弄清楚AOP,博主也是拼了.这篇打算写写AOP,说起AOP,其实博主接触这个概念也才几个月,了解后才知道,原来之前自己写的好多代码原理就是基于 ...

  10. C#进阶系列——AOP

    一.AOP概念(转自) 老规矩,还是先看官方解释:AOP(Aspect-Oriented Programming,面向切面的编程),它是可以通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程 ...

随机推荐

  1. 面试题54. 二叉搜索树的第k大节点

    地址:https://leetcode-cn.com/problems/er-cha-sou-suo-shu-de-di-kda-jie-dian-lcof/ <?php /** 面试题54. ...

  2. thinkphp6实现仿微信朋友圈,用户可发布图片和文字内容,用户可评论,其他用户可评论文章,也可回复用户评论,多层级评论,无限级评论

    功能:仿微信朋友圈,用户可发布图片和文字内容,用户可评论,其他用户可评论文章,也可回复用户评论,多层级评论,无限级评论数据库示例:朋友圈内容表 article表:id content image li ...

  3. Web前端入门第 17 问:前端开发编辑器及插件推荐

    HELLO,这里是大熊学习前端开发的入门笔记. 本系列笔记基于 windows 系统. 虽然说 Web 前端开发用记事本也能玩,但正常的开发者绝不用记事本玩(大佬除外). 想想要用记事本扣一个淘宝.京 ...

  4. 使用C#创建一个MCP客户端

    前言 网上使用Python创建一个MCP客户端的教程已经有很多了,而使用C#创建一个MCP客户端的教程还很少. 为什么要创建一个MCP客户端呢? 创建了一个MCP客户端之后,你就可以使用别人写好的一些 ...

  5. Docker容器详解

    [] 容器(Container)是一种轻量级的虚拟化技术,它通过操作系统级的虚拟化,将应用程序及其依赖环境打包在一起,确保应用程序可以在任何环境中一致运行.与虚拟机不同,容器共享宿主操作系统的内核,而 ...

  6. gazebo小车模型(附带仿真环境)

    博客地址:https://www.cnblogs.com/zylyehuo/ 参考链接 1.(https://blog.csdn.net/qq_43406338/article/details/109 ...

  7. ANSYS 导出节点的位移数据

    1. 数据保存 确定待提取的节点编号: 获取节点位移变量: 将节点位移变量存储到数组中,用于数据传递: ! 输出对应节点的位移到csv文件 ! 注意同时导入.db和.rst,并切换到/post26模块 ...

  8. 【Docker】简介

    Docker 简介 某个应用,如果可以提供服务,那么就可以打包成docker供给他人使用 是什么 我们具体来看看Docker. 大家需要注意,Docker本身并不是容器,它是创建容器的工具,是应用容器 ...

  9. 学习Django【1】模型

    编辑 models.py 文件,改变模型. 运行 python manage.py makemigrations 为模型的改变生成迁移文件. 运行 python manage.py migrate 来 ...

  10. ro在xe10.3上的安装

    在学习研究RO. RO9.2.101.1295在xe10.3上安装遇到新问题.记录处理的办法: 没有采用执行exe安装的方法.而是采用复制源代码后编译安装. 1.把生成的bpl.dcp安装到默认目录, ...