From 7f83b92fcdd17ddba5d8964048418cf76c7c8e57 Mon Sep 17 00:00:00 2001 From: Yoshi Askharoun Date: Wed, 24 Sep 2025 10:34:28 -0500 Subject: [PATCH] [WIP] If-elseif-else clause decompilation --- libs/UIX.DecompXml/CodeBlockInfo.cs | 2 +- libs/UIX.DecompXml/ControlFlowAnalyzer.cs | 75 +++++++++++++++--- libs/UIX.DecompXml/Decompiler.Script.cs | 92 ++++++++++++++--------- 3 files changed, 125 insertions(+), 44 deletions(-) diff --git a/libs/UIX.DecompXml/CodeBlockInfo.cs b/libs/UIX.DecompXml/CodeBlockInfo.cs index 72b6490..c36ea57 100644 --- a/libs/UIX.DecompXml/CodeBlockInfo.cs +++ b/libs/UIX.DecompXml/CodeBlockInfo.cs @@ -6,7 +6,7 @@ using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; namespace Microsoft.Iris.DecompXml; -internal class CodeBlockInfo +internal record CodeBlockInfo { public CodeBlockInfo(uint startOffset, uint endOffset, SyntaxKind kind, ExpressionSyntax expression = null) { diff --git a/libs/UIX.DecompXml/ControlFlowAnalyzer.cs b/libs/UIX.DecompXml/ControlFlowAnalyzer.cs index 16ba0e5..d09085d 100644 --- a/libs/UIX.DecompXml/ControlFlowAnalyzer.cs +++ b/libs/UIX.DecompXml/ControlFlowAnalyzer.cs @@ -18,7 +18,7 @@ public static class ControlFlowAnalyzer OpCode.ReturnValue, OpCode.ReturnVoid, OpCode.Jump, ]; - public static List CreateGraph(Instruction[] instructions) + public static List CreateGraph(Instruction[] instructions) { // See slide 15 (page 8) of https://www.cs.utexas.edu/~lin/cs380c/handout03.pdf @@ -42,7 +42,7 @@ public static class ControlFlowAnalyzer } // Pass II: Segment the procedure so a leader starts each block - List blocks = []; + List blocks = []; foreach (var leaderOffset in leaderOffsets) { @@ -71,14 +71,14 @@ public static class ControlFlowAnalyzer ?? uint.MaxValue; } - ControlFlowBlock block = new(leaderOffset, lastInstruction.Offset, body, nextOffset, jumpOffset); + BasicControlFlowBlock block = new(leaderOffset, lastInstruction.Offset, body, nextOffset, jumpOffset); blocks.Add(block); } // Pass III: Resolve next and branch target blocks for (var b = 0; b < blocks.Count; b++) { - var block = blocks[b]; + var block = (BasicControlFlowBlock)blocks[b]; if (block.NextOffset is not uint.MaxValue) block = block with { Next = blocks[IndexOfBlockFromStartOffset(block.NextOffset)] }; @@ -104,7 +104,21 @@ public static class ControlFlowAnalyzer } } - public static string SerializeToGraphviz(IEnumerable blocks) + public static List CollapseBlocks(this List blocks) + { + for (int b = 0; b < blocks.Count; b++) + { + var block = blocks[b]; + if (block.Body[^1].OpCode is OpCode.JumpIfFalse) + { + // If conditions always end with JMPF + } + } + + return []; + } + + public static string SerializeToGraphviz(IEnumerable blocks) { var sortedBlocks = blocks.OrderBy(b => b.StartOffset).ToArray(); StringBuilder sb = new(); @@ -132,8 +146,8 @@ public static class ControlFlowAnalyzer if (block.NextOffset is not uint.MaxValue) sb.AppendLine($" {nodeId}:s -> {block.NextOffset}:n;"); - if (block.BranchTargetOffset is not uint.MaxValue) - sb.AppendLine($" {nodeId}:s -> {block.BranchTargetOffset}:n [color=red];"); + if (block is BasicControlFlowBlock { BranchTargetOffset: not uint.MaxValue } cfBlock) + sb.AppendLine($" {nodeId}:s -> {cfBlock.BranchTargetOffset}:n [color=red];"); } sb.AppendLine("}"); @@ -142,6 +156,49 @@ public static class ControlFlowAnalyzer } } -public record ControlFlowBlock(uint StartOffset, uint EndOffset, List Body, +public interface IProgramBlock +{ + uint StartOffset { get; } + uint EndOffset { get; } + uint NextOffset { get; } + List Body { get; } + IProgramBlock Next { get; } + + bool HasEdgeTo(IProgramBlock block); +} + +public abstract record ProgramBlock(uint StartOffset, uint EndOffset, List Body, + uint NextOffset = uint.MaxValue, IProgramBlock Next = null) + : IProgramBlock +{ + public virtual bool HasEdgeTo(IProgramBlock block) => NextOffset == block.StartOffset; +} + +public record BasicControlFlowBlock(uint StartOffset, uint EndOffset, List Body, uint NextOffset = uint.MaxValue, uint BranchTargetOffset = uint.MaxValue, - ControlFlowBlock Next = null, ControlFlowBlock BranchTarget = null); + IProgramBlock Next = null, IProgramBlock BranchTarget = null) + : ProgramBlock(StartOffset, EndOffset, Body, NextOffset, Next) +{ + public override bool HasEdgeTo(IProgramBlock block) => base.HasEdgeTo(block) || BranchTargetOffset == block.StartOffset; +} + +public record ConditionalControlFlowBlock(uint StartOffset, uint EndOffset, List Body, + uint NextOffset = uint.MaxValue, IProgramBlock Next = null, + IProgramBlock IfBlock = null, List ElseIfBlocks = null, IProgramBlock ElseBlock = null) + : ProgramBlock(StartOffset, EndOffset, Body, NextOffset, Next) +{ + public ConditionalControlFlowBlock(IProgramBlock programBlock, + IProgramBlock ifBlock = null, List elseIfBlocks = null, IProgramBlock elseBlock = null) + : this(programBlock.StartOffset, programBlock.EndOffset, programBlock.Body, + programBlock.NextOffset, programBlock.Next, ifBlock, elseIfBlocks ?? [], elseBlock) + { + } + + public override bool HasEdgeTo(IProgramBlock block) + { + return base.HasEdgeTo(block) + || IfBlock.StartOffset == block.StartOffset + || ElseIfBlocks.Any(b => b.StartOffset == block.StartOffset) + || ElseBlock.StartOffset == block.StartOffset; + } +} diff --git a/libs/UIX.DecompXml/Decompiler.Script.cs b/libs/UIX.DecompXml/Decompiler.Script.cs index a24a460..5edb4a4 100644 --- a/libs/UIX.DecompXml/Decompiler.Script.cs +++ b/libs/UIX.DecompXml/Decompiler.Script.cs @@ -34,9 +34,22 @@ partial class Decompiler var dotGraph = ControlFlowAnalyzer.SerializeToGraphviz(controlBlocks); Console.WriteLine(dotGraph); + // TODO: Search for loops + + // TODO: Search for branch conditions + var collapsedBlocks = controlBlocks.CollapseBlocks(); + Stack blockStack = []; blockStack.Push(new(0, methodBody[^1].Offset, SyntaxKind.Block, null)); + HashSet jumpFalseToOffsets = new(methodBody + .Where(i => i.OpCode is OpCode.JumpIfFalse) + .Select(i => (uint)i.Operands.First().Value)); + + HashSet jumpToOffsets = new(methodBody + .Where(i => i.OpCode is OpCode.Jump) + .Select(i => (uint)i.Operands.First().Value)); + HashSet scopedLocals = []; Stack stack = new(); @@ -44,20 +57,33 @@ partial class Decompiler { var instruction = methodBody[i]; - // TODO: Handle for loops - if (instruction.Offset == blockStack.Peek().EndOffset) + if (jumpFalseToOffsets.Contains(instruction.Offset)) { - // Make sure there is only one top-level block - if (blockStack.Count > 1) + var currentBlock = blockStack.Pop() with { EndOffset = instruction.Offset }; + currentBlock.FinalizeBlock(blockStack.Peek()); + + // This address marks the end of the affirmative branch of an IF clause. + // If multiple blocks lead to this address, then we're outside of the IF clause entirely. + // Otherwise, it's probably the start of an ELSE clause. + + var currentControlBlock = controlBlocks.First(b => instruction.Offset >= b.StartOffset && instruction.Offset <= b.EndOffset); + if (controlBlocks.Count(b => b.HasEdgeTo(currentControlBlock)) <= 1) { - var currentBlock = blockStack.Pop(); - currentBlock.FinalizeBlock(blockStack.Peek()); + blockStack.Push(new(instruction.Offset, uint.MaxValue, SyntaxKind.ElseClause, null)); } - else + } + + if (jumpToOffsets.Contains(instruction.Offset)) + { + // This address is code that will be unconditionally executed. For now, + // we'll assume that this is the end of IF/ELSE clauses. + while (blockStack.Count > 1) { - // End of function - if (i + 1 != methodBody.Length) - throw new InvalidOperationException("Expected end of function!"); + if (blockStack.Peek().Kind is SyntaxKind.ElseClause) + { + var currentBlock = blockStack.Pop() with { EndOffset = instruction.Offset }; + currentBlock.FinalizeBlock(blockStack.Peek()); + } } } @@ -126,6 +152,8 @@ partial class Decompiler .WithInitializer(EqualsValueClause(newSymbolValueExpr)) ) )); + + scopedLocals.Add(symbolRef.Symbol); } else { @@ -181,38 +209,34 @@ partial class Decompiler } var isPeek = opCode is OpCode.JumpIfFalsePeek or OpCode.JumpIfTruePeek or OpCode.JumpIfNullPeek; - var rawJumpCondition = IrisExpression.ToSyntax(isPeek ? stack.Peek() : stack.Pop(), _context); + var jumpCondition = IrisExpression.ToSyntax(isPeek ? stack.Peek() : stack.Pop(), _context); - // TODO: Invert jump condition when decompiling to blocks instead of gotos - var jumpCondition = opCode switch + if (opCode is OpCode.JumpIfFalse) { - OpCode.JumpIfFalse or - OpCode.JumpIfFalsePeek => LogicalNotOf(rawJumpCondition), - - OpCode.JumpIfTruePeek => rawJumpCondition, - - OpCode.JumpIfNullPeek => BinaryExpression(SyntaxKind.EqualsEqualsToken, - rawJumpCondition, - LiteralExpression(SyntaxKind.NullLiteralExpression)), - - _ => throw new NotImplementedException() - }; - - //var ifBlock = new CodeBlockInfo(instruction.Offset, jumpToOffset, SyntaxKind.IfStatement, jumpCondition); - //blockStack.Push(ifBlock); - - var ifBlock = IfStatement(SimplifyExpression(jumpCondition), - GotoStatement(SyntaxKind.GotoStatement, IdentifierName($"UIB_{jumpToOffset:X4}"))) - .WithLeadingTrivia(Comment($"/* UIB_{instruction.Offset:X4} */")); - blockStack.Peek().Statements.Add(ifBlock); + // JMPF is used to evaluate the branch condition + var ifBlock = new CodeBlockInfo(instruction.Offset, jumpToOffset, SyntaxKind.IfStatement, jumpCondition); + blockStack.Push(ifBlock); + } + else if (opCode is OpCode.JumpIfFalsePeek or OpCode.JumpIfTruePeek) + { + // JMPFP and JMPTP are only used to implement short-circuiting + var ifBlock = SimplifyExpression(jumpCondition); + stack.Push(ifBlock); + } break; case OpCode.Jump: var jumpOffset = (uint)instruction.Operands.First().Value; - // TODO - blockStack.Peek().Statements.Add(GotoStatement(SyntaxKind.GotoStatement, IdentifierName($"UIB_{jumpOffset:X4}"))); + if (jumpOffset > instruction.Offset) + { + } + else + { + throw new NotImplementedException("Loops, ternaries, and null coalescing are not supported at this time"); + } + break; case OpCode.ReturnValue: