InjectFix原理学习(实现修复加法的热更)


前期准备


  • 根据读上面的文章,可以了解到,InjectFix步骤分为以下几步
    插桩: 就是通过标签指定需要修复的函数,通过Mono.Cecil插件,在函数最前方插入IL指令,通过唯一key值判断是否有修复代码,如果有将执行修复函数内
    修复: 修改错误的代码,通过Mono.Cecil插件,检查修复函数的代码,生产修复函数的IL指令并存储到文件内,并和上面插桩代码的唯一key值对应
    执行修复代码的IL指令: 这部分就是解析生成的IL指令,也就是虚拟机,这部分有大量的unsafe代码

  • 首先看一下最终效果图,就是把加法写错了,然后通过修复将原来写错的减法修改成加法
  • 首先下面这张图,函数名为Sum是进行加法的,因为写错了,打印出来的结果是-1
  • 下面是通过修复,原代码依然的错误的,但是打印出来的是正确是数值3

插桩

  • 这块就是插桩的代码,执行后会在Sum函数IL指令最上方,插入PatchLoad.HasPatch(0),检查函数,0是因为我这只有一个函数,要修复直接用0这个硬编码就行,如果有修复需要,将会执行genMethod这个生成的代码
  • genMethod的IL指令生成,部分直接采用了硬编码,具体就是实例化一个Call对象,将参数Push进去,并使用PatchLoad的virtualMachine执行Execute函数
    static void InjectMethod(ModuleDefinition module, MethodDefinition method)
    {
        //TODO 使用InsertBefore插入

        // 生成方法
        var genMethod = GenPatchMethod(module, method);


        // 插桩
        var ilp = method.Body.GetILProcessor();
        var startNopIns = method.Body.Instructions[0];
        var brFalseJumpIns = startNopIns.Next; // 条件不满足时跳转的指令

        var ins_ldstr = ilp.Create(OpCodes.Ldstr, "0"); // 插入patchKey
        ilp.InsertBefore(startNopIns, ins_ldstr); //插到index = 0位置

        var incrIndex = 0;
        var method_HasPatch = module.Types.Single(t => t.Name == "PatchLoad").Methods.Single(m => m.Name == "HasPatch");
        var ins_call = ilp.Create(OpCodes.Call, method_HasPatch);
        IncrAddIns(ilp, ins_call, ref incrIndex);
        var ins_brfalse = ilp.Create(OpCodes.Brfalse, brFalseJumpIns);
        IncrAddIns(ilp, ins_brfalse, ref incrIndex);

        // 用于测试直接返回0
        //var ins_ldc_i4_0 = ilp.Create(OpCodes.Ldc_I4_0);
        //IncrAddIns(ilp, ins_ldc_i4_0, ref incrIndex);

        // 拥有Patch时返回执生成方法,并返回
        if(method.Parameters.Count > 0) {
            //如果有参数 把参数从局部放入栈中
            //for (int i = 0; i < method.Parameters.Count; i++) {
            //    IncrAddIns(ilp, Instruction.Create(OpCodes.Ldarg_S, (byte)i), ref incrIndex);
            //}

            // 知道Sum有2个参数 直接方进入就行了
            IncrAddIns(ilp, Instruction.Create(OpCodes.Ldarg_1), ref incrIndex);
            IncrAddIns(ilp, Instruction.Create(OpCodes.Ldarg_2), ref incrIndex);

        }

        IncrAddIns(ilp, Instruction.Create(OpCodes.Call, genMethod), ref incrIndex);

        var ins_ret = ilp.Create(OpCodes.Ret);
        IncrAddIns(ilp, ins_ret, ref incrIndex);
    }
    
    /// <summary>
    /// 生成补丁方法
    /// </summary>
    /// <returns></returns>
    static MethodDefinition GenPatchMethod(ModuleDefinition module, MethodDefinition method) {
        // PatchLoad
        var patchLoadType = module.Types.Single(t => t.Name == "PatchLoad");
        var vmField = patchLoadType.Fields.Single(f => f.Name == "virtualMachine");
        // Call
        var callType = module.Types.Single(t => t.Name == "Call");
        var callBegin = callType.Methods.Single(m => m.Name == "Begin");
        var callPushInt32 = callType.Methods.Single(m => m.Name == "PushInt32");
        var callGetInt32 = callType.Methods.Single(m => m.Name == "GetInt32");
        // VirtualMachine
        var vmExecute = module.Types.Single(t => t.Name == "VirtualMachine")
            .Methods.Single(m => m.Name == "Execute" && m.Parameters.Count == 4);
        // 基础方法
        // 返回值
        // 参数

        var returnType = method.ReturnType;
        Mono.Cecil.MethodAttributes methodAttributes = Mono.Cecil.MethodAttributes.Public | Mono.Cecil.MethodAttributes.Static;
        MethodDefinition patchMethod = new MethodDefinition(GenPatchMethodPrefix, methodAttributes, returnType);

        // 添加参数
        foreach (var parameter in method.Parameters) {
            patchMethod.Parameters.Add(new ParameterDefinition(parameter.ParameterType));
        }

        // var call
        VariableDefinition callVar = new VariableDefinition(callType);
        patchMethod.Body.Variables.Add(callVar);

        var ilp  = patchMethod.Body.GetILProcessor();

        ilp.Append(Instruction.Create(OpCodes.Nop));
        ilp.Append(Instruction.Create(OpCodes.Call, callBegin));
        ilp.Append(Instruction.Create(OpCodes.Stloc_0));
        // 将位于特定索引处的局部变量的地址加载到计算堆栈上
        ilp.Append(Instruction.Create(OpCodes.Ldloca_S, callVar));
        // 将索引为 0 的参数加载到计算堆栈上。
        ilp.Append(Instruction.Create(OpCodes.Ldarg_0));
        ilp.Append(Instruction.Create(OpCodes.Call, callPushInt32));
        ilp.Append(Instruction.Create(OpCodes.Nop));
        ilp.Append(Instruction.Create(OpCodes.Ldloca_S, callVar));
        ilp.Append(Instruction.Create(OpCodes.Ldarg_1));
        ilp.Append(Instruction.Create(OpCodes.Call, callPushInt32));
        ilp.Append(Instruction.Create(OpCodes.Nop));
        // 将静态字段的值推送到堆栈
        ilp.Append(Instruction.Create(OpCodes.Ldsfld, vmField));
        ilp.Append(Instruction.Create(OpCodes.Ldc_I4_0));
        ilp.Append(Instruction.Create(OpCodes.Ldloca_S, callVar));
        ilp.Append(Instruction.Create(OpCodes.Ldc_I4_2));
        ilp.Append(Instruction.Create(OpCodes.Ldc_I4_0));
        ilp.Append(Instruction.Create(OpCodes.Call, vmExecute));
        ilp.Append(Instruction.Create(OpCodes.Nop));

        ilp.Append(Instruction.Create(OpCodes.Ldloca_S, callVar));
        ilp.Append(Instruction.Create(OpCodes.Ldc_I4_0));
        ilp.Append(Instruction.Create(OpCodes.Call, callGetInt32));
        // 将计算堆栈1 存储在局部变量列表中
        //ilp.Append(Instruction.Create(OpCodes.Stloc_1));
        // 将局部变量1 加载到计算堆栈中
        //ilp.Append(Instruction.Create(OpCodes.Ldloc_1));
        ilp.Append(Instruction.Create(OpCodes.Ret));


        // 测试方法
        //ilp.Append(Instruction.Create(OpCodes.Ldstr, "我是生成的方法"));
        //var log_method = module.ImportReference(typeof(Debug).GetMethod("Log", new Type[] { typeof(object) }));
        //ilp.Append(Instruction.Create(OpCodes.Call, log_method));

        // 测试用  直接返回0
        //ilp.Append(Instruction.Create(OpCodes.Ldc_I4_0));
        //ilp.Append(Instruction.Create(OpCodes.Ret));

        patchLoadType.Methods.Add(patchMethod);

        return patchMethod;
    }
  • 执行完这部分代码,可以使用ildasm看看,Sum函数的IL指令的变化,蓝圈是插入的IL指令,红圈是原本Sum的指令

修复

  • 修复就是通过遍历Body.Instructions中所有IL指令,解析并转成VMInstruction格式的指令,我这边图方便,直接序列化Json通过EditorPrefs.SetString存储在Unity编辑器下了,InjectFix就很复杂了最后存储在一个文件里面,感兴趣可以去读InjectFix源码
    static void Fix(ModuleDefinition module, MethodDefinition method) {
        //{
        //  .custom instance void PatchAttribute::.ctor() = (01 00 00 00 ) 
        //  // 代码大小       9 (0x9)
        //  .maxstack  2
        //  .locals init(int32 V_0)
        //  IL_0000: nop
        // IL_0001:  ldarg.1
        //  IL_0002: ldarg.2
        //  IL_0003: add
        // IL_0004:  stloc.0
        //  IL_0005: br.s IL_0007
        //  IL_0007: ldloc.0
        //  IL_0008: ret
        //}
        var ils = method.Body.Instructions;
        var parseIls = new List<VMInstruction>();
        for (int i = 0; i < ils.Count; i++) {
            var il = ils[i];
            var ilStr = ils[i].OpCode.Code.ToString();
            switch (il.OpCode.Code) {
                case Mono.Cecil.Cil.Code.Nop:
                    parseIls.Add(new VMInstruction {
                        Code = Code.StackSpace,
                        Operand = (method.Body.Variables.Count << 16) | method.Body.MaxStackSize
                    }); // local | maxstack
                    break;
                case Mono.Cecil.Cil.Code.Ldarg_0:
                case Mono.Cecil.Cil.Code.Ldarg_1:
                case Mono.Cecil.Cil.Code.Ldarg_2:
                case Mono.Cecil.Cil.Code.Ldarg_3:
                    parseIls.Add(new VMInstruction() {
                        Code = Code.Ldarg,
                        Operand = int.Parse(ilStr.Substring(ilStr.Length - 1)) - 1
                    });
                    break;
                case Mono.Cecil.Cil.Code.Add:
                    parseIls.Add(new VMInstruction {
                        Code = (Code)Enum.Parse(typeof(Code), ilStr),
                        Operand = 0
                    });
                    break;
                case Mono.Cecil.Cil.Code.Stloc_0:
                case Mono.Cecil.Cil.Code.Stloc_1:
                case Mono.Cecil.Cil.Code.Stloc_2:
                case Mono.Cecil.Cil.Code.Stloc_3:
                    parseIls.Add(new VMInstruction {
                        Code = Code.Stloc,
                        Operand = int.Parse(ilStr.Substring(ilStr.Length - 1)),
                    });
                    break;
                case Mono.Cecil.Cil.Code.Ret:
                    parseIls.Add(new VMInstruction {
                        Code = Code.Ret,
                        Operand = method.ReturnType.ToString() == "System.Void" ? 0 : 1,
                    });
                    break;
            }
        }

        foreach (var vmil in parseIls) {
            Debug.LogError(vmil);
        }

        var json = JsonMapper.ToJson(parseIls);
        // Debug.LogError(json);
        EditorPrefs.SetString(FixJson, json);
    }

虚拟机

  • 这个基本就是InjectFix虚拟机实现,今天有点困,改天再写吧

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值