在看这篇文章之前必须对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.代码里面有大量的装箱拆箱操作,再加上是模拟指令,性能比较差!不建议频繁使用的地方打补丁