Injectfix源码解析

在看这篇文章之前必须对il指令有所了解

推荐文章:【小白学C#】浅谈.NET中的IL代码 - 马三小伙儿 - 博客园

基础就不讲了,可以看看官方文档介绍:https://github.com/Tencent/InjectFix

injfectfix原理就是打桩,然后生成指令,injfectfix自己写的虚拟机解析运行补丁指令

[MenuItem("InjectFix/Inject", false, 1)]
public static void InjectAssemblys()
{
    if (EditorApplication.isCompiling || Application.isPlaying)
    {
        UnityEngine.Debug.LogError("compiling or playing");
        return;
    }
    EditorUtility.DisplayProgressBar("Inject", "injecting...", 0);
    try
    {
        InjectAllAssemblys();
    }
    catch(Exception e)
    {
        UnityEngine.Debug.LogError(e);
    }
    EditorUtility.ClearProgressBar();
}

先从注入开始,首先会根据标签

var configure = Configure.GetConfigureByTags(new List<string>() {
                "IFix.IFixAttribute",
                "IFix.InterpretAttribute",
                "IFix.ReverseWrapperAttribute",
            });
生成process_cfg配置,配置里面就是需要操作的类,和黑名单的方法,然后根据配置打桩
inject IFix.Core.dll Assembly-CSharp.dll process_cfg Assembly-CSharp.ill.bytes Assembly-CSharpNew.dll 

 我参数传入IFix.Core.dll,读取配置

if (tranlater.Process(assembly, ilfixAassembly, configure, mode)
    == CodeTranslator.ProcessResult.Processed)
{
    //发现程序集已经被注入,主要是防止已经注入的函数包含的注入逻辑会导致死循环
    Console.WriteLine("Error: the new assembly must not be inject, please reimport the project!");
    return;
}

[Patch]
private void test()
{
    if (WrappersManagerImpl.IsPatched(0))
    {
        WrappersManagerImpl.GetPatch(0).__Gen_Wrap_0(this);
        return;
    }
    abb = 55534355;
    AAA();
}

 这就是注入的函数,会根据WrappersManagerImpl.IsPatched查看是否有补丁,有补丁的话就跳转运行虚拟机指令,没有就原函数运行

public void __Gen_Wrap_0(object P0)
{
	Call call = Call.Begin();
	if (anonObj != null)
	{
		call.PushObject(anonObj);
	}
	call.PushObject(P0);
	virtualMachine.Execute(methodId, ref call, (anonObj == null) ? 1 : 2);
}

那到底补丁是如何工作呢?

unsafe static public VirtualMachine CreateVirtualMachine(int loopCount)
        {
            Instruction[][] methods = new Instruction[][]
            {
                new Instruction[] //int add(int a, int b)
                {
                    new Instruction {Code = Code.StackSpace, Operand = 2 },
                    new Instruction {Code = Code.Ldarg, Operand = 0 },//加载a
                    new Instruction {Code = Code.Ldarg, Operand = 1 },//加载b
                    new Instruction {Code = Code.Add },//调用指令add,a+b
                    new Instruction {Code = Code.Ret , Operand = 1},
                },
                new Instruction[] // void test()
                {
                    new Instruction {Code = Code.StackSpace, Operand = (1 << 16) | 2}, // local | maxstack
                    //TODO: local init
                    new Instruction {Code = Code.Ldc_I4, Operand = 0 }, //1 把int值0推到堆栈上
                    new Instruction {Code = Code.Stloc, Operand = 0},   //2 从计算堆栈的顶部弹出当前值并将其存储到0索引处的局部变量列表中
                    new Instruction {Code = Code.Br, Operand =  9}, // 3 跳转+9

                    new Instruction {Code = Code.Ldc_I4, Operand = 1 }, //4 把int值1推入堆栈
                    new Instruction {Code = Code.Ldc_I4, Operand = 2 }, //5 把int值2推入堆栈
                    new Instruction {Code = Code.Call, Operand = (2 << 16) | 0}, //6 调用add,也就是调用方法0
                    new Instruction {Code = Code.Pop }, //7 计算结果弹出

                    new Instruction {Code = Code.Ldloc, Operand = 0 }, //8 把局部变量索引0推入堆栈
                    new Instruction {Code = Code.Ldc_I4, Operand = 1 }, //9 把1推入堆栈
                    new Instruction {Code = Code.Add }, //10 相加,相当于步长加1
                    new Instruction {Code = Code.Stloc, Operand = 0 }, //11 把结果存入局部变量索引0

                    new Instruction {Code = Code.Ldloc, Operand = 0 }, // 12 把局部变量索引0推入堆栈
                    new Instruction {Code = Code.Ldc_I4, Operand =  loopCount}, // 13 把loopCount推入堆栈
                    new Instruction {Code = Code.Blt, Operand = -10 }, //14 如果局部变量索引0小于loopCount,则跳转-10

                    new Instruction {Code = Code.Ret, Operand = 0 } 
                }
            };

 这个是生成补丁的函数,会有一个唯一id,对应Instruction[][] methods = new Instruction[][]里面的一条条指令,是二维数组,id就是Array对应的索引

List<IntPtr> nativePointers = new List<IntPtr>();

IntPtr nativePointer = System.Runtime.InteropServices.Marshal.AllocHGlobal(
    sizeof(Instruction*) * methods.Length);
Instruction** unmanagedCodes = (Instruction**)nativePointer.ToPointer();
nativePointers.Add(nativePointer);

for (int i = 0; i < methods.Length; i++)
{
    nativePointer = System.Runtime.InteropServices.Marshal.AllocHGlobal(
        sizeof(Instruction) * methods[i].Length);
    unmanagedCodes[i] = (Instruction*)nativePointer.ToPointer();
    for (int j = 0; j < methods[i].Length; j++)
    {
        unmanagedCodes[i][j] = methods[i][j];
    }
    nativePointers.Add(nativePointer);
}

把上面的指令转成非托管代码,这样就可以获得指针,就可以实现指令跳转

virtualMachine.Execute(1, ref call, 0);运行Test()

public void Execute(int methodIndex, ref Call call, int argsCount, int refCount = 0)
        {
            Execute(unmanagedCodes[methodIndex], call.argumentBase + refCount, call.managedStack,
                call.evaluationStackBase, argsCount, methodIndex, refCount, call.topWriteBack);
        }
unmanagedCodes[methodIndex]就获取到test指针入口Instruction* pc,然后pc++;一步一步模拟调用函数了
       //injectfix自己模拟的堆栈
      public ThreadStackInfo() 
        {
            //index = idx;
            evaluationStackHandler = Marshal.AllocHGlobal(sizeof(Value) * VirtualMachine.MAX_EVALUATION_STACK_SIZE);
            unmanagedStackHandler = Marshal.AllocHGlobal(sizeof(UnmanagedStack));

            UnmanagedStack = (UnmanagedStack*)unmanagedStackHandler.ToPointer();
            UnmanagedStack->Base = UnmanagedStack->Top = (Value*)evaluationStackHandler.ToPointer();
            ManagedStack = new object[VirtualMachine.MAX_EVALUATION_STACK_SIZE];
        }

 堆栈大小1024 * 10,超过就会有问题

public static Call Begin()
{
    var stack = ThreadStackInfo.Stack;
    return new Call()
    {
        managedStack = stack.ManagedStack,
        currentTop = stack.UnmanagedStack->Top,
        argumentBase = stack.UnmanagedStack->Top,
        evaluationStackBase = stack.UnmanagedStack->Base,
        topWriteBack = &(stack.UnmanagedStack->Top)
    };
}

Call主要就是操作堆栈,push,pop ,get 

public Value* Execute(Instruction* pc, Value* argumentBase, object[] managedStack,
    Value* evaluationStackBase, int argsCount, int methodIndex,
    int refCount = 0, Value** topWriteBack = null)
{
    if (pc->Code != Code.StackSpace) //TODO:删了pc会慢,但手机可能会快
    {
        throwRuntimeException(new InvalidProgramException("invalid code!"), topWriteBack == null);
    }

    int leavePoint = 0; //由于首指令是插入的StackSpace,所以leavePoint不可能等于0
    Exception throwExcepton = null; //use by rethrow
    Instruction* pcb = pc;

    int localsCount = (pc->Operand >> 16);
    int maxStack = (pc->Operand & 0xFFFF);

    int argumentPos = (int)(argumentBase - evaluationStackBase);
    if (argumentPos + argsCount + localsCount + maxStack > MAX_EVALUATION_STACK_SIZE)
    {
        throwRuntimeException(new StackOverflowException(), topWriteBack == null);
    }

    Value* localBase = argumentBase + argsCount;
    Value* evaluationStackPointer = localBase + localsCount;

    pc++;

    while (true) //TODO: 常用指令应该放前面
    {
        try
        {
            var code = pc->Code;
            switch (code)
            {
                case Code.Ldarg:
                    {
                        copy(evaluationStackBase, evaluationStackPointer, argumentBase + pc->Operand,
                            managedStack);
                        evaluationStackPointer++;
                    }
                    break;

argumentBase入口指针位置

Value* localBase = argumentBase + argsCount 就是局部变量的起始位置,如果没有传入函数参数,就是0

evaluationStackBase当前堆栈指针位置 localBase + localsCount,也就是栈顶

函数参数,局部变量都有了,就开始调用指令了,操作指令

case Code.Ldarg:
{
    copy(evaluationStackBase, evaluationStackPointer, argumentBase + pc->Operand,
        managedStack);
    evaluationStackPointer++;
}
break;

Code.Ldarg指令就是把argumentBase + pc->Operand指针位置的值,压入栈顶

 public enum Code
    {
        Nop,
        Break,
        Ldarg,
        Ldloc,
        Stloc,
        Ldarga,
        Starg,
        Ldloca,
        Ldnull,
        Ldc_I4,
        Ldc_I8,
        Ldc_R4,
        Ldc_R8,
        Dup,
        Pop,
        Jmp,
        Call,
        CallExtern,
        //Calli,
        Ret,
        ......
}

有兴趣的可以详细看看每个指令如何操作

现在知道injfectfix如何调用补丁,调用非补丁函数怎么调用呢

 
case Code.CallExtern://部分来自Call部分来自Callvirt
    int methodId = pc->Operand & 0xFFFF;
    int paramCount = pc->Operand >> 16;
    var externInvokeFunc = externInvokers[methodId];
    if (externInvokeFunc == null)
    {
        externInvokers[methodId] = externInvokeFunc
            = (new ReflectionMethodInvoker(externMethods[methodId])).Invoke;
    };
    var top = evaluationStackPointer - paramCount;
    Call call = new Call()
    {
        argumentBase = top,
        currentTop = top,
        managedStack = managedStack,
        evaluationStackBase = evaluationStackBase
    };
    //调用外部前,需要保存当前top,以免外部从新进入内部时覆盖栈
    ThreadStackInfo.Stack.UnmanagedStack->Top = evaluationStackPointer;
    externInvokeFunc(this, ref call, code == Code.Newobj);
    evaluationStackPointer = call.currentTop;
    break;

externMethods[methodId]注意看这个就是获得函数体,在打补丁的时候,会记录所有需要被调用的原生函数,通过反射Type.GetType(assemblyQualifiedName)获得

 
public enum ValueType
    {
        Integer,
        Long,
        Float,
        Double,
        StackReference,//Value = pointer, 
        StaticFieldReference,
        FieldReference,//Value1 = objIdx, Value2 = fieldIdx
        ChainFieldReference,
        Object,        //Value1 = objIdx
        ValueType,     //Value1 = objIdx
        ArrayReference,//Value1 = objIdx, Value2 = elemIdx
    }

    [System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
    public struct Value
    {
        public ValueType Type;
        public int Value1;
        public int Value2;
    }

堆栈里面都是Value类型,可以模拟所有C#有的类型

总结:

1.首先通过标签获取需要fix的函数,字段,包括新增,写入配置表,注入dll,生成fix bytes

2.根据配置,初始化injfectfix自己虚拟机,申请非托管内存模拟堆栈操作

3.代码里面有大量的装箱拆箱操作,再加上是模拟指令,性能比较差!不建议频繁使用的地方打补丁

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值