using System; using System.Collections.Generic; using System.Linq; using Cpp2IL.Core.Il2CppApiFunctions; using Cpp2IL.Core.ISIL; using Cpp2IL.Core.Model.Contexts; namespace Cpp2IL.Core.Analysis; /// /// Maps calls to KeyFunctionAddresses to their underlying IL opcodes. E.g. il2cpp_codegen_object_new => newobj. /// Eventually will include box/unbox/throw/etc /// public static class KeyFunctionRecovery { //All of these have the same params in the same order so we treat them as equal. private static readonly HashSet ObjectNewFunctions = [ "il2cpp_object_new", "il2cpp_vm_object_new", "il2cpp_codegen_object_new", ]; //These all take the exception to throw as their only real argument. private static readonly HashSet RaiseExceptionFunctions = [ nameof(BaseKeyFunctionAddresses.il2cpp_raise_exception), nameof(BaseKeyFunctionAddresses.il2cpp_vm_exception_raise), nameof(BaseKeyFunctionAddresses.il2cpp_codegen_raise_exception), ]; //Both take the class to box as and a pointer to the value. private static readonly HashSet BoxFunctions = [ nameof(BaseKeyFunctionAddresses.il2cpp_value_box), nameof(BaseKeyFunctionAddresses.il2cpp_vm_object_box), ]; public static void Run(MethodAnalysisContext method) { foreach (var instruction in method.ControlFlowGraph!.Blocks.SelectMany(block => block.Instructions)) { if (instruction.Operands is not [StringLiteral { Value: var keyFunction }, ..]) continue; if (ObjectNewFunctions.Contains(keyFunction)) RewriteObjectNew(instruction); else if (keyFunction == nameof(BaseKeyFunctionAddresses.il2cpp_codegen_write_barrier)) RemoveWriteBarrier(instruction); else if (RaiseExceptionFunctions.Contains(keyFunction)) RewriteRaiseException(instruction); else if (BoxFunctions.Contains(keyFunction)) RewriteBox(instruction); else if (keyFunction == nameof(BaseKeyFunctionAddresses.il2cpp_vm_reflection_get_type_object)) RewriteTypeObject(instruction); else if (keyFunction == nameof(BaseKeyFunctionAddresses.InternalCalls_Resolve)) RewriteInternalCallResolve(instruction, method); } } private static void RemoveWriteBarrier(Instruction instruction) { instruction.OpCode = OpCode.Nop; instruction.SetOperands(); } private static void RewriteRaiseException(Instruction instruction) { // A void call has no return value operand, so the exception is one slot earlier var exceptionIndex = instruction.OpCode == OpCode.CallVoid ? 1 : 2; if (!instruction.IsCall || instruction.Operands.Count <= exceptionIndex) return; var exception = instruction.Operands[exceptionIndex]; instruction.OpCode = OpCode.Throw; instruction.SetOperands(exception); } private static void RewriteBox(Instruction instruction) { // function name, result, class, address of value. if (instruction.OpCode != OpCode.Call || instruction.Operands is not [_, var result, TypeAnalysisContext boxedType, var value, ..]) return; instruction.OpCode = OpCode.Box; instruction.SetOperands(result, boxedType, value); } private static void RewriteTypeObject(Instruction instruction) { if (instruction.OpCode != OpCode.Call || instruction.Operands is not [_, var result, TypeAnalysisContext type, ..]) return; instruction.OpCode = OpCode.Move; instruction.SetOperands(result, type); } private static void RewriteInternalCallResolve(Instruction instruction, MethodAnalysisContext method) { if (instruction.OpCode != OpCode.Call || instruction.Operands is not [_, var result, Immediate nameAddress, ..]) return; if (ThrowHelperRecovery.ReadCStringAtVirtualAddress(method.AppContext, nameAddress.UnsignedValue, 256) is not { } name) return; if (ResolveInternalCallName(method.AppContext, name) is not { DeclaringType.DeclaringAssembly: { } assembly } resolved) return; var pointer = new RuntimeMethodInfoAnalysisContext(resolved, assembly); instruction.OpCode = OpCode.Move; instruction.SetOperands(result, pointer); RewriteCachedPointerLoads(method, result, pointer); } private static void RewriteCachedPointerLoads(MethodAnalysisContext method, IOperand result, RuntimeMethodInfoAnalysisContext pointer) { var cache = method.ControlFlowGraph!.Instructions .Where(i => i is { OpCode: OpCode.Move, Operands: [MemoryOperand { IsConstant: true }, _] }) .Where(i => ReferenceEquals(i.Operands[1], result)) .Select(i => ((MemoryOperand)i.Operands[0]).Addend) .Distinct() .ToList(); if (cache.Count != 1) return; foreach (var instruction in method.ControlFlowGraph.Instructions) { if (instruction is { OpCode: OpCode.Move, Operands: [LocalVariable destination, MemoryOperand { IsConstant: true } load] } && load.Addend == cache[0]) instruction.SetOperands(destination, pointer); } } internal static MethodAnalysisContext? ResolveInternalCallName(ApplicationAnalysisContext appContext, string name) { var separator = name.IndexOf("::", StringComparison.Ordinal); if (separator < 0) return null; var typeName = name[..separator]; var signature = name[(separator + 2)..]; var parenthesis = signature.IndexOf('('); var methodName = parenthesis < 0 ? signature : signature[..parenthesis]; if (appContext.LibCpp2IlContext.ReflectionCache.GetTypeByFullName(typeName) is not { } typeDefinition || appContext.ResolveContextForType(typeDefinition) is not { } type) return null; var candidates = type.Methods.Where(m => m.Name == methodName).ToList(); if (candidates.Count <= 1) return candidates.FirstOrDefault(); // Overloaded, so fall back on the parameter list in the name var parameters = parenthesis < 0 ? "" : signature[(parenthesis + 1)..].TrimEnd(')'); var count = parameters.Length == 0 ? 0 : parameters.Split(',').Length; var byParameterCount = candidates.Where(m => m.Parameters.Count == count).ToList(); return byParameterCount.Count == 1 ? byParameterCount[0] : null; } private static void RewriteObjectNew(Instruction instruction) { // Needs the function name, the result, and the class argument. if (instruction.OpCode != OpCode.Call || instruction.Operands.Count < 3) return; var result = instruction.Operands[1]; var klass = instruction.Operands[2]; instruction.OpCode = OpCode.Newobj; instruction.SetOperands(result, klass); } }