From ed6fea0efba0c9e47e27e443d00e53822764a17c Mon Sep 17 00:00:00 2001 From: patchzyy <64382339+patchzyy@users.noreply.github.com> Date: Sun, 18 Oct 2026 14:03:38 +0200 Subject: [PATCH] fix 279 (#297) * fix 279 * treat conditional calls as calls * keep local linked branches as jumps --- .../Disassembly/PpcDisassembler.cs | 5 ++ .../src/Translator.Core/Lifting/PpcLifter.cs | 28 +++++-- .../Translation/FunctionTranslator.cs | 21 +++-- .../FunctionTranslatorCoverageTests.cs | 83 ++++++++++++++++--- .../PpcLifterAdditionalCoverageTests.cs | 81 ++++++++++++++++++ .../PpcLifterCoverageTests.cs | 1 + 6 files changed, 194 insertions(+), 25 deletions(-) diff --git a/translator/src/Translator.Core/Disassembly/PpcDisassembler.cs b/translator/src/Translator.Core/Disassembly/PpcDisassembler.cs index 413db4b..48f5440 100644 --- a/translator/src/Translator.Core/Disassembly/PpcDisassembler.cs +++ b/translator/src/Translator.Core/Disassembly/PpcDisassembler.cs @@ -230,6 +230,11 @@ public sealed class PpcDisassembler : IDisposable { foreach (var target in ins.BranchTargets) { + // A conditional call to another function returns to its fallthrough. + if (ins.IsCall && IsKnownExternalFunctionEntry(target)) + { + continue; + } localControlFlowTargets.Add(target); if (!reachable.Contains(target)) { diff --git a/translator/src/Translator.Core/Lifting/PpcLifter.cs b/translator/src/Translator.Core/Lifting/PpcLifter.cs index c7ffe57..bb57f86 100644 --- a/translator/src/Translator.Core/Lifting/PpcLifter.cs +++ b/translator/src/Translator.Core/Lifting/PpcLifter.cs @@ -690,10 +690,11 @@ public sealed partial class PpcLifter var rawInstr = ReadRawInstruction(ins); var bo = (int)((rawInstr >> 21) & 0x1F); var bi = (int)((rawInstr >> 16) & 0x1F); - var fallthrough = $"0x{ins.EndAddress:X8}"; - var targetLabel = mnemonic == "bcctr" - ? $"indirect_ctr_{ins.Address:X8}" - : $"call_ctr_{ins.Address:X8}_{ins.EndAddress:X8}"; + var linked = mnemonic == "bcctrl"; + var targetLabel = linked + ? $"call_ctr_{ins.Address:X8}_{ins.EndAddress:X8}" + : $"indirect_ctr_{ins.Address:X8}"; + var fallthrough = linked ? LinkedFallthroughLabel(ins) : $"0x{ins.EndAddress:X8}"; return new[] { new IrBranch("raw", targetLabel, fallthrough, BuildBoConditionExpression(bo, bi, allowCtr: false)) }; } @@ -1943,6 +1944,15 @@ public sealed partial class PpcLifter }; case "bltl": + case "bgtl": + case "beql": + case "bsol": + case "bgel": + case "blel": + case "bnel": + case "bnsl": + case "bdnzl": + case "bdzl": case "bcl": { var rawInstr = ReadRawInstruction(ins); @@ -1978,14 +1988,13 @@ public sealed partial class PpcLifter var rawInstr = ReadRawInstruction(ins); var bo = (int)((rawInstr >> 21) & 0x1F); var bi = (int)((rawInstr >> 16) & 0x1F); - var fallthrough = $"0x{ins.EndAddress:X8}"; var targetLabel = $"call_lr_{ins.Address:X8}_{ins.EndAddress:X8}"; var instructions = new List(); if ((bo & 0x04) == 0) { instructions.Add(new IrBinary("ctr", IrValue.Register("ctr"), IrValue.Imm(-1), "add")); } - instructions.Add(new IrBranch("raw", targetLabel, fallthrough, BuildBoConditionExpression(bo, bi, allowCtr: true))); + instructions.Add(new IrBranch("raw", targetLabel, LinkedFallthroughLabel(ins), BuildBoConditionExpression(bo, bi, allowCtr: true))); return instructions; } @@ -2004,10 +2013,9 @@ public sealed partial class PpcLifter crField = NormalizeRegister(crOp.Name); } - var fallthrough = $"0x{ins.EndAddress:X8}"; var targetLabel = $"call_lr_{ins.Address:X8}_{ins.EndAddress:X8}"; var cond = mnemonic.Replace("lrl", string.Empty, StringComparison.OrdinalIgnoreCase); - return new[] { new IrBranch(cond, targetLabel, fallthrough, crField) }; + return new[] { new IrBranch(cond, targetLabel, LinkedFallthroughLabel(ins), crField) }; } case "bne": @@ -3033,6 +3041,10 @@ private static string TargetLabel(PpcInstruction ins, HashSet validAddress return $"0x{target:X8}"; } + // Not-taken path of a linked conditional branch: LR still receives the return address. + private static string LinkedFallthroughLabel(PpcInstruction ins) => + $"link_branch_{ins.EndAddress:X8}_{ins.EndAddress:X8}"; + private static string BuildBoConditionExpression(int bo, int bi, bool allowCtr) { var crFieldIdx = bi / 4; diff --git a/translator/src/Translator.Core/Translation/FunctionTranslator.cs b/translator/src/Translator.Core/Translation/FunctionTranslator.cs index 34bb2aa..86ab11a 100644 --- a/translator/src/Translator.Core/Translation/FunctionTranslator.cs +++ b/translator/src/Translator.Core/Translation/FunctionTranslator.cs @@ -420,7 +420,7 @@ public sealed class FunctionTranslator continue; } - if (TryBuildSyntheticBlock(label, out var synth)) + if (TryBuildSyntheticBlock(label, existingLabels, out var synth)) { irBlocks.Add(synth); existingLabels.Add(label); @@ -839,7 +839,7 @@ public sealed class FunctionTranslator return rewritten; } - private static bool TryBuildSyntheticBlock(string label, out IrBasicBlock block) + private static bool TryBuildSyntheticBlock(string label, IReadOnlySet localLabels, out IrBasicBlock block) { const string IndirectCtrPrefix = "indirect_ctr_"; const string CallCtrPrefix = "call_ctr_"; @@ -883,11 +883,18 @@ public sealed class FunctionTranslator if (TryParseLinkBranchLabel(label, LinkBranchPrefix, out fallthrough, out var target)) { - block = new IrBasicBlock(label, new IrInstruction[] - { - new IrAssign("lr", IrValue.Imm((int)fallthrough)), - new IrJump($"0x{target:X8}") - }); + block = new IrBasicBlock(label, target == fallthrough || localLabels.Contains($"0x{target:X8}") + ? new IrInstruction[] + { + new IrAssign("lr", IrValue.Imm((int)fallthrough)), + new IrJump($"0x{target:X8}") + } + : new IrInstruction[] + { + new IrAssign("lr", IrValue.Imm((int)fallthrough)), + new IrCall(string.Empty, $"0x{target:X8}", BuildAbiCallArgs()), + new IrJump($"0x{fallthrough:X8}") + }); return true; } diff --git a/translator/tests/Translator.Tests/FunctionTranslatorCoverageTests.cs b/translator/tests/Translator.Tests/FunctionTranslatorCoverageTests.cs index f7d09e2..9f2f229 100644 --- a/translator/tests/Translator.Tests/FunctionTranslatorCoverageTests.cs +++ b/translator/tests/Translator.Tests/FunctionTranslatorCoverageTests.cs @@ -45,6 +45,37 @@ public class FunctionTranslatorCoverageTests Assert.Equal(TimeSpan.Zero, discovery.Metrics.CodeGen); } + [Theory] + [InlineData(0x4D820021u)] // beqlrl + [InlineData(0x4D820421u)] // bcctrl, BO=12 BI=2 + public void Discover_LinkedConditionalRegisterBranchUpdatesLrWhenNotTaken(uint branchWord) + { + var memory = new byte[MemoryLayout.RamSize]; + WriteWord(memory, 0x80001000, branchWord); + WriteWord(memory, 0x80001004, 0x7C6802A6); // mflr r3 + WriteWord(memory, 0x80001008, 0x4E800020); // blr + + var image = new ProgramImage( + memory, + AddressRange.FromStartAndSize(0x80001000, 0x1004), + AddressRange.FromStartAndSize(0x80001000, 0x1004), + default, + "test"); + var discovery = new FunctionTranslator(image).Discover( + 0x80001000, + TranslationOptions.Default with { MaxBytes = 0x20, AllowUnsupportedInstructions = true }); + + var blocks = discovery.Ssa.Function.Blocks; + var branch = Assert.Single(blocks.SelectMany(b => b.Instructions).OfType()); + Assert.Equal("link_branch_80001004_80001004", branch.FalseLabel); + + var notTaken = Assert.Single(blocks, b => b.Label == branch.FalseLabel); + var lrAssign = Assert.Single(notTaken.Instructions.OfType()); + Assert.StartsWith("lr", lrAssign.Destination, StringComparison.Ordinal); + Assert.Equal(unchecked((int)0x80001004u), lrAssign.Value.Constant); + Assert.Equal("0x80001004", Assert.IsType(notTaken.Instructions[^1]).TargetLabel); + } + [Fact] public void Translate_PreservesFallthroughAfterDirectLinkBranch() { @@ -354,18 +385,20 @@ public class FunctionTranslatorCoverageTests Assert.Equal("f13", abiArgs[^1].RegisterName); } + private static readonly HashSet NoLocalLabels = new(); + [Fact] public void TryBuildSyntheticBlock_CoversIndirectAndCallForms() { - var indirectArgs = new object?[] { "indirect_ctr_80001234", null! }; + var indirectArgs = new object?[] { "indirect_ctr_80001234", NoLocalLabels, null! }; Assert.True(InvokePrivate("TryBuildSyntheticBlock", indirectArgs)); - var indirect = Assert.IsType(indirectArgs[1]); + var indirect = Assert.IsType(indirectArgs[2]); Assert.Equal("indirect_ctr_80001234", indirect.Label); Assert.Equal("ctr", Assert.IsType(Assert.Single(indirect.Instructions)).Target.RegisterName); - var callCtrArgs = new object?[] { "call_ctr_80001000_80001004", null! }; + var callCtrArgs = new object?[] { "call_ctr_80001000_80001004", NoLocalLabels, null! }; Assert.True(InvokePrivate("TryBuildSyntheticBlock", callCtrArgs)); - var callCtr = Assert.IsType(callCtrArgs[1]); + var callCtr = Assert.IsType(callCtrArgs[2]); Assert.Collection( callCtr.Instructions, ins => @@ -382,9 +415,9 @@ public class FunctionTranslatorCoverageTests }, ins => Assert.Equal("0x80001004", Assert.IsType(ins).TargetLabel)); - var callLrArgs = new object?[] { "call_lr_80002000_80002004", null! }; + var callLrArgs = new object?[] { "call_lr_80002000_80002004", NoLocalLabels, null! }; Assert.True(InvokePrivate("TryBuildSyntheticBlock", callLrArgs)); - var callLr = Assert.IsType(callLrArgs[1]); + var callLr = Assert.IsType(callLrArgs[2]); Assert.Collection( callLr.Instructions, ins => @@ -407,9 +440,39 @@ public class FunctionTranslatorCoverageTests }, ins => Assert.Equal("0x80002004", Assert.IsType(ins).TargetLabel)); - var absoluteArgs = new object?[] { "0x800D7FCC", null! }; + var notTakenArgs = new object?[] { "link_branch_80002004_80002004", NoLocalLabels, null! }; + Assert.True(InvokePrivate("TryBuildSyntheticBlock", notTakenArgs)); + var notTaken = Assert.IsType(notTakenArgs[2]); + Assert.Collection( + notTaken.Instructions, + ins => + { + var assign = Assert.IsType(ins); + Assert.Equal("lr", assign.Destination); + Assert.Equal(unchecked((int)0x80002004u), assign.Value.Constant); + }, + ins => Assert.Equal("0x80002004", Assert.IsType(ins).TargetLabel)); + + var linkedCallArgs = new object?[] { "link_branch_80002004_800D7FCC", NoLocalLabels, null! }; + Assert.True(InvokePrivate("TryBuildSyntheticBlock", linkedCallArgs)); + var linkedCall = Assert.IsType(linkedCallArgs[2]); + Assert.Collection( + linkedCall.Instructions, + ins => Assert.Equal(unchecked((int)0x80002004u), Assert.IsType(ins).Value.Constant), + ins => Assert.Equal("0x800D7FCC", Assert.IsType(ins).Target), + ins => Assert.Equal("0x80002004", Assert.IsType(ins).TargetLabel)); + + var localLinkedArgs = new object?[] { "link_branch_80002004_80002000", new HashSet { "0x80002000" }, null! }; + Assert.True(InvokePrivate("TryBuildSyntheticBlock", localLinkedArgs)); + var localLinked = Assert.IsType(localLinkedArgs[2]); + Assert.Collection( + localLinked.Instructions, + ins => Assert.Equal(unchecked((int)0x80002004u), Assert.IsType(ins).Value.Constant), + ins => Assert.Equal("0x80002000", Assert.IsType(ins).TargetLabel)); + + var absoluteArgs = new object?[] { "0x800D7FCC", NoLocalLabels, null! }; Assert.True(InvokePrivate("TryBuildSyntheticBlock", absoluteArgs)); - var absolute = Assert.IsType(absoluteArgs[1]); + var absolute = Assert.IsType(absoluteArgs[2]); Assert.Equal("0x800D7FCC", absolute.Label); Assert.Collection( absolute.Instructions, @@ -421,9 +484,9 @@ public class FunctionTranslatorCoverageTests }, ins => Assert.Null(Assert.IsType(ins).Value)); - var invalidArgs = new object?[] { "plain_label", null! }; + var invalidArgs = new object?[] { "plain_label", NoLocalLabels, null! }; Assert.False(InvokePrivate("TryBuildSyntheticBlock", invalidArgs)); - Assert.Null(invalidArgs[1]); + Assert.Null(invalidArgs[2]); } [Fact] diff --git a/translator/tests/Translator.Tests/PpcLifterAdditionalCoverageTests.cs b/translator/tests/Translator.Tests/PpcLifterAdditionalCoverageTests.cs index 32162d6..2c0734e 100644 --- a/translator/tests/Translator.Tests/PpcLifterAdditionalCoverageTests.cs +++ b/translator/tests/Translator.Tests/PpcLifterAdditionalCoverageTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using Translator.Core.Disassembly; using Translator.Core.Ir; @@ -37,6 +38,85 @@ public class PpcLifterAdditionalCoverageTests Assert.Equal($"0x{branch.EndAddress:X8}", decision.FalseLabel); } + [Fact] + public void ConditionalCallTargetIsNotDecodedAsLocalCode() + { + const uint entry = 0x80000000; + const uint callee = entry + 0x10; + + var image = TranslatorCppTestHarness.CreateImage( + (entry + 0x00, 0x41820011), // beql callee + (entry + 0x04, 0x38600001), // li r3, 1 + (entry + 0x08, 0x4E800020), // blr + (callee + 0x00, 0x38600002), // li r3, 2 + (callee + 0x04, 0x4E800020)); // blr + + using var disassembler = new PpcDisassembler(); + var instructions = disassembler.DisassembleFunction( + image, entry, maxInstructions: 32, maxBytes: 0x80, knownFunctionEntryPoints: new HashSet { entry, callee }); + + Assert.Equal(new[] { entry, entry + 4, entry + 8 }, instructions.Select(ins => ins.Address)); + } + + [Fact] + public void ConditionalCallToLocalTargetKeepsLocalEdge() + { + const uint entry = 0x80000000; + + var image = TranslatorCppTestHarness.CreateImage( + (entry + 0x00, 0x4182000D), // beql local + (entry + 0x04, 0x38600001), // li r3, 1 + (entry + 0x08, 0x4E800020), // blr + (entry + 0x0C, 0x38600002), // local: li r3, 2 + (entry + 0x10, 0x4E800020)); // blr + + using var disassembler = new PpcDisassembler(); + var instructions = disassembler.DisassembleFunction( + image, entry, maxInstructions: 32, maxBytes: 0x80, knownFunctionEntryPoints: new HashSet { entry }); + + Assert.Contains(instructions, ins => ins.Address == entry + 0x0C); + } + + [Theory] + [InlineData(0x41820009u, "beql", "0x80001008")] + [InlineData(0x41810009u, "bgtl", "0x80001008")] + [InlineData(0x40820009u, "bnel", "0x80001008")] + [InlineData(0x42000009u, "bdnzl", "0x80001008")] + [InlineData(0x42400009u, "bdzl", "0x80001008")] + public void LinkedConditionalRelativeBranchFormsSetLrOnBothPaths(uint word, string mnemonic, string target) + { + var branch = PpcDecoder.Decode(0x80001000, word); + var ir = Assert.Single(new PpcLifter().Lift(new[] { branch })).Ir; + + Assert.Equal(mnemonic, branch.Mnemonic); + + var lrAssign = Assert.Single(ir.OfType()); + Assert.Equal("lr", lrAssign.Destination); + Assert.Equal(unchecked((int)0x80001004u), lrAssign.Value.Constant); + Assert.IsType(ir[^2]); + + var decision = Assert.IsType(ir[^1]); + Assert.Equal("raw", decision.Condition); + Assert.Equal($"link_branch_80001004_{target[2..]}", decision.TrueLabel); + Assert.Equal("0x80001004", decision.FalseLabel); + Assert.Equal(mnemonic.StartsWith("bd", StringComparison.Ordinal), ir.OfType().Any(b => b.Destination == "ctr")); + } + + [Theory] + [InlineData(0x4D820021u)] + [InlineData(0x4D820421u)] + [InlineData(0x4D030021u)] + public void LinkedConditionalRegisterBranchesRouteNotTakenPathThroughLrUpdate(uint word) + { + var branch = PpcDecoder.Decode(0x80001000, word); + var ir = Assert.Single(new PpcLifter().Lift(new[] { branch })).Ir; + + var decision = Assert.IsType(ir[^1]); + Assert.Equal("link_branch_80001004_80001004", decision.FalseLabel); + Assert.StartsWith(branch.Mnemonic.Contains("ctr", StringComparison.Ordinal) ? "call_ctr_" : "call_lr_", decision.TrueLabel, StringComparison.Ordinal); + Assert.DoesNotContain(ir, ins => ins is IrAssign { Destination: "lr" }); + } + [Fact] public void LiftsAdditionalNonDotArithmeticAndLogicalForms() { @@ -365,6 +445,7 @@ public class PpcLifterAdditionalCoverageTests var bclrl = Instruction((12u << 21) | (8u << 16), "bclrl", new PpcConditionRegisterOperand("cr2", 8)); var bclrlBranch = Assert.IsType(Assert.Single(Assert.Single(lifter.Lift(new[] { bclrl })).Ir)); Assert.Contains("call_lr_80000000_80000004", bclrlBranch.TrueLabel, StringComparison.Ordinal); + Assert.Equal("link_branch_80000004_80000004", bclrlBranch.FalseLabel); Assert.Equal("raw", bclrlBranch.Condition); Assert.Contains("GetCRBit(ctx, 2, 0)", bclrlBranch.ConditionRegister, StringComparison.Ordinal); } diff --git a/translator/tests/Translator.Tests/PpcLifterCoverageTests.cs b/translator/tests/Translator.Tests/PpcLifterCoverageTests.cs index c8ee8af..16ad792 100644 --- a/translator/tests/Translator.Tests/PpcLifterCoverageTests.cs +++ b/translator/tests/Translator.Tests/PpcLifterCoverageTests.cs @@ -564,6 +564,7 @@ public class PpcLifterCoverageTests Assert.Equal("beq", beqlrlBranch.Condition); Assert.Equal("cr3", beqlrlBranch.ConditionRegister); Assert.Contains("call_lr_80000000_80000004", beqlrlBranch.TrueLabel, StringComparison.Ordinal); + Assert.Equal("link_branch_80000004_80000004", beqlrlBranch.FalseLabel); var bgtlrl = Instruction(0, "bgtlrl"); var bgtlrlBranch = Assert.IsType(Assert.Single(Assert.Single(lifter.Lift(new[] { bgtlrl })).Ir));