使用Harmony Transpiler Patch跳过IL指令时遇异常及方法未拦截问题
问题分析与解决方案
为什么原代码无法跳过目标方法
你的判断完全正确——原代码中无论是否匹配到目标指令,都会执行yield return instr,导致原Callvirt指令被完整保留并执行,所以MethodToSkip仍会被调用,只是在它之前多执行了TestMethod。
为什么放入else分支会触发InvalidProgramException
Callvirt指令的执行依赖严格的IL栈状态:调用实例方法时,栈顶必须是该类的实例引用(如果方法带参数,还要依次压入对应参数)。如果直接移除这个Callvirt指令,原本为它准备的栈元素(实例引用、参数)没有被消费,后续指令会读取错误的栈数据,破坏IL的栈平衡,最终触发无效IL代码的异常。
正确的修改方式
方案1:签名匹配时直接替换
如果TestMethod的签名(返回值类型、参数列表)和MethodToSkip完全一致,直接替换原指令即可,此时栈操作逻辑和原指令完全兼容,不会报错:
static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions) { int found = 0; foreach (CodeInstruction instr in instructions) { if (instr.opcode == OpCodes.Callvirt && instr.Calls(methodToSkip)) { found++; // 返回TestMethod的调用,跳过原指令 yield return new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(TestPatch), nameof(TestMethod))); continue; } yield return instr; } if (found == 0) { Plugin.pluginLog.LogError($"Cannot find symbol {methodToSkip}"); } else { Plugin.pluginLog.LogWarning($"Found {found} occurrences of {methodToSkip}"); } }
方案2:签名不匹配时清理栈
如果TestMethod的签名和原方法不同,需要手动清理栈上为原方法准备的参数。比如原MethodToSkip是带1个参数的实例方法,栈上会存在[实例引用, 参数]两个元素,若TestMethod是无参无返回值方法,需要用Pop指令将这些元素从栈中移除:
static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions) { int found = 0; foreach (CodeInstruction instr in instructions) { if (instr.opcode == OpCodes.Callvirt && instr.Calls(methodToSkip)) { found++; // 先执行TestMethod yield return new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(TestPatch), nameof(TestMethod))); // 清理原方法的实例引用和1个参数,共2次Pop yield return new CodeInstruction(OpCodes.Pop); yield return new CodeInstruction(OpCodes.Pop); continue; // 跳过原指令 } yield return instr; } if (found == 0) { Plugin.pluginLog.LogError($"Cannot find symbol {methodToSkip}"); } else { Plugin.pluginLog.LogWarning($"Found {found} occurrences of {methodToSkip}"); } }
注意:需根据
MethodToSkip的实际签名调整Pop次数:
- 实例方法:至少Pop1次(移除实例引用),每个额外参数再Pop1次
- 静态方法:无需移除实例引用,仅按参数数量执行Pop
调试辅助技巧
可以用Harmony的扩展方法查看指令的栈变化,辅助排查栈平衡问题:
// 调试时打印每个指令的栈操作变化 foreach (var instr in instructions) { Plugin.pluginLog.LogDebug($"{instr.opcode} - {instr.operand} | 栈变化量: {instr.GetStackChange()}"); }
内容的提问来源于stack exchange,提问作者graem
相关产品推荐
相关产品推荐

