mirror of
https://github.com/AssetRipper/Cpp2IL.git
synced 2026-11-13 18:07:02 -10:00
727 lines
30 KiB
C#
727 lines
30 KiB
C#
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Cpp2IL.Core.ISIL;
|
|
using Cpp2IL.Core.Model.Contexts;
|
|
using Cpp2IL.Core.Utils;
|
|
|
|
namespace Cpp2IL.Core.Analysis;
|
|
|
|
public static class LocalVariables
|
|
{
|
|
public static int MaxTypePropagationLoopCount = 5000;
|
|
|
|
private const long StaticFieldsOffset64 = 0xB8;
|
|
private const long StaticFieldsOffset32 = 0x5C;
|
|
|
|
public static void CreateAll(MethodAnalysisContext method)
|
|
{
|
|
var cfg = method.ControlFlowGraph!;
|
|
var instructions = cfg.Instructions;
|
|
|
|
// Get all registers
|
|
var registers = new List<Register>();
|
|
foreach (var instruction in instructions)
|
|
registers.AddRange(GetRegisters(instruction));
|
|
|
|
// Remove duplicates
|
|
registers = registers.Distinct().ToList();
|
|
|
|
// Map those to locals
|
|
var locals = new Dictionary<Register, LocalVariable>();
|
|
for (var i = 0; i < registers.Count; i++)
|
|
{
|
|
var register = registers[i];
|
|
locals.Add(register, new LocalVariable($"v{i}", register));
|
|
}
|
|
|
|
// Replace registers with locals
|
|
foreach (var instruction in instructions)
|
|
{
|
|
for (var i = 0; i < instruction.Operands.Count; i++)
|
|
{
|
|
var operand = instruction.Operands[i];
|
|
|
|
if (operand is Register register)
|
|
instruction.SetOperand(i, locals[register]);
|
|
|
|
if (operand is AddressOf { Target: Register addressed })
|
|
instruction.SetOperand(i, new AddressOf(locals[addressed]));
|
|
|
|
if (operand is MemoryOperand memory)
|
|
{
|
|
if (memory.Base != null)
|
|
{
|
|
var baseRegister = (Register)memory.Base;
|
|
memory.Base = locals[baseRegister];
|
|
}
|
|
|
|
if (memory.Index != null)
|
|
{
|
|
var index = (Register)memory.Index;
|
|
memory.Index = locals[index];
|
|
}
|
|
|
|
instruction.SetOperand(i, memory);
|
|
}
|
|
}
|
|
}
|
|
|
|
method.Locals = locals.Select(kv => kv.Value).ToList();
|
|
|
|
// Return local names
|
|
var retValIndex = 0;
|
|
foreach (var instruction in instructions)
|
|
{
|
|
if (instruction.OpCode != OpCode.Return || instruction.Operands.Count != 1) continue;
|
|
|
|
var returnLocal = (LocalVariable)instruction.Sources[0];
|
|
|
|
returnLocal.Name = $"returnVal{retValIndex + 1}";
|
|
returnLocal.IsReturn = true;
|
|
retValIndex++;
|
|
}
|
|
|
|
// Add parameter names
|
|
var paramLocals = new List<LocalVariable>();
|
|
|
|
var operandOffset = method.IsStatic ? 0 : 1; // 'this'
|
|
|
|
// 'this' param
|
|
if (!method.IsStatic && method.Locals.Count > 0 && method.ParameterOperands.Count > 0)
|
|
{
|
|
var thisOperand = (Register)method.ParameterOperands[0];
|
|
var thisLocal = method.Locals.FirstOrDefault(l => l.Register.Number == thisOperand.Number && l.Register.Version == -1);
|
|
|
|
if (thisLocal != null)
|
|
{
|
|
thisLocal.Name = "this";
|
|
thisLocal.IsThis = true;
|
|
paramLocals.Add(thisLocal);
|
|
}
|
|
else
|
|
{
|
|
method.AddWarning($"'this' local not found (operand: {thisOperand})");
|
|
}
|
|
}
|
|
|
|
// Check if method has MethodInfo*
|
|
var hasMethodInfo = (method.ParameterOperands.Count - operandOffset) > method.Parameters.Count;
|
|
var methodInfoIndex = method.ParameterOperands.Count - 1;
|
|
|
|
// Add normal parameter names
|
|
for (var i = 0; i < method.Parameters.Count; i++)
|
|
{
|
|
var operandIndex = i + operandOffset;
|
|
if (hasMethodInfo && operandIndex == methodInfoIndex)
|
|
break; // Skip MethodInfo*
|
|
|
|
if (operandIndex >= method.ParameterOperands.Count)
|
|
break;
|
|
|
|
if (method.ParameterOperands[operandIndex] is not Register reg)
|
|
continue;
|
|
|
|
var local = method.Locals.FirstOrDefault(l => l.Register.Number == reg.Number && l.Register.Version == -1);
|
|
if (local == null)
|
|
continue;
|
|
|
|
local.Name = method.Parameters[i].ParameterName;
|
|
paramLocals.Add(local);
|
|
}
|
|
|
|
// Add MethodInfo*
|
|
if (hasMethodInfo)
|
|
{
|
|
var methodInfoOperand = (Register)method.ParameterOperands[methodInfoIndex];
|
|
var methodInfoLocal = method.Locals.FirstOrDefault(l => l.Register.Number == methodInfoOperand.Number && l.Register.Version == -1);
|
|
|
|
if (methodInfoLocal != null)
|
|
{
|
|
methodInfoLocal.Name = "methodInfo";
|
|
methodInfoLocal.IsMethodInfo = true;
|
|
paramLocals.Add(methodInfoLocal);
|
|
}
|
|
}
|
|
|
|
method.ParameterLocals = paramLocals;
|
|
|
|
// the hidden return buffer takes the first argument register. we type it as the return
|
|
// type so stores into it resolve to fields
|
|
if (method.AppContext.Binary.PointerSizeBytes == 8
|
|
&& method.AppContext.InstructionSet.CallingConventionResolver?.HiddenReturnBufferRegister(method) is { } bufferRegister
|
|
&& method.Locals.FirstOrDefault(l => l.Register.Number == bufferRegister.Number && l.Register.Version == -1) is { } bufferLocal)
|
|
{
|
|
bufferLocal.Name = "returnBuffer";
|
|
bufferLocal.Type = method.ReturnType;
|
|
}
|
|
}
|
|
|
|
public static void RemoveUnused(MethodAnalysisContext method)
|
|
{
|
|
var cfg = method.ControlFlowGraph!;
|
|
cfg.BuildUseDefLists();
|
|
|
|
var usedLocals = new HashSet<LocalVariable>();
|
|
|
|
foreach (var block in cfg.Blocks)
|
|
{
|
|
foreach (var usedVar in block.Use.OfType<LocalVariable>())
|
|
usedLocals.Add(usedVar);
|
|
|
|
foreach (var definedVar in block.Def.OfType<LocalVariable>())
|
|
usedLocals.Add(definedVar);
|
|
}
|
|
|
|
method.Locals.RemoveAll(x => !usedLocals.Contains(x));
|
|
}
|
|
|
|
private static List<Register> GetRegisters(Instruction instruction)
|
|
{
|
|
var registers = new List<Register>();
|
|
|
|
foreach (var operand in instruction.Operands)
|
|
{
|
|
if (operand is AddressOf { Target: Register addressed })
|
|
{
|
|
if (!registers.Contains(addressed))
|
|
registers.Add(addressed);
|
|
}
|
|
|
|
if (operand is Register register)
|
|
{
|
|
if (!registers.Contains(register))
|
|
registers.Add(register);
|
|
}
|
|
|
|
if (operand is MemoryOperand memory)
|
|
{
|
|
if (memory.Base != null)
|
|
{
|
|
var baseRegister = (Register)memory.Base;
|
|
if (!registers.Contains(baseRegister))
|
|
registers.Add(baseRegister);
|
|
}
|
|
|
|
if (memory.Index != null)
|
|
{
|
|
var index = (Register)memory.Index;
|
|
if (!registers.Contains(index))
|
|
registers.Add(index);
|
|
}
|
|
}
|
|
}
|
|
|
|
return registers;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves field accesses and propagates types together, to a fixpoint, while the method is
|
|
/// still in SSA form (every local has a single, version-stable definition).
|
|
///
|
|
/// The two are mutually enabling and so cannot be ordered as separate passes: a typed base lets
|
|
/// <see cref="MetadataResolver.ResolveFieldOffsets"/> turn <c>[base + offset]</c> into a
|
|
/// <see cref="FieldReference"/>, a resolved field load types its result with the field's type,
|
|
/// and that result is in turn the base of the next access (directly, or after flowing through
|
|
/// moves/phis). Both steps are monotonic - each only ever resolves an operand or fills a
|
|
/// previously-unknown type - so the loop converges.
|
|
/// </summary>
|
|
public static void ResolveTypesAndFields(MethodAnalysisContext method)
|
|
{
|
|
// Seed types from fixed ground truth - the method's own signature, and type-metadata global
|
|
// loads. Applied once up front and, being applied first, they win over anything inferred later.
|
|
PropagateFromReturn(method);
|
|
PropagateFromParameters(method);
|
|
SeedRuntimeClassTypes(method);
|
|
SeedNewobjResults(method);
|
|
SeedMethodInfoTypes(method);
|
|
SeedComparisonResults(method);
|
|
SeedFloatLiterals(method);
|
|
|
|
// Everywhere there's a CallVoid after a Newobj, we can resolve the constructor call.
|
|
MetadataResolver.ResolveConstructorCalls(method);
|
|
|
|
// Everything else is mutually enabling and so runs to a fixpoint: a typed receiver lets an
|
|
// ambiguous call resolve, a resolved call types its return value and arguments, a typed base
|
|
// lets a field offset resolve, a field load types its result, and any of those can be the
|
|
// receiver/base of the next step. Every pass is monotonic - it only resolves an operand or
|
|
// fills a previously-unknown type - so the loop converges.
|
|
var changed = true;
|
|
var loopCount = 0;
|
|
|
|
while (changed)
|
|
{
|
|
if (MaxTypePropagationLoopCount != -1 && ++loopCount > MaxTypePropagationLoopCount)
|
|
throw new DecompilerException($"Type and field resolution not settling! (looped {MaxTypePropagationLoopCount} times)");
|
|
|
|
changed = false;
|
|
changed |= MetadataResolver.ResolveCallsViaMethodInfo(method);
|
|
changed |= MetadataResolver.ResolveAmbiguousCalls(method);
|
|
changed |= MetadataResolver.ResolveVirtualCalls(method);
|
|
changed |= PropagateFromCallParameters(method);
|
|
changed |= MetadataResolver.ResolveFieldOffsets(method);
|
|
changed |= RgctxResolver.Run(method);
|
|
changed |= PropagateStaticFieldStorage(method);
|
|
changed |= TypeAddressedLocals(method);
|
|
changed |= PropagateTypesOnce(method);
|
|
}
|
|
}
|
|
|
|
// A type-metadata global load (Move local, typeof(T)) puts the runtime class pointer for T into
|
|
// the local - an Il2CppClass*, not an instance of T. That is known exactly from the instruction,
|
|
// so it is seeded as ground truth (overriding any prior guess) before the inference fixpoint,
|
|
// rather than letting a monotonic pass first mistype the local as T itself.
|
|
private static void SeedRuntimeClassTypes(MethodAnalysisContext method)
|
|
{
|
|
foreach (var instruction in method.ControlFlowGraph!.Instructions)
|
|
{
|
|
if (instruction.OpCode != OpCode.Move || instruction.Operands.Count < 2)
|
|
continue;
|
|
|
|
if (instruction.Operands[0] is LocalVariable destination
|
|
&& instruction.Operands[1] is TypeAnalysisContext type and not (RuntimeMethodInfoAnalysisContext or RuntimeFieldInfoAnalysisContext))
|
|
destination.Type = new RuntimeClassTypeAnalysisContext(type, type.DeclaringAssembly);
|
|
}
|
|
}
|
|
|
|
private static void SeedNewobjResults(MethodAnalysisContext method)
|
|
{
|
|
foreach (var instruction in method.ControlFlowGraph!.Instructions)
|
|
{
|
|
if (instruction.OpCode != OpCode.Newobj || instruction.Operands.Count < 2)
|
|
continue;
|
|
|
|
if (instruction.Operands[0] is LocalVariable destination && InstantiatedType(instruction.Operands[1]) is { } type)
|
|
destination.Type = type;
|
|
}
|
|
}
|
|
|
|
private static TypeAnalysisContext? InstantiatedType(IOperand classOperand) =>
|
|
classOperand switch
|
|
{
|
|
LocalVariable { Type: RuntimeClassTypeAnalysisContext { RepresentedType: var t } } => t,
|
|
RuntimeClassTypeAnalysisContext { RepresentedType: var t } => t,
|
|
TypeAnalysisContext type => type, //not sure this is actually valid but for completeness
|
|
_ => null,
|
|
};
|
|
|
|
// A method/field-metadata global load (Move local, methodof(M) / fieldof(F)) puts a MethodInfo*
|
|
// or FieldInfo* into the local. MetadataResolver already resolved the address to a context naming
|
|
// the member; that same context is the local's type (a runtime handle, recoverable via its
|
|
// RepresentedMethod/RepresentedField).
|
|
private static void SeedMethodInfoTypes(MethodAnalysisContext method)
|
|
{
|
|
foreach (var instruction in method.ControlFlowGraph!.Instructions)
|
|
{
|
|
if (instruction.OpCode != OpCode.Move || instruction.Operands.Count < 2)
|
|
continue;
|
|
|
|
if (instruction.Operands[0] is LocalVariable destination
|
|
&& instruction.Operands[1] is RuntimeMethodInfoAnalysisContext or RuntimeFieldInfoAnalysisContext)
|
|
destination.Type = (TypeAnalysisContext)instruction.Operands[1];
|
|
}
|
|
}
|
|
|
|
// A comparison (CheckEqual, CheckLess, ...) writes a 0/1 result into its destination, so that local
|
|
// is a System.Boolean regardless of what the compared operands are.
|
|
private static void SeedComparisonResults(MethodAnalysisContext method)
|
|
{
|
|
var booleanType = method.AppContext.SystemTypes.SystemBooleanType;
|
|
|
|
foreach (var instruction in method.ControlFlowGraph!.Instructions)
|
|
{
|
|
if (instruction.OpCode is < OpCode.CheckEqual or > OpCode.CheckLessOrEqual)
|
|
continue;
|
|
|
|
if (instruction.Destination is LocalVariable destination)
|
|
destination.Type = booleanType;
|
|
}
|
|
}
|
|
|
|
//Handles typing of locals for ref/out params. Returns whether anything new was typed
|
|
public static bool TypeAddressedLocals(MethodAnalysisContext method)
|
|
{
|
|
var changed = false;
|
|
|
|
foreach (var instruction in method.ControlFlowGraph!.Instructions)
|
|
{
|
|
if (!instruction.IsCall || instruction.Operands[0] is not MethodAnalysisContext calledMethod)
|
|
continue;
|
|
|
|
var firstArg = instruction.OpCode == OpCode.CallVoid ? 1 : 2;
|
|
|
|
// the receiver of a value type's instance method is a pointer to the value
|
|
if (!calledMethod.IsStatic && firstArg < instruction.Operands.Count
|
|
&& instruction.Operands[firstArg] is AddressOf { Target: LocalVariable receiver }
|
|
&& calledMethod.DeclaringType is { IsValueType: true } declaringType)
|
|
changed |= SetTypeIfUnknown(receiver, declaringType);
|
|
|
|
var paramOffset = firstArg + (calledMethod.IsStatic ? 0 : 1);
|
|
|
|
for (var i = paramOffset; i < instruction.Operands.Count; i++)
|
|
{
|
|
var parameterIndex = i - paramOffset;
|
|
if (parameterIndex > calledMethod.Parameters.Count - 1) // Probably MethodInfo*
|
|
continue;
|
|
|
|
if (instruction.Operands[i] is AddressOf { Target: LocalVariable referenced }
|
|
&& calledMethod.Parameters[parameterIndex].ParameterType is ByRefTypeAnalysisContext { ElementType: { } referencedType })
|
|
changed |= SetTypeIfUnknown(referenced, referencedType);
|
|
}
|
|
}
|
|
|
|
return changed;
|
|
}
|
|
|
|
// Fills in a local's type only when it is currently unknown, keeping propagation monotonic (a
|
|
// type, once set, is never changed) so the fixpoint terminates. Returns whether it set anything.
|
|
private static bool SetTypeIfUnknown(LocalVariable local, TypeAnalysisContext? type)
|
|
{
|
|
if (type == null || local.Type != null)
|
|
return false;
|
|
|
|
local.Type = type;
|
|
return true;
|
|
}
|
|
|
|
private static bool PropagateStaticFieldStorage(MethodAnalysisContext method)
|
|
{
|
|
var staticFieldsOffset = method.AppContext.Binary.is32Bit ? StaticFieldsOffset32 : StaticFieldsOffset64;
|
|
var changed = false;
|
|
|
|
foreach (var instruction in method.ControlFlowGraph!.Instructions)
|
|
{
|
|
if (instruction.OpCode != OpCode.Move || instruction.Operands.Count < 2)
|
|
continue;
|
|
|
|
if (instruction.Operands[0] is not LocalVariable destination || destination.Type is StaticFieldStorageTypeAnalysisContext)
|
|
continue;
|
|
|
|
if (instruction.Operands[1] is not MemoryOperand { Index: null, Scale: 0 } memory || memory.Addend != staticFieldsOffset)
|
|
continue;
|
|
|
|
if (memory.Base is not LocalVariable { Type: RuntimeClassTypeAnalysisContext { RepresentedType: var owner } })
|
|
continue;
|
|
|
|
destination.Type = new StaticFieldStorageTypeAnalysisContext(owner, owner.DeclaringAssembly);
|
|
changed = true;
|
|
}
|
|
|
|
return changed;
|
|
}
|
|
|
|
// A single propagation sweep over every move and phi. Returns whether it filled in any type.
|
|
private static bool PropagateTypesOnce(MethodAnalysisContext method)
|
|
{
|
|
var changed = false;
|
|
|
|
foreach (var instruction in method.ControlFlowGraph!.Instructions)
|
|
{
|
|
switch (instruction.OpCode)
|
|
{
|
|
case OpCode.Move:
|
|
changed |= PropagateMove(instruction, method.AppContext.Binary.PointerSizeBytes);
|
|
break;
|
|
case OpCode.Phi:
|
|
changed |= PropagatePhi(instruction);
|
|
break;
|
|
case OpCode.Add or OpCode.Subtract or OpCode.Multiply:
|
|
changed |= PropagateArithmetic(instruction, method);
|
|
break;
|
|
case OpCode.Divide or OpCode.Modulo:
|
|
changed |= PropagateArithmetic(instruction, method) || PropagateIntegerResult(instruction, method);
|
|
break;
|
|
case OpCode.And or OpCode.Or or OpCode.Xor or OpCode.Not or OpCode.Negate
|
|
or OpCode.ShiftLeft or OpCode.ShiftRight:
|
|
changed |= PropagateIntegerResult(instruction, method);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return changed;
|
|
}
|
|
|
|
// A local assigned a float/double literal (a lifted rodata constant load) is that float type
|
|
private static void SeedFloatLiterals(MethodAnalysisContext method)
|
|
{
|
|
foreach (var instruction in method.ControlFlowGraph!.Instructions)
|
|
{
|
|
if (instruction.OpCode != OpCode.Move || instruction.Operands[0] is not LocalVariable destination)
|
|
continue;
|
|
|
|
destination.Type = instruction.Operands[1] switch
|
|
{
|
|
FloatLiteral => method.AppContext.SystemTypes.SystemSingleType,
|
|
DoubleLiteral => method.AppContext.SystemTypes.SystemDoubleType,
|
|
_ => destination.Type,
|
|
};
|
|
}
|
|
}
|
|
|
|
// Arithmetic on a float operand is float arithmetic, so the result is that float type.
|
|
private static bool PropagateArithmetic(Instruction instruction, MethodAnalysisContext method)
|
|
{
|
|
if (instruction.Operands is not [LocalVariable { Type: null } destination, var left, var right])
|
|
return false;
|
|
|
|
if ((FloatOperandType(left, method) ?? FloatOperandType(right, method)) is not { } floatType)
|
|
return false;
|
|
|
|
return SetTypeIfUnknown(destination, floatType);
|
|
}
|
|
|
|
// An integer operand makes the result an integer. Excludes bool operands so flag logic stays boolean.
|
|
private static bool PropagateIntegerResult(Instruction instruction, MethodAnalysisContext method)
|
|
{
|
|
if (instruction.Operands[0] is not LocalVariable { Type: null } destination)
|
|
return false;
|
|
|
|
for (var i = 1; i < instruction.Operands.Count; i++)
|
|
if (IntegerResultType(instruction.Operands[i], method) is { } integerType)
|
|
return SetTypeIfUnknown(destination, integerType);
|
|
|
|
return false;
|
|
}
|
|
|
|
private static TypeAnalysisContext? IntegerResultType(IOperand operand, MethodAnalysisContext method)
|
|
{
|
|
var type = operand switch
|
|
{
|
|
LocalVariable { Type: { } localType } => localType,
|
|
FieldReference field => field.Field.FieldType,
|
|
_ => null,
|
|
};
|
|
|
|
return type?.FullName switch
|
|
{
|
|
"System.Byte" or "System.SByte" or "System.Int16" or "System.UInt16"
|
|
or "System.Int32" or "System.UInt32" or "System.Char" => method.AppContext.SystemTypes.SystemInt32Type,
|
|
"System.Int64" or "System.UInt64" => method.AppContext.SystemTypes.SystemInt64Type,
|
|
_ => null,
|
|
};
|
|
}
|
|
|
|
private static TypeAnalysisContext? FloatOperandType(IOperand operand, MethodAnalysisContext method) =>
|
|
operand switch
|
|
{
|
|
FloatLiteral => method.AppContext.SystemTypes.SystemSingleType,
|
|
DoubleLiteral => method.AppContext.SystemTypes.SystemDoubleType,
|
|
LocalVariable { Type: { FullName: "System.Single" } single } => single,
|
|
LocalVariable { Type: { FullName: "System.Double" } @double } => @double,
|
|
_ => null,
|
|
};
|
|
|
|
private static bool PropagateMove(Instruction move, int pointerSize)
|
|
{
|
|
var destination = move.Operands[0];
|
|
var source = move.Operands[1];
|
|
|
|
// Move local, local: copy a known type in whichever direction is missing it.
|
|
if (destination is LocalVariable destLocal && source is LocalVariable sourceLocal)
|
|
return SetTypeIfUnknown(destLocal, sourceLocal.Type) || SetTypeIfUnknown(sourceLocal, destLocal.Type);
|
|
|
|
// Move local, field: a field load types its result with the field's type. This is the edge
|
|
// that lets the loaded value go on to be the base of a further field access.
|
|
if (destination is LocalVariable loadDest && source is FieldReference loadField)
|
|
return SetTypeIfUnknown(loadDest, loadField.Field.FieldType);
|
|
|
|
// Move field, local: a field store types the stored value with the field's type.
|
|
if (destination is FieldReference storeField && source is LocalVariable storeSource)
|
|
return SetTypeIfUnknown(storeSource, storeField.Field.FieldType);
|
|
|
|
// An element of T[] is a T, whether we loaded it (reference arrays) or only computed its address
|
|
if (destination is LocalVariable { Type: null } elementDest
|
|
&& source is MemoryOperand { Base: LocalVariable { Type: SzArrayTypeAnalysisContext { ElementType: { } elementType } } } elementAccess
|
|
&& (elementAccess.Index != null || elementAccess.Addend >= 4L * pointerSize))
|
|
return SetTypeIfUnknown(elementDest, elementType);
|
|
|
|
// Move local, [byref]: dereferencing a managed pointer to a reference type yields that referent
|
|
// (a struct byref accesses fields directly with no deref, so this only fires for class referents).
|
|
if (destination is LocalVariable { Type: null } derefDest
|
|
&& source is MemoryOperand { Index: null, Scale: 0, Addend: 0, Base: LocalVariable { Type: ByRefTypeAnalysisContext { ElementType: { IsValueType: false } referent } } })
|
|
return SetTypeIfUnknown(derefDest, referent);
|
|
|
|
// Move local, [obj]: offset 0 of a reference-typed value is its klass pointer.
|
|
if (destination is LocalVariable { Type: null } klassDest
|
|
&& source is MemoryOperand { Index: null, Scale: 0, Addend: 0, Base: LocalVariable { Type: { } baseType } }
|
|
&& baseType is not (RuntimeClassTypeAnalysisContext or StaticFieldStorageTypeAnalysisContext or RuntimeMethodInfoAnalysisContext or RuntimeFieldInfoAnalysisContext or ByRefTypeAnalysisContext)
|
|
&& !baseType.IsValueType)
|
|
return SetTypeIfUnknown(klassDest, new RuntimeClassTypeAnalysisContext(baseType, baseType.DeclaringAssembly));
|
|
|
|
return false;
|
|
}
|
|
|
|
// A phi is a copy from each predecessor's value, so types flow both ways across it - mirroring
|
|
// the bidirectional Move copies it decays into once SSA is destroyed.
|
|
private static bool PropagatePhi(Instruction phi)
|
|
{
|
|
if (phi.Operands[0] is not LocalVariable destination)
|
|
return false;
|
|
|
|
var changed = false;
|
|
|
|
// Forward: an untyped phi result takes the type of any typed input.
|
|
if (destination.Type == null)
|
|
{
|
|
for (var i = 1; i < phi.Operands.Count; i++)
|
|
{
|
|
if (phi.Operands[i] is LocalVariable { Type: { } inputType })
|
|
{
|
|
changed = SetTypeIfUnknown(destination, inputType);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Backward: a typed phi result types each of its still-untyped inputs.
|
|
if (destination.Type != null)
|
|
{
|
|
for (var i = 1; i < phi.Operands.Count; i++)
|
|
{
|
|
if (phi.Operands[i] is LocalVariable input)
|
|
changed |= SetTypeIfUnknown(input, destination.Type);
|
|
}
|
|
}
|
|
|
|
return changed;
|
|
}
|
|
|
|
private static bool PropagateFromCallParameters(MethodAnalysisContext method)
|
|
{
|
|
var changed = false;
|
|
|
|
// A lea and the call it's passed to are still separate here. The address only gets folded into the
|
|
// call later, so an argument's address-of has to be found through the local carrying it.
|
|
var addressesOf = new Dictionary<LocalVariable, LocalVariable>();
|
|
foreach (var instruction in method.ControlFlowGraph!.Instructions)
|
|
{
|
|
if (instruction.OpCode == OpCode.Move
|
|
&& instruction.Operands[0] is LocalVariable pointer
|
|
&& instruction.Operands[1] is AddressOf { Target: LocalVariable pointee })
|
|
addressesOf[pointer] = pointee;
|
|
}
|
|
|
|
LocalVariable? Addressed(IOperand operand) => operand switch
|
|
{
|
|
AddressOf { Target: LocalVariable direct } => direct,
|
|
LocalVariable local when addressesOf.TryGetValue(local, out var indirect) => indirect,
|
|
_ => null
|
|
};
|
|
|
|
foreach (var instruction in method.ControlFlowGraph.Instructions)
|
|
{
|
|
if (!instruction.IsCall)
|
|
continue;
|
|
|
|
if (instruction.Operands[0] is not MethodAnalysisContext calledMethod)
|
|
continue;
|
|
|
|
// Return value: a constructor yields its declaring type, otherwise the declared return type.
|
|
if (instruction.Destination is LocalVariable returnValue)
|
|
{
|
|
var producedType = calledMethod.Name is ".ctor" or ".cctor" ? calledMethod.DeclaringType : calledMethod.ReturnType;
|
|
|
|
if (producedType != method.AppContext.SystemTypes.SystemVoidType)
|
|
changed |= SetTypeIfUnknown(returnValue, producedType);
|
|
}
|
|
|
|
|
|
// Call operands
|
|
// 0. Target
|
|
// 1. ReturnValue
|
|
// 2. thisParam
|
|
// ... parameters
|
|
|
|
// CallVoid operands
|
|
// 0. Target
|
|
// 1. thisParam
|
|
// ... parameters
|
|
var thisParamIndex = instruction.OpCode == OpCode.CallVoid ? 1 : 2;
|
|
|
|
// 'this' param
|
|
if (!calledMethod.IsStatic
|
|
&& instruction.Operands[thisParamIndex] is LocalVariable thisParam)
|
|
{
|
|
changed |= SetTypeIfUnknown(thisParam, calledMethod.DeclaringType);
|
|
}
|
|
|
|
// Value type instance method, first arg is address of value, but we need to type the value
|
|
if (!calledMethod.IsStatic
|
|
&& Addressed(instruction.Operands[thisParamIndex]) is { } addressedReceiver
|
|
&& calledMethod.DeclaringType is { IsValueType: true } valueType)
|
|
{
|
|
changed |= SetTypeIfUnknown(addressedReceiver, valueType);
|
|
}
|
|
|
|
// Remaining arguments map positionally onto the callee's declared parameters.
|
|
var paramOffset = calledMethod.IsStatic ? 1 : 2;
|
|
if (instruction.OpCode == OpCode.Call) // Skip the return value operand
|
|
paramOffset += 1;
|
|
|
|
for (var i = paramOffset; i < instruction.Operands.Count; i++)
|
|
{
|
|
var parameterIndex = i - paramOffset;
|
|
if (parameterIndex > calledMethod.Parameters.Count - 1) // Probably MethodInfo*
|
|
continue;
|
|
|
|
var parameterType = calledMethod.Parameters[parameterIndex].ParameterType;
|
|
|
|
if (parameterType is ByRefTypeAnalysisContext { ElementType: { } referencedType }
|
|
&& Addressed(instruction.Operands[i]) is { } referenced)
|
|
{
|
|
changed |= SetTypeIfUnknown(referenced, referencedType);
|
|
continue;
|
|
}
|
|
|
|
if (instruction.Operands[i] is LocalVariable local)
|
|
changed |= SetTypeIfUnknown(local, parameterType);
|
|
}
|
|
}
|
|
|
|
return changed;
|
|
}
|
|
|
|
private static void PropagateFromParameters(MethodAnalysisContext method)
|
|
{
|
|
// 'this'
|
|
if (!method.IsStatic)
|
|
{
|
|
var thisLocal = method.ParameterLocals.FirstOrDefault(p => p.IsThis);
|
|
if (thisLocal != null)
|
|
thisLocal.Type = method.DeclaringType is { GenericParameters.Count: > 0 } generic
|
|
? new GenericInstanceTypeAnalysisContext(generic, generic.GenericParameters)
|
|
: method.DeclaringType;
|
|
}
|
|
|
|
if (method.ParameterLocals.FirstOrDefault(p => p.IsMethodInfo) is { } methodInfoLocal && method.DeclaringType is { } owner)
|
|
methodInfoLocal.Type = new RuntimeMethodInfoAnalysisContext(method, owner.DeclaringAssembly);
|
|
|
|
if (method.Parameters.Count == 0)
|
|
return;
|
|
|
|
// Normal params
|
|
var paramIndex = 0;
|
|
foreach (var local in method.ParameterLocals)
|
|
{
|
|
if (local.IsThis || local.IsMethodInfo)
|
|
continue;
|
|
|
|
if (paramIndex >= method.Parameters.Count)
|
|
break;
|
|
|
|
local.Type = method.Parameters[paramIndex].ParameterType;
|
|
paramIndex++;
|
|
}
|
|
}
|
|
|
|
private static void PropagateFromReturn(MethodAnalysisContext method)
|
|
{
|
|
var returns = method.ControlFlowGraph!.Instructions.Where(i => i.OpCode == OpCode.Return);
|
|
|
|
foreach (var instruction in returns)
|
|
{
|
|
if (instruction.Operands.Count == 1 && instruction.Operands[0] is LocalVariable local)
|
|
local.Type = method.ReturnType;
|
|
}
|
|
}
|
|
}
|