mirror of
https://github.com/AssetRipper/Cpp2IL.git
synced 2026-11-13 18:07:02 -10:00
238 lines
8.9 KiB
C#
238 lines
8.9 KiB
C#
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using Cpp2IL.Core.Graphs;
|
|
using Cpp2IL.Core.ISIL;
|
|
using Cpp2IL.Core.Model.Contexts;
|
|
|
|
namespace Cpp2IL.Core.Analysis;
|
|
|
|
public class StackAnalyzer
|
|
{
|
|
[DebuggerDisplay("Size = {Size}")]
|
|
private class StackState
|
|
{
|
|
public int Size;
|
|
public StackState Copy() => new() { Size = this.Size };
|
|
}
|
|
|
|
private Dictionary<Block, StackState> _inComingState = [];
|
|
private Dictionary<Block, StackState> _outGoingState = [];
|
|
private Dictionary<Instruction, StackState> _instructionState = [];
|
|
|
|
/// <summary>
|
|
/// Max allowed count of blocks to visit (-1 for no limit).
|
|
/// </summary>
|
|
public static int MaxBlockVisitCount = 500000; //High enough to not be legitimately hit, but still give up if something loops infinitely.
|
|
|
|
public static void Analyze(MethodAnalysisContext method)
|
|
{
|
|
var analyzer = new StackAnalyzer();
|
|
|
|
var graph = method.ControlFlowGraph!;
|
|
graph.RemoveUnreachableBlocks(); // Without this indirect jumps (in try catch i think) cause some weird stuff
|
|
|
|
analyzer._inComingState = new Dictionary<Block, StackState> { { graph.EntryBlock, new StackState() } };
|
|
|
|
analyzer.TraverseGraph(graph.EntryBlock);
|
|
|
|
// The exit block has no outgoing state if it was never reached (e.g. every path loops or
|
|
// throws). That's fine - just skip the end-of-method stack balance check in that case.
|
|
if (analyzer._outGoingState.TryGetValue(graph.ExitBlock, out var outDelta) && outDelta.Size != 0)
|
|
{
|
|
var outText = outDelta.Size < 0 ? "-" + (-outDelta.Size).ToString("X") : outDelta.Size.ToString("X");
|
|
method.AddWarning($"Method ends with non empty stack ({outText}), the output could be wrong!");
|
|
}
|
|
|
|
analyzer.ResolveFrameAliases(graph);
|
|
analyzer.CorrectOffsets(graph);
|
|
ReplaceStackWithRegisters(method);
|
|
|
|
graph.RemoveNops();
|
|
graph.RemoveEmptyBlocks();
|
|
}
|
|
|
|
// consider mov [reg], [stack pointer]
|
|
// now we need to handle [reg] as if it were a stack pointer, forever.
|
|
private void ResolveFrameAliases(ISILControlFlowGraph graph)
|
|
{
|
|
var aliases = new Dictionary<string, int>();
|
|
|
|
foreach (var instruction in graph.EntryBlock.Successors.SelectMany(b => b.Instructions))
|
|
{
|
|
if (instruction is { OpCode: OpCode.Move, Operands: [Register destination, Register { Name: "rsp" }] }
|
|
&& _instructionState.TryGetValue(instruction, out var atCopy))
|
|
aliases[destination.Name] = atCopy.Size;
|
|
}
|
|
|
|
if (aliases.Count == 0)
|
|
return;
|
|
|
|
// Following a register that gets reassigned would need flow analysis, so stop trusting it entirely
|
|
foreach (var instruction in graph.Instructions)
|
|
{
|
|
if (instruction is { OpCode: OpCode.Move, Operands: [Register, Register { Name: "rsp" }] })
|
|
continue;
|
|
|
|
if (instruction.Destination is Register written)
|
|
aliases.Remove(written.Name);
|
|
}
|
|
|
|
foreach (var instruction in graph.Instructions)
|
|
{
|
|
if (!_instructionState.TryGetValue(instruction, out var state))
|
|
continue;
|
|
|
|
for (var i = 0; i < instruction.Operands.Count; i++)
|
|
{
|
|
if (instruction.Operands[i] is not MemoryOperand { Index: null, Scale: 0, Base: Register frameBase } memory)
|
|
continue;
|
|
|
|
if (!aliases.TryGetValue(frameBase.Name, out var frameOffset))
|
|
continue;
|
|
|
|
instruction.SetOperand(i, new StackOffset((int)(frameOffset + memory.Addend - state.Size)));
|
|
}
|
|
}
|
|
}
|
|
|
|
private void CorrectOffsets(ISILControlFlowGraph graph)
|
|
{
|
|
foreach (var block in graph.Blocks)
|
|
{
|
|
foreach (var instruction in block.Instructions)
|
|
{
|
|
if (instruction is { OpCode: OpCode.ShiftStack })
|
|
{
|
|
// Nop the shift stack instruction
|
|
instruction.OpCode = OpCode.Nop;
|
|
instruction.SetOperands();
|
|
continue;
|
|
}
|
|
|
|
int? state = null;
|
|
|
|
// Correct offset for stack operands.
|
|
for (var i = 0; i < instruction.Operands.Count; i++)
|
|
{
|
|
var op = instruction.Operands[i];
|
|
|
|
var slot = op switch
|
|
{
|
|
StackOffset direct => direct,
|
|
AddressOf { Target: StackOffset addressed } => addressed,
|
|
_ => (StackOffset?)null
|
|
};
|
|
|
|
if (slot is { } offset)
|
|
{
|
|
// This can only be done before modifying any of the instruction operands,
|
|
// as doing so will make the dictionary lookup impossible.
|
|
state ??= _instructionState[instruction].Size;
|
|
|
|
var actual = new StackOffset(state.Value + offset.Offset);
|
|
instruction.SetOperand(i, op is AddressOf ? new AddressOf(actual) : actual);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Traverse the graph and calculate the stack state for each block and instruction
|
|
private void TraverseGraph(Block initialBlock, int initialVisitedBlockCount = 0)
|
|
{
|
|
var blockLevelState = new Stack<(Block, int)>();
|
|
blockLevelState.Push((initialBlock, initialVisitedBlockCount));
|
|
|
|
while (blockLevelState.Count > 0)
|
|
{
|
|
var (block, visitedBlockCount) = blockLevelState.Pop();
|
|
|
|
// Copy current state
|
|
var incomingState = _inComingState[block];
|
|
var currentState = incomingState.Copy();
|
|
|
|
// Process instructions
|
|
foreach (var instruction in block.Instructions)
|
|
{
|
|
_instructionState[instruction] = currentState;
|
|
|
|
if (instruction.OpCode == OpCode.ShiftStack)
|
|
{
|
|
var offset = (int)((Immediate)instruction.Operands[0]).Value;
|
|
currentState = currentState.Copy();
|
|
currentState.Size += offset;
|
|
}
|
|
else if (block.Instructions[^1] == instruction && block.BlockType == BlockType.TailCall)
|
|
{
|
|
// Tail calls clear stack
|
|
currentState = currentState.Copy();
|
|
currentState.Size = 0;
|
|
}
|
|
}
|
|
|
|
// Tail calls clear stack
|
|
if (block.BlockType == BlockType.TailCall)
|
|
currentState.Size = 0;
|
|
|
|
_outGoingState[block] = currentState;
|
|
|
|
visitedBlockCount++;
|
|
|
|
if (MaxBlockVisitCount != -1 && visitedBlockCount > MaxBlockVisitCount)
|
|
throw new DecompilerException($"Stack state not settling! ({visitedBlockCount} blocks already visited)");
|
|
|
|
// Visit successors
|
|
foreach (var successor in block.Successors)
|
|
{
|
|
// Already visited
|
|
if (_inComingState.TryGetValue(successor, out var existingState))
|
|
{
|
|
if (existingState.Size != currentState.Size)
|
|
{
|
|
_inComingState[successor] = currentState.Copy();
|
|
blockLevelState.Push((successor, visitedBlockCount + 1));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Set incoming delta and add to queue
|
|
_inComingState[successor] = currentState.Copy();
|
|
blockLevelState.Push((successor, visitedBlockCount + 1));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void ReplaceStackWithRegisters(MethodAnalysisContext method)
|
|
{
|
|
var instructions = method.ControlFlowGraph!.Instructions;
|
|
|
|
// Replace stack offset operands
|
|
foreach (var instruction in instructions)
|
|
{
|
|
for (var i = 0; i < instruction.Operands.Count; i++)
|
|
{
|
|
var operand = instruction.Operands[i];
|
|
|
|
if (operand is StackOffset offset)
|
|
instruction.SetOperand(i, new Register(null, NameForSlot(offset)));
|
|
|
|
if (operand is AddressOf { Target: StackOffset addressed })
|
|
instruction.SetOperand(i, new AddressOf(new Register(null, NameForSlot(addressed))));
|
|
}
|
|
}
|
|
|
|
// Replace params
|
|
for (var i = 0; i < method.ParameterOperands.Count; i++)
|
|
{
|
|
var parameter = method.ParameterOperands[i];
|
|
|
|
if (parameter is StackOffset offset)
|
|
method.ParameterOperands[i] = new Register(null, NameForSlot(offset));
|
|
}
|
|
}
|
|
|
|
private static string NameForSlot(StackOffset offset) => offset.Offset < 0 ? $"stack_-{-offset.Offset:X}" : $"stack_{offset.Offset:X}";
|
|
}
|