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);
}
}