mirror of
https://github.com/AssetRipper/Cpp2IL.git
synced 2026-11-13 20:05:45 -10:00
403 lines
16 KiB
C#
403 lines
16 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Cpp2IL.Core.Graphs;
|
|
using Cpp2IL.Core.ISIL;
|
|
using Cpp2IL.Core.Model.Contexts;
|
|
using Cpp2IL.Core.Utils;
|
|
|
|
namespace Cpp2IL.Core.Analysis;
|
|
|
|
// Turns the raw Il2CppArray layout (header, then length, then inline elements) back into array ops
|
|
public static class ArrayRecovery
|
|
{
|
|
private static readonly HashSet<string> ArrayNewFunctions =
|
|
[
|
|
"SzArrayNew",
|
|
"il2cpp_vm_array_new_specific",
|
|
"il2cpp_array_new_specific",
|
|
];
|
|
|
|
// Il2CppArray is {Il2CppObject obj; void* bounds; il2cpp_array_size_t max_length;} then the elements, on all versions(?)
|
|
private static long LengthOffset(int pointerSize) => 3L * pointerSize;
|
|
private static long ElementsOffset(int pointerSize) => 4L * pointerSize;
|
|
|
|
public static void Run(MethodAnalysisContext method)
|
|
{
|
|
RecoverAccesses(method);
|
|
RecoverStructElementAddresses(method);
|
|
GroupInitialisers(method.ControlFlowGraph!);
|
|
}
|
|
|
|
private static void RecoverAccesses(MethodAnalysisContext method)
|
|
{
|
|
var pointerSize = method.AppContext.Binary.PointerSizeBytes;
|
|
|
|
foreach (var instruction in method.ControlFlowGraph!.Instructions)
|
|
{
|
|
RecoverAllocation(instruction);
|
|
|
|
for (var i = 0; i < instruction.Operands.Count; i++)
|
|
{
|
|
if (instruction.Operands[i] is not MemoryOperand memory
|
|
|| memory.Base is not LocalVariable { Type: SzArrayTypeAnalysisContext arrayType } array)
|
|
continue;
|
|
|
|
if (memory.Index == null && memory.Scale == 0 && memory.Addend == LengthOffset(pointerSize))
|
|
{
|
|
instruction.SetOperand(i, new ArrayLength(array));
|
|
continue;
|
|
}
|
|
|
|
if (ElementIndex(memory, arrayType, pointerSize) is { } index)
|
|
instruction.SetOperand(i, new ArrayAccess(array, index));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Group initializers after an array allocation together so ILSpy decompiles them better
|
|
private static void GroupInitialisers(ISILControlFlowGraph cfg)
|
|
{
|
|
var movedAny = false;
|
|
|
|
foreach (var block in cfg.Blocks.ToList())
|
|
{
|
|
foreach (var allocation in block.Instructions.ToList())
|
|
{
|
|
if (allocation.OpCode != OpCode.NewArr || allocation.Operands[0] is not LocalVariable array)
|
|
continue;
|
|
|
|
var stores = new List<(Block Block, Instruction Instruction)>();
|
|
var current = block;
|
|
var index = current.Instructions.IndexOf(allocation) + 1;
|
|
|
|
while (true)
|
|
{
|
|
if (index >= current.Instructions.Count)
|
|
{
|
|
// only a straight-line run can be regrouped without changing what runs when
|
|
if (current.Successors.Count != 1 || current.Successors[0].Predecessors.Count != 1)
|
|
break;
|
|
|
|
current = current.Successors[0];
|
|
index = 0;
|
|
continue;
|
|
}
|
|
|
|
var instruction = current.Instructions[index];
|
|
|
|
if (IsElementStore(instruction, array))
|
|
{
|
|
stores.Add((current, instruction));
|
|
index++;
|
|
continue;
|
|
}
|
|
|
|
if (!ReadsArray(instruction, array))
|
|
{
|
|
index++;
|
|
continue;
|
|
}
|
|
|
|
// Found the first read. Move the allocation and its stores immediately in front, so the whole array is built in one chain with the elements already computed.
|
|
if (stores.Count > 1)
|
|
{
|
|
foreach (var (storeBlock, store) in stores)
|
|
storeBlock.Instructions.Remove(store);
|
|
|
|
block.Instructions.Remove(allocation);
|
|
|
|
var moved = new List<Instruction> { allocation };
|
|
moved.AddRange(stores.Select(s => s.Instruction));
|
|
|
|
current.Instructions.InsertRange(current.Instructions.IndexOf(instruction), moved);
|
|
movedAny = true;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Emptying a block out entirely leaves branches pointing at nothing to jump to
|
|
if (movedAny)
|
|
cfg.RemoveEmptyBlocks();
|
|
}
|
|
|
|
private static bool IsElementStore(Instruction instruction, LocalVariable array) =>
|
|
instruction.OpCode == OpCode.Move && instruction.Operands[0] is ArrayAccess { Index: Immediate } stored
|
|
&& ReferenceEquals(stored.Array, array)
|
|
&& !ReadsArray(instruction, array);
|
|
|
|
private static bool ReadsArray(Instruction instruction, LocalVariable array)
|
|
{
|
|
for (var i = 0; i < instruction.Operands.Count; i++)
|
|
{
|
|
if (i == 0 && instruction.OpCode == OpCode.Move)
|
|
continue;
|
|
|
|
var reads = instruction.Operands[i] switch
|
|
{
|
|
LocalVariable local => ReferenceEquals(local, array),
|
|
ArrayAccess access => ReferenceEquals(access.Array, array),
|
|
ArrayLength length => ReferenceEquals(length.Array, array),
|
|
MemoryOperand memory => ReferenceEquals(memory.Base, array) || ReferenceEquals(memory.Index, array),
|
|
AddressOf { Target: LocalVariable addressed } => ReferenceEquals(addressed, array),
|
|
_ => false
|
|
};
|
|
|
|
if (reads)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static void RecoverAllocation(Instruction instruction)
|
|
{
|
|
// Call "SzArrayNew", result, typeof(T[]), length, ...
|
|
if (!instruction.IsCall || instruction.Operands is not [StringLiteral { Value: var name }, LocalVariable result, TypeAnalysisContext type, { } length, ..]
|
|
|| !ArrayNewFunctions.Contains(name))
|
|
return;
|
|
|
|
instruction.OpCode = OpCode.NewArr;
|
|
instruction.SetOperands(result, type, length);
|
|
|
|
if (result.Type is not SzArrayTypeAnalysisContext)
|
|
result.Type = type;
|
|
}
|
|
|
|
private static IOperand? ElementIndex(MemoryOperand memory, SzArrayTypeAnalysisContext arrayType, int pointerSize)
|
|
{
|
|
var elementSize = ElementSize(arrayType.ElementType, pointerSize);
|
|
var offset = memory.Addend - ElementsOffset(pointerSize);
|
|
|
|
if (offset < 0 || elementSize == 0 || offset % elementSize != 0)
|
|
return null;
|
|
|
|
if (memory.Index == null)
|
|
return memory.Scale == 0 ? new Immediate(offset / elementSize) : null;
|
|
|
|
return memory.Scale == elementSize && offset == 0 ? memory.Index : null;
|
|
}
|
|
|
|
private static long ElementSize(TypeAnalysisContext elementType, int pointerSize)
|
|
{
|
|
if (!elementType.IsValueType)
|
|
return pointerSize;
|
|
|
|
return elementType.FullName switch
|
|
{
|
|
"System.Boolean" or "System.Byte" or "System.SByte" => 1,
|
|
"System.Int16" or "System.UInt16" or "System.Char" => 2,
|
|
"System.Int32" or "System.UInt32" or "System.Single" => 4,
|
|
"System.Int64" or "System.UInt64" or "System.Double" => 8,
|
|
"System.IntPtr" or "System.UIntPtr" => pointerSize,
|
|
_ => 0 // struct arrays are handled by the element-address path, which sizes them from metadata
|
|
};
|
|
}
|
|
|
|
// Recovers &array[i] over struct arrays. Struct elements are never loaded outright, the compiler
|
|
// computes their address via lea chains and calls through it. We solve the index as a linear function
|
|
// of a local and demand an exact hit on the metadata stride, so a real load can't match by accident.
|
|
private static void RecoverStructElementAddresses(MethodAnalysisContext method)
|
|
{
|
|
var pointerSize = method.AppContext.Binary.PointerSizeBytes;
|
|
var cfg = method.ControlFlowGraph!;
|
|
|
|
var definitions = SingleDefinitions(cfg);
|
|
var uses = CollectUses(cfg);
|
|
|
|
foreach (var instruction in cfg.Instructions)
|
|
{
|
|
if (instruction.IsCall)
|
|
{
|
|
for (var i = 1; i < instruction.Operands.Count; i++)
|
|
{
|
|
if (MatchElementAddress(instruction.Operands[i], pointerSize, definitions) is { } inlined)
|
|
instruction.SetOperand(i, inlined);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
// a Move from memory could be a load or a lifted lea, and only the uses tell them apart
|
|
if (instruction is not { OpCode: OpCode.Move, Operands: [LocalVariable destination, MemoryOperand] })
|
|
continue;
|
|
|
|
if (definitions.TryGetValue(destination, out var single) && single == null)
|
|
continue;
|
|
|
|
if (!uses.TryGetValue(destination, out var destinationUses) || destinationUses.Count == 0
|
|
|| !destinationUses.All(u => u.Instruction.IsCall || IsMemoryBase(u.Instruction.Operands[u.OperandIndex], destination)))
|
|
continue;
|
|
|
|
if (MatchElementAddress(instruction.Operands[1], pointerSize, definitions) is { } address)
|
|
instruction.SetOperand(1, address);
|
|
}
|
|
}
|
|
|
|
private static AddressOf? MatchElementAddress(IOperand operand, int pointerSize, Dictionary<LocalVariable, Instruction?> definitions)
|
|
{
|
|
if (operand is not MemoryOperand memory
|
|
|| memory.Base is not LocalVariable { Type: SzArrayTypeAnalysisContext arrayType } array)
|
|
return null;
|
|
|
|
var elementType = arrayType.ElementType;
|
|
if (!elementType.IsValueType || ElementSize(elementType, pointerSize) != 0)
|
|
return null;
|
|
|
|
var elementSize = MetadataElementSize(elementType, pointerSize);
|
|
if (elementSize <= 0)
|
|
return null;
|
|
|
|
return StructElementIndex(memory, array, elementSize, pointerSize, definitions) is { } index
|
|
? new AddressOf(new ArrayAccess(array, index))
|
|
: null;
|
|
}
|
|
|
|
private static bool IsMemoryBase(IOperand operand, LocalVariable local)
|
|
=> operand is MemoryOperand { Base: LocalVariable baseLocal } && ReferenceEquals(baseLocal, local);
|
|
|
|
private static long MetadataElementSize(TypeAnalysisContext elementType, int pointerSize)
|
|
=> TypeSizes.UnboxedSize(elementType, pointerSize);
|
|
|
|
private static IOperand? StructElementIndex(MemoryOperand memory, LocalVariable array, long elementSize, int pointerSize,
|
|
Dictionary<LocalVariable, Instruction?> definitions)
|
|
{
|
|
var indexAffine = memory.Index is LocalVariable indexLocal
|
|
? ScaleBy(Evaluate(indexLocal, definitions, 0), Math.Max(memory.Scale, 1))
|
|
: new Affine(null, 0, 0);
|
|
|
|
if (Sum(indexAffine, new Affine(null, 0, memory.Addend)) is not { } address)
|
|
return null;
|
|
|
|
if (ReferenceEquals(address.Root, array))
|
|
return null;
|
|
|
|
var offset = address.Offset - ElementsOffset(pointerSize);
|
|
|
|
if (address.Root != null)
|
|
return address.Multiplier == elementSize && offset == 0 ? address.Root : null;
|
|
|
|
return offset >= 0 && offset % elementSize == 0 ? new Immediate(offset / elementSize) : null;
|
|
}
|
|
|
|
// value = Multiplier * Root + Offset (a null Root means it's just a constant)
|
|
private readonly record struct Affine(LocalVariable? Root, long Multiplier, long Offset);
|
|
|
|
private static Affine? Evaluate(IOperand operand, Dictionary<LocalVariable, Instruction?> definitions, int depth)
|
|
{
|
|
if (depth > 8)
|
|
return null;
|
|
|
|
switch (operand)
|
|
{
|
|
case Immediate { Value: var value }:
|
|
return new Affine(null, 0, value);
|
|
|
|
case LocalVariable local:
|
|
{
|
|
if (!definitions.TryGetValue(local, out var definition) || definition == null)
|
|
return new Affine(local, 1, 0);
|
|
|
|
return definition switch
|
|
{
|
|
{ OpCode: OpCode.Move, Operands: [_, MemoryOperand lea] } => EvaluateLea(lea, definitions, depth + 1),
|
|
{ OpCode: OpCode.Move, Operands: [_, var source] } => Evaluate(source, definitions, depth + 1),
|
|
{ OpCode: OpCode.Add, Operands: [_, var left, var right] } => Sum(Evaluate(left, definitions, depth + 1), Evaluate(right, definitions, depth + 1)),
|
|
{ OpCode: OpCode.ShiftLeft, Operands: [_, var left, Immediate { Value: >= 0 and < 32 } shift] } => ScaleBy(Evaluate(left, definitions, depth + 1), 1L << (int)shift.Value),
|
|
{ OpCode: OpCode.Multiply, Operands: [_, var left, Immediate factor] } => ScaleBy(Evaluate(left, definitions, depth + 1), factor.Value),
|
|
_ => new Affine(local, 1, 0)
|
|
};
|
|
}
|
|
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static Affine? EvaluateLea(MemoryOperand lea, Dictionary<LocalVariable, Instruction?> definitions, int depth)
|
|
{
|
|
var result = (Affine?)new Affine(null, 0, lea.Addend);
|
|
|
|
if (lea.Base != null)
|
|
result = Sum(result, Evaluate(lea.Base, definitions, depth));
|
|
|
|
if (lea.Index != null)
|
|
result = Sum(result, ScaleBy(Evaluate(lea.Index, definitions, depth), Math.Max(lea.Scale, 1)));
|
|
|
|
return result;
|
|
}
|
|
|
|
private static Affine? Sum(Affine? left, Affine? right)
|
|
{
|
|
if (left is not { } l || right is not { } r)
|
|
return null;
|
|
|
|
if (l.Root != null && r.Root != null && !ReferenceEquals(l.Root, r.Root))
|
|
return null;
|
|
|
|
return new Affine(l.Root ?? r.Root, l.Multiplier + r.Multiplier, l.Offset + r.Offset);
|
|
}
|
|
|
|
private static Affine? ScaleBy(Affine? value, long factor)
|
|
=> value is { } affine ? new Affine(affine.Root, affine.Multiplier * factor, affine.Offset * factor) : null;
|
|
|
|
// null means the local has more than one definition
|
|
private static Dictionary<LocalVariable, Instruction?> SingleDefinitions(ISILControlFlowGraph cfg)
|
|
{
|
|
var definitions = new Dictionary<LocalVariable, Instruction?>();
|
|
|
|
foreach (var instruction in cfg.Instructions)
|
|
if (instruction.Destination is LocalVariable destination)
|
|
definitions[destination] = definitions.ContainsKey(destination) ? null : instruction;
|
|
|
|
return definitions;
|
|
}
|
|
|
|
private static Dictionary<LocalVariable, List<(Instruction Instruction, int OperandIndex)>> CollectUses(ISILControlFlowGraph cfg)
|
|
{
|
|
var uses = new Dictionary<LocalVariable, List<(Instruction, int)>>();
|
|
|
|
foreach (var instruction in cfg.Instructions)
|
|
{
|
|
for (var i = 0; i < instruction.Operands.Count; i++)
|
|
{
|
|
var operand = instruction.Operands[i];
|
|
|
|
if (ReferenceEquals(operand, instruction.Destination))
|
|
continue;
|
|
|
|
foreach (var local in OperandLocals(operand))
|
|
{
|
|
if (!uses.TryGetValue(local, out var sites))
|
|
uses[local] = sites = [];
|
|
sites.Add((instruction, i));
|
|
}
|
|
}
|
|
}
|
|
|
|
return uses;
|
|
}
|
|
|
|
private static IEnumerable<LocalVariable> OperandLocals(IOperand operand)
|
|
{
|
|
switch (operand)
|
|
{
|
|
case LocalVariable direct:
|
|
yield return direct;
|
|
break;
|
|
case MemoryOperand memory:
|
|
if (memory.Base is LocalVariable baseLocal)
|
|
yield return baseLocal;
|
|
if (memory.Index is LocalVariable indexLocal)
|
|
yield return indexLocal;
|
|
break;
|
|
case AddressOf { Target: LocalVariable addressed }:
|
|
yield return addressed;
|
|
break;
|
|
}
|
|
}
|
|
}
|