From 25c69ae28e46a127d87db8d394f857e34da54f99 Mon Sep 17 00:00:00 2001 From: Michael G <10155689+DarthMDev@users.noreply.github.com> Date: Thu, 10 Sep 2026 09:51:16 -0400 Subject: [PATCH] Fix crash from Kamek skip-return hooks (Item Rain crash) (#182) * fix: Kamek LR-continuation hook discovery and dispatch * test: cover branching Kamek LR continuations * review fix * another review fix fix: get the new tests to pass test: expose LR restore and loop continuation regressions * Update translator/src/Translator.Core/Mods/ContinuationPlanner.cs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * test: cover continuation regressions from the new path-sensitive planner * Update translator/src/Translator.Core/Mods/ContinuationPlanner.cs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * test: cover continuation regressions from the new path-sensitive planner * fix: preserve LR continuation analysis across large handlers and clobbers * fix: track LR-relative r1 across update-form stack stores * Harden LR-relative continuation test coverage * Fix LR/SP continuation state tracking --------- Co-authored-by: patchzyy <64382339+patchzyy@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- translator/src/Translator.Cli/Program.cs | 150 +---- .../Mods/ContinuationPlanner.cs | 527 ++++++++++++++++++ .../ContinuationPlannerTests.cs | 50 ++ .../LrContinuationCodeGenTests.cs | 63 +++ .../LrRelativeContinuationTests.cs | 448 +++++++++++++++ 5 files changed, 1112 insertions(+), 126 deletions(-) create mode 100644 translator/tests/Translator.Tests/LrContinuationCodeGenTests.cs create mode 100644 translator/tests/Translator.Tests/LrRelativeContinuationTests.cs diff --git a/translator/src/Translator.Cli/Program.cs b/translator/src/Translator.Cli/Program.cs index 9eee788..ac1f3c0 100644 --- a/translator/src/Translator.Cli/Program.cs +++ b/translator/src/Translator.Cli/Program.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Buffers.Binary; using System.Collections.Generic; using System.Diagnostics; @@ -1891,6 +1891,7 @@ int RunTranslateModCore(string[] argsTail, string? outputDirectoryOverride) overlayBuild, continuationPlan, retroWfcResolvedExecutableHooks, + patchPlan, kamekFunctionStarts, moduleLinkBase, selected.CodeSize, @@ -2231,6 +2232,7 @@ int EmitModCpp( OverlayBuildResult overlayBuild, ContinuationPlan continuationPlan, IReadOnlyCollection? retroWfcExecutableHooks, + KamekPatchPlan patchPlan, IReadOnlyList kamekFunctionStarts, uint moduleLinkBase, uint moduleLinkedCodeSize, @@ -2272,14 +2274,25 @@ int EmitModCpp( .ToHashSet(); var queuedContinuationAddresses = continuationPlan.Entries.Select(e => e.Address).ToHashSet(); var discoveredContinuationQueue = new Queue(); - var linkedHookLrBasesByTarget = retroWfcExecutableHooks is null - ? new Dictionary() - : retroWfcExecutableHooks - .Where(h => h.TargetAddress.HasValue && RetroWfcHookSetsLinkRegister(h)) - .GroupBy(h => h.TargetAddress!.Value) - .ToDictionary( - g => g.Key, - g => g.Select(h => h.ContinuationAddress).Distinct().ToArray()); + var hookLrBases = new List<(uint TargetAddress, uint ContinuationAddress)>(); + if (retroWfcExecutableHooks is not null) + { + hookLrBases.AddRange( + retroWfcExecutableHooks + .Where(h => h.TargetAddress.HasValue && RetroWfcHookSetsLinkRegister(h)) + .Select(h => (h.TargetAddress!.Value, h.ContinuationAddress))); + } + foreach (var patch in patchPlan.ExecutablePatches.Where(p => p.CommandId == KamekCommandId.BranchLink && p.Arguments.Count > 0)) + { + var target = KamekAddress.Resolve(patch.Arguments[0], patchPlan.ModuleGuestBase); + hookLrBases.Add((target, checked(patch.CommandAddress + 4u))); + } + + var linkedHookLrBasesByTarget = hookLrBases + .GroupBy(h => h.TargetAddress) + .ToDictionary( + g => g.Key, + g => g.Select(h => h.ContinuationAddress).Distinct().ToArray()); var lrContinuationCallTargets = linkedHookLrBasesByTarget.Keys.ToHashSet(); var linkedCallFallthroughLrOverrides = retroWfcExecutableHooks is null ? new Dictionary() @@ -2750,123 +2763,8 @@ IEnumerable DirectModuleTargets(FunctionTranslationResult result, uint mod } } -IEnumerable DiscoverLrRelativeIndirectJumpOffsets(FunctionTranslationResult result) -{ - var lrOffsets = new Dictionary(StringComparer.OrdinalIgnoreCase); - int? ctrOffset = null; - - foreach (var instruction in result.Instructions) - { - var mnemonic = instruction.Mnemonic.ToLowerInvariant(); - if (mnemonic == "mflr" && TryGetInstructionReg(instruction, 0, out var lrDest)) - { - lrOffsets[lrDest] = 0; - continue; - } - - if ((mnemonic == "mr" || mnemonic == "or") && - TryGetInstructionReg(instruction, 0, out var moveDest) && - TryGetInstructionReg(instruction, 1, out var moveSource) && - (mnemonic == "mr" || - (instruction.Operands.Count >= 3 && - instruction.Operands[2] is PpcRegisterOperand moveSource2 && - string.Equals(NormalizeInstructionReg(moveSource2.Name), moveSource, StringComparison.OrdinalIgnoreCase)))) - { - if (lrOffsets.TryGetValue(moveSource, out var sourceOffset)) - { - lrOffsets[moveDest] = sourceOffset; - } - else - { - lrOffsets.Remove(moveDest); - } - continue; - } - - if (mnemonic == "addi" && - TryGetInstructionReg(instruction, 0, out var addDest) && - TryGetInstructionReg(instruction, 1, out var addBase) && - TryGetInstructionImm(instruction, 2, out var imm)) - { - if (lrOffsets.TryGetValue(addBase, out var baseOffset)) - { - lrOffsets[addDest] = checked(baseOffset + imm); - } - else - { - lrOffsets.Remove(addDest); - } - continue; - } - - if (mnemonic == "mtctr" && TryGetInstructionReg(instruction, 0, out var ctrSource)) - { - ctrOffset = lrOffsets.TryGetValue(ctrSource, out var sourceOffset) ? sourceOffset : null; - continue; - } - - if (mnemonic == "bctr") - { - if (ctrOffset.HasValue) - { - yield return ctrOffset.Value; - } - ctrOffset = null; - continue; - } - - if (TryInstructionWritesDest(instruction, out var dest)) - { - lrOffsets.Remove(dest); - } - } - - static bool TryGetInstructionReg(PpcInstruction instruction, int index, out string register) - { - if (instruction.Operands.Count > index && instruction.Operands[index] is PpcRegisterOperand operand) - { - register = NormalizeInstructionReg(operand.Name); - return true; - } - - register = string.Empty; - return false; - } - - static bool TryGetInstructionImm(PpcInstruction instruction, int index, out int immediate) - { - if (instruction.Operands.Count > index && instruction.Operands[index] is PpcImmediateOperand operand) - { - immediate = operand.Value; - return true; - } - - immediate = 0; - return false; - } - - static bool TryInstructionWritesDest(PpcInstruction instruction, out string destination) - { - destination = string.Empty; - if (instruction.Operands.Count == 0 || instruction.Operands[0] is not PpcRegisterOperand operand) - { - return false; - } - - var mnemonic = instruction.Mnemonic.ToLowerInvariant(); - if (mnemonic.StartsWith("st", StringComparison.Ordinal) || - mnemonic.StartsWith("b", StringComparison.Ordinal) || - mnemonic.StartsWith("cmp", StringComparison.Ordinal)) - { - return false; - } - - destination = NormalizeInstructionReg(operand.Name); - return true; - } - - static string NormalizeInstructionReg(string register) => register.ToLowerInvariant(); -} +IEnumerable DiscoverLrRelativeIndirectJumpOffsets(FunctionTranslationResult result) => + ContinuationPlanner.DiscoverLrRelativeIndirectJumpOffsets(result.Instructions); static bool RetroWfcHookSetsLinkRegister(RetroWfcExecutableHookPlan hook) => hook.TypeName is "call" or "branchCtrLink" || diff --git a/translator/src/Translator.Core/Mods/ContinuationPlanner.cs b/translator/src/Translator.Core/Mods/ContinuationPlanner.cs index 7027278..e583bd5 100644 --- a/translator/src/Translator.Core/Mods/ContinuationPlanner.cs +++ b/translator/src/Translator.Core/Mods/ContinuationPlanner.cs @@ -1,4 +1,5 @@ using System.Buffers.Binary; +using System.Collections.Immutable; using System.Text.Json; using Translator.Core.Disassembly; using Translator.Core.Parsing.Kamek; @@ -273,4 +274,530 @@ public static class ContinuationPlanner Or, AddSigned } + + public static IEnumerable DiscoverLrRelativeIndirectJumpOffsets(IReadOnlyList instructions) + { + if (instructions.Count == 0) + { + yield break; + } + + var indexByAddress = new Dictionary(instructions.Count); + for (var i = 0; i < instructions.Count; i++) + { + indexByAddress.TryAdd(instructions[i].Address, i); + } + + var visited = new HashSet[instructions.Count]; + for (var i = 0; i < instructions.Count; i++) + { + visited[i] = new HashSet(); + } + + var seenOffsets = new HashSet(); + var worklist = new Queue<(int Index, PathState State)>(); + const int MaxStatesPerInstruction = 16; + + void Enqueue(int targetIndex, PathState stateToEnqueue) + { + worklist.Enqueue((targetIndex, stateToEnqueue)); + } + + int? GetFallthroughIndex(PpcInstruction instruction) + { + if (indexByAddress.TryGetValue(instruction.EndAddress, out var nextIndex)) + { + return nextIndex; + } + + return null; + } + + Enqueue(0, PathState.Empty); + + while (worklist.Count > 0) + { + var (idx, state) = worklist.Dequeue(); + if (!visited[idx].Add(state)) + { + continue; + } + + if (visited[idx].Count > MaxStatesPerInstruction) + { + continue; + } + + var instruction = instructions[idx]; + var mnemonic = instruction.Mnemonic.ToLowerInvariant(); + var nextState = state; + + if (mnemonic == "mflr" && TryGetInstructionReg(instruction, 0, out var lrDest)) + { + if (lrDest == "r1") + { + nextState = nextState.WithClearedStackOffsets(); + } + + nextState = nextState.LrReturnOffset.HasValue + ? nextState.WithLrOffset(lrDest, nextState.LrReturnOffset.Value) + : nextState.WithoutLrOffset(lrDest); + } + else if ((mnemonic == "mr" || mnemonic == "or") && + TryGetInstructionReg(instruction, 0, out var moveDest) && + TryGetInstructionReg(instruction, 1, out var moveSource) && + (mnemonic == "mr" || + (instruction.Operands.Count >= 3 && + instruction.Operands[2] is PpcRegisterOperand moveSource2 && + string.Equals(NormalizeInstructionReg(moveSource2.Name), moveSource, StringComparison.OrdinalIgnoreCase)))) + { + if (moveDest == "r1" && moveSource != "r1") + { + nextState = nextState.WithClearedStackOffsets(); + } + nextState = nextState.LrOffsets.TryGetValue(moveSource, out var sourceOffset) + ? nextState.WithLrOffset(moveDest, sourceOffset) + : nextState.WithoutLrOffset(moveDest); + } + else if ((mnemonic == "addi" || mnemonic == "addic") && + TryGetInstructionReg(instruction, 0, out var addDest) && + TryGetInstructionReg(instruction, 1, out var addBase) && + TryGetInstructionImm(instruction, 2, out var imm)) + { + if (addDest == "r1") + { + nextState = addBase == "r1" + ? nextState.WithSpDelta(unchecked(nextState.SpDelta + imm)) + : nextState.WithClearedStackOffsets(); + } + nextState = nextState.LrOffsets.TryGetValue(addBase, out var baseOffset) + ? nextState.WithLrOffset(addDest, unchecked(baseOffset + imm)) + : nextState.WithoutLrOffset(addDest); + } + else if (mnemonic == "mtctr" && TryGetInstructionReg(instruction, 0, out var ctrSource)) + { + var newCtrOffset = nextState.LrOffsets.TryGetValue(ctrSource, out var sourceOffset) ? sourceOffset : (int?)null; + nextState = nextState.WithCtrOffset(newCtrOffset); + } + else if (mnemonic == "mtlr" && TryGetInstructionReg(instruction, 0, out var lrSource)) + { + var newLrReturnOffset = nextState.LrOffsets.TryGetValue(lrSource, out var sourceOffset) ? sourceOffset : (int?)null; + nextState = nextState.WithLrReturnOffset(newLrReturnOffset); + } + else if (mnemonic == "stw" && + TryGetInstructionReg(instruction, 0, out var storeSrc) && + TryGetInstructionDisplacement(instruction, 1, out var storeDisp, out var storeBase, out _)) + { + if (storeBase == "r1") + { + var targetSlot = nextState.SpDelta + storeDisp; + nextState = nextState.LrOffsets.TryGetValue(storeSrc, out var offset) + ? nextState.WithStackOffset(targetSlot, offset) + : nextState.WithoutStackOffset(targetSlot); + } + } + else if (mnemonic == "stwu" && + TryGetInstructionReg(instruction, 0, out var stwuSrc) && + TryGetInstructionDisplacement(instruction, 1, out var stwuDisp, out var stwuBase, out _)) + { + if (stwuBase == "r1") + { + var targetSlot = nextState.SpDelta + stwuDisp; + nextState = nextState.LrOffsets.TryGetValue(stwuSrc, out var offset) + ? nextState.WithStackOffset(targetSlot, offset) + : nextState.WithoutStackOffset(targetSlot); + nextState = nextState.WithAdjustedStackPointer(stwuDisp); + } + else + { + nextState = nextState.WithoutLrOffset(stwuBase); + } + } + else if (TryGetStackStoreRange(instruction, out var storeOffset, out var storeSize, out var updatesStackPointer)) + { + nextState = nextState.WithoutStackOffsetsInRange( + nextState.SpDelta + storeOffset, + storeSize); + if (updatesStackPointer) + { + nextState = nextState.WithAdjustedStackPointer(storeOffset); + } + } + else if (mnemonic == "lwz" && + TryGetInstructionReg(instruction, 0, out var loadDest) && + TryGetInstructionDisplacement(instruction, 1, out var loadDisp, out var loadBase, out _)) + { + if (loadBase == "r1") + { + var targetSlot = nextState.SpDelta + loadDisp; + var hasStackOffset = nextState.StackOffsets.TryGetValue(targetSlot, out var offset); + + if (loadDest == "r1") + { + nextState = nextState.WithClearedStackOffsets(); + } + + nextState = hasStackOffset + ? nextState.WithLrOffset(loadDest, offset) + : nextState.WithoutLrOffset(loadDest); + } + else + { + nextState = nextState.WithoutLrOffset(loadDest); + if (loadDest == "r1") + { + nextState = nextState.WithClearedStackOffsets(); + } + } + } + else + { + if (TryInstructionWritesDest(instruction, out var destinations)) + { + foreach (var dest in destinations) + { + nextState = nextState.WithoutLrOffset(dest); + if (dest == "r1") + { + nextState = nextState.WithClearedStackOffsets(); + } + } + } + } + + if (instruction.IsCall || mnemonic == "bl" || mnemonic == "blrl") + { + nextState = nextState.WithLrReturnOffset(null).WithCtrOffset(null); + for (var register = 0; register <= 12; register++) + { + if (register != 1 && register != 2) + { + nextState = nextState.WithoutLrOffset($"r{register}"); + } + } + } + + if (mnemonic == "bctr") + { + if (state.CtrOffset.HasValue && seenOffsets.Add(state.CtrOffset.Value)) + { + yield return state.CtrOffset.Value; + } + + nextState = nextState.WithCtrOffset(null); + if (instruction.BranchTargets.Count == 0) + { + continue; + } + } + + var isReturn = !instruction.IsCall && (instruction.IsReturn || mnemonic == "blr" || mnemonic == "bclr" || + (mnemonic.StartsWith("b", StringComparison.Ordinal) && mnemonic.EndsWith("lr", StringComparison.Ordinal))); + if (isReturn) + { + if (state.LrReturnOffset.HasValue && state.LrReturnOffset.Value != 0 && seenOffsets.Add(state.LrReturnOffset.Value)) + { + yield return state.LrReturnOffset.Value; + } + + if (!instruction.IsConditionalBranch) + { + continue; + } + } + + if (instruction.IsUnconditionalBranch) + { + foreach (var target in instruction.BranchTargets) + { + if (indexByAddress.TryGetValue(target, out var targetIndex)) + { + Enqueue(targetIndex, nextState); + } + } + } + else if (instruction.IsConditionalBranch) + { + var fallthrough = GetFallthroughIndex(instruction); + if (fallthrough.HasValue) + { + Enqueue(fallthrough.Value, nextState); + } + + if (!isReturn) + { + foreach (var target in instruction.BranchTargets) + { + if (indexByAddress.TryGetValue(target, out var targetIndex)) + { + Enqueue(targetIndex, nextState); + } + } + } + } + else + { + var fallthrough = GetFallthroughIndex(instruction); + if (fallthrough.HasValue) + { + Enqueue(fallthrough.Value, nextState); + } + } + } + + static bool TryGetInstructionReg(PpcInstruction instruction, int index, out string register) + { + if (instruction.Operands.Count > index && instruction.Operands[index] is PpcRegisterOperand operand) + { + register = NormalizeInstructionReg(operand.Name); + return true; + } + + register = string.Empty; + return false; + } + + static bool TryGetInstructionDisplacement(PpcInstruction instruction, int index, out int offset, out string baseRegister, out int baseRegisterNumber) + { + if (instruction.Operands.Count > index && instruction.Operands[index] is PpcDisplacementOperand operand) + { + offset = operand.Offset; + baseRegister = NormalizeInstructionReg(operand.BaseRegister); + baseRegisterNumber = operand.BaseRegisterNumber; + return true; + } + + offset = 0; + baseRegister = string.Empty; + baseRegisterNumber = -1; + return false; + } + + static bool TryGetInstructionImm(PpcInstruction instruction, int index, out int immediate) + { + if (instruction.Operands.Count > index && instruction.Operands[index] is PpcImmediateOperand operand) + { + immediate = operand.Value; + return true; + } + + immediate = 0; + return false; + } + + static bool TryInstructionWritesDest(PpcInstruction instruction, out IReadOnlyList destinations) + { + if (instruction.Operands.Count == 0 || instruction.Operands[0] is not PpcRegisterOperand operand) + { + destinations = Array.Empty(); + return false; + } + + var mnemonic = instruction.Mnemonic.ToLowerInvariant(); + if (mnemonic.StartsWith("st", StringComparison.Ordinal) || + mnemonic.StartsWith("b", StringComparison.Ordinal) || + mnemonic.StartsWith("cmp", StringComparison.Ordinal)) + { + destinations = Array.Empty(); + return false; + } + + if (mnemonic == "lmw") + { + var startReg = Math.Clamp(operand.Number, 0, 31); + var regs = new string[32 - startReg]; + for (var r = startReg; r <= 31; r++) + { + regs[r - startReg] = $"r{r}"; + } + destinations = regs; + return true; + } + + destinations = [NormalizeInstructionReg(operand.Name)]; + return true; + } + + static bool TryGetStackStoreRange(PpcInstruction instruction, out int offset, out int size, out bool updatesStackPointer) + { + offset = 0; + size = 0; + updatesStackPointer = false; + if (!TryGetInstructionDisplacement(instruction, 1, out offset, out var baseRegister, out _) || + baseRegister != "r1") + { + return false; + } + + switch (instruction.Mnemonic.ToLowerInvariant()) + { + case "stfs": + size = 4; + return true; + case "stfsu": + size = 4; + updatesStackPointer = true; + return true; + case "stfd": + size = 8; + return true; + case "stfdu": + size = 8; + updatesStackPointer = true; + return true; + case "stmw" when instruction.Operands[0] is PpcRegisterOperand register: + size = checked((32 - Math.Clamp(register.Number, 0, 31)) * 4); + return true; + default: + return false; + } + } + + static string NormalizeInstructionReg(string register) => register.ToLowerInvariant(); + } + + private sealed class PathState : IEquatable + { + public ImmutableDictionary LrOffsets { get; } + public int? CtrOffset { get; } + public int? LrReturnOffset { get; } + public int SpDelta { get; } + public ImmutableDictionary StackOffsets { get; } + + public PathState( + ImmutableDictionary lrOffsets, + int? ctrOffset, + int? lrReturnOffset, + int spDelta, + ImmutableDictionary stackOffsets) + { + LrOffsets = lrOffsets; + CtrOffset = ctrOffset; + LrReturnOffset = lrReturnOffset; + SpDelta = spDelta; + StackOffsets = stackOffsets; + } + + public static readonly PathState Empty = new( + ImmutableDictionary.Empty.WithComparers(StringComparer.OrdinalIgnoreCase), + null, + 0, + 0, + ImmutableDictionary.Empty); + + public PathState WithLrOffset(string register, int offset) => + LrOffsets.TryGetValue(register, out var cur) && cur == offset + ? this + : new(LrOffsets.SetItem(register, offset), CtrOffset, LrReturnOffset, SpDelta, StackOffsets); + + public PathState WithoutLrOffset(string register) => + LrOffsets.ContainsKey(register) + ? new(LrOffsets.Remove(register), CtrOffset, LrReturnOffset, SpDelta, StackOffsets) + : this; + + public PathState WithCtrOffset(int? ctrOffset) => + ctrOffset == CtrOffset + ? this + : new(LrOffsets, ctrOffset, LrReturnOffset, SpDelta, StackOffsets); + + public PathState WithLrReturnOffset(int? lrReturnOffset) => + lrReturnOffset == LrReturnOffset + ? this + : new(LrOffsets, CtrOffset, lrReturnOffset, SpDelta, StackOffsets); + + public PathState WithSpDelta(int spDelta) => + spDelta == SpDelta + ? this + : new(LrOffsets, CtrOffset, LrReturnOffset, spDelta, StackOffsets); + + public PathState WithAdjustedStackPointer(int displacement) + { + // r1 can hold an LR-relative address too. Update both relations; + // guest address arithmetic wraps at 32 bits. + var updated = WithSpDelta(unchecked(SpDelta + displacement)); + return LrOffsets.TryGetValue("r1", out var offset) + ? updated.WithLrOffset("r1", unchecked(offset + displacement)) + : updated; + } + + public PathState WithStackOffset(int slot, int offset) => + StackOffsets.TryGetValue(slot, out var cur) && cur == offset + ? this + : new(LrOffsets, CtrOffset, LrReturnOffset, SpDelta, StackOffsets.SetItem(slot, offset)); + + public PathState WithoutStackOffset(int slot) => + StackOffsets.ContainsKey(slot) + ? new(LrOffsets, CtrOffset, LrReturnOffset, SpDelta, StackOffsets.Remove(slot)) + : this; + + public PathState WithoutStackOffsetsInRange(int start, int size) + { + var end = checked(start + size); + var remaining = StackOffsets; + foreach (var slot in StackOffsets.Keys) + { + if (slot < end && start < checked(slot + 4)) + { + remaining = remaining.Remove(slot); + } + } + + return remaining.Count == StackOffsets.Count + ? this + : new(LrOffsets, CtrOffset, LrReturnOffset, SpDelta, remaining); + } + + public PathState WithClearedStackOffsets() => + StackOffsets.IsEmpty + ? this + : new(LrOffsets, CtrOffset, LrReturnOffset, SpDelta, ImmutableDictionary.Empty); + + public bool Equals(PathState? other) + { + if (ReferenceEquals(this, other)) return true; + if (other is null) return false; + if (CtrOffset != other.CtrOffset || LrReturnOffset != other.LrReturnOffset || SpDelta != other.SpDelta) return false; + if (LrOffsets.Count != other.LrOffsets.Count || StackOffsets.Count != other.StackOffsets.Count) return false; + foreach (var (k, v) in LrOffsets) + { + if (!other.LrOffsets.TryGetValue(k, out var otherV) || v != otherV) + { + return false; + } + } + foreach (var (k, v) in StackOffsets) + { + if (!other.StackOffsets.TryGetValue(k, out var otherV) || v != otherV) + { + return false; + } + } + return true; + } + + public override bool Equals(object? obj) => obj is PathState other && Equals(other); + + public override int GetHashCode() + { + var hash = new HashCode(); + hash.Add(CtrOffset); + hash.Add(LrReturnOffset); + hash.Add(SpDelta); + hash.Add(LrOffsets.Count); + var regHash = 0; + foreach (var (k, v) in LrOffsets) + { + regHash ^= HashCode.Combine(StringComparer.OrdinalIgnoreCase.GetHashCode(k), v); + } + hash.Add(regHash); + hash.Add(StackOffsets.Count); + var stackHash = 0; + foreach (var (k, v) in StackOffsets) + { + stackHash ^= HashCode.Combine(k, v); + } + hash.Add(stackHash); + return hash.ToHashCode(); + } + } } diff --git a/translator/tests/Translator.Tests/ContinuationPlannerTests.cs b/translator/tests/Translator.Tests/ContinuationPlannerTests.cs index efa2c51..08aae9a 100644 --- a/translator/tests/Translator.Tests/ContinuationPlannerTests.cs +++ b/translator/tests/Translator.Tests/ContinuationPlannerTests.cs @@ -1,4 +1,6 @@ using System.Buffers.Binary; +using System.Linq; +using Translator.Core.Disassembly; using Translator.Core.Mods; using Translator.Core.Mods.Mkwii; using Translator.Core.Parsing.Kamek; @@ -128,6 +130,54 @@ public class ContinuationPlannerTests Assert.Contains("Retro WFC executable hook continuation", entry.Reason); } + [Fact] + public void DiscoverLrRelativeIndirectJumpOffsets_DiscoversSkipReturnOffset() + { + var instructions = new[] + { + PpcDecoder.Decode(0x8180D8E8, 0x7FE802A6u), // mflr r31 + PpcDecoder.Decode(0x8180D8EC, 0x3BFF0014u), // addi r31, r31, 20 + PpcDecoder.Decode(0x8180D8F0, 0x7FE803A6u), // mtlr r31 + PpcDecoder.Decode(0x8180D8F4, 0x4E800020u), // blr + }; + + var offsets = ContinuationPlanner.DiscoverLrRelativeIndirectJumpOffsets(instructions).ToArray(); + var offset = Assert.Single(offsets); + Assert.Equal(20, offset); + } + + [Fact] + public void DiscoverLrRelativeIndirectJumpOffsets_IgnoresStandardLrRestore() + { + var instructions = new[] + { + PpcDecoder.Decode(0x8180D8E8, 0x7FE802A6u), // mflr r31 + PpcDecoder.Decode(0x8180D8EC, 0x93E10008u), // stw r31, 8(r1) + PpcDecoder.Decode(0x8180D8F0, 0x83E10008u), // lwz r31, 8(r1) + PpcDecoder.Decode(0x8180D8F4, 0x7FE803A6u), // mtlr r31 + PpcDecoder.Decode(0x8180D8F8, 0x4E800020u), // blr + }; + + var offsets = ContinuationPlanner.DiscoverLrRelativeIndirectJumpOffsets(instructions); + Assert.Empty(offsets); + } + + [Fact] + public void DiscoverLrRelativeIndirectJumpOffsets_SupportsBctrOffset() + { + var instructions = new[] + { + PpcDecoder.Decode(0x8180D8E8, 0x7FE802A6u), // mflr r31 + PpcDecoder.Decode(0x8180D8EC, 0x397F0008u), // addi r11, r31, 8 + PpcDecoder.Decode(0x8180D8F0, 0x7D6903A6u), // mtctr r11 + PpcDecoder.Decode(0x8180D8F4, 0x4E800420u), // bctr + }; + + var offsets = ContinuationPlanner.DiscoverLrRelativeIndirectJumpOffsets(instructions).ToArray(); + var offset = Assert.Single(offsets); + Assert.Equal(8, offset); + } + private static KamekChunk EmptyChunk() => new( 0, diff --git a/translator/tests/Translator.Tests/LrContinuationCodeGenTests.cs b/translator/tests/Translator.Tests/LrContinuationCodeGenTests.cs new file mode 100644 index 0000000..c11bff6 --- /dev/null +++ b/translator/tests/Translator.Tests/LrContinuationCodeGenTests.cs @@ -0,0 +1,63 @@ +using Translator.Core.Analysis.Ssa; +using Translator.Core.Analysis.Representation; +using Translator.Core.CodeGen; +using Translator.Core.Ir; +using Translator.Core.Representation; +using Xunit; + +namespace Translator.Tests; + +// This binary-free code-generation regression must run in the default suite. +public class LrContinuationCodeGenTests +{ + [Fact] + public void CodeGenerator_DispatchesGuestCallLrContinuationWithoutMarkingTargetNonReturning() + { + var function = new IrFunction( + "lr_continuation_call", + "0x800E591C", + new[] + { + new IrBasicBlock("0x800E591C", new IrInstruction[] + { + new IrAssign("lr", IrValue.Imm(unchecked((int)0x800E5920u))), + new IrCall(string.Empty, "0x8179AC3C", Array.Empty()), + new IrAssign("r3", IrValue.Imm(8)), + new IrReturn(null) + }), + new IrBasicBlock("0x800E5934", new IrInstruction[] + { + new IrAssign("r3", IrValue.Imm(1)), + new IrReturn(null) + }) + }); + + var types = new RepresentationEnvironment(new Dictionary + { + ["lr"] = ValueRepresentation.UInt32, + ["r3"] = ValueRepresentation.UInt32 + }); + var signature = new FunctionAbiClassification("lr_continuation_call", ValueRepresentation.Void); + + var ssa = new SsaTransformer().Convert(function); + var code = new CxxLinearCodeGenerator().Emit( + 0x800E591C, + ssa, + signature, + types, + lrContinuationCallTargets: new HashSet { 0x8179AC3Cu }); + + var callIndex = code.IndexOf("InvokeDirectCpu<0x8179AC3Cu>(ctx);", StringComparison.Ordinal); + var fallthroughGuardIndex = code.IndexOf("if (ctx->lr != 0x800E5920u)", callIndex, StringComparison.Ordinal); + var localCaseIndex = code.IndexOf("case 0x800E5934u:", fallthroughGuardIndex, StringComparison.Ordinal); + var returnIndex = code.IndexOf("return;", localCaseIndex, StringComparison.Ordinal); + var fallthroughAssignmentIndex = code.IndexOf("r3 = 8;", callIndex, StringComparison.Ordinal); + + Assert.True(callIndex >= 0); + Assert.True(fallthroughGuardIndex > callIndex); + Assert.True(localCaseIndex > fallthroughGuardIndex); + Assert.True(returnIndex > localCaseIndex); + Assert.True(fallthroughAssignmentIndex > returnIndex, code); + Assert.Contains("goto loc_800E5934;", code); + } +} diff --git a/translator/tests/Translator.Tests/LrRelativeContinuationTests.cs b/translator/tests/Translator.Tests/LrRelativeContinuationTests.cs new file mode 100644 index 0000000..ec58376 --- /dev/null +++ b/translator/tests/Translator.Tests/LrRelativeContinuationTests.cs @@ -0,0 +1,448 @@ +using System.Buffers.Binary; +using Translator.Core.Disassembly; +using Translator.Core.Loading; +using Translator.Core.Mods; +using Xunit; + +namespace Translator.Tests; + +public class LrRelativeContinuationTests +{ + [Theory] + [InlineData(20, 40)] + [InlineData(40, 20)] + public void MutuallyExclusiveAdjustmentsKeepBothOffsets(int firstOffset, int secondOffset) + { + // Both arms start with the incoming LR and join at mtlr. Adding the + // offsets together invents a continuation that neither arm can reach. + var offsets = DiscoverOffsets( + 0x7FE802A6u, // +00: mflr r31 + 0x2C030000u, // +04: cmpwi r3,0 + 0x4182000Cu, // +08: beq +0x14 + AddiR31(firstOffset), // +0C: addi r31,r31,firstOffset + 0x48000008u, // +10: b +0x18 + AddiR31(secondOffset), // +14: addi r31,r31,secondOffset + 0x7FE803A6u, // +18: mtlr r31 + 0x4E800020u);// +1C: blr + + Assert.Equal(new[] { 20, 40 }, offsets); + } + + [Fact] + public void NormalReturnArmDoesNotEraseSkipReturnAtSharedBlr() + { + // The normal arm writes the original LR; it must not overwrite the + // other arm's LR + 20 in the analysis of the shared return. + var offsets = DiscoverOffsets( + 0x7FE802A6u, // +00: mflr r31 + 0x2C030000u, // +04: cmpwi r3,0 + 0x41820010u, // +08: beq +0x18 + 0x397F0014u, // +0C: addi r11,r31,20 + 0x7D6803A6u, // +10: mtlr r11 + 0x48000008u, // +14: b +0x1C + 0x7FE803A6u, // +18: mtlr r31 + 0x4E800020u);// +1C: blr + + Assert.Equal(new[] { 20 }, offsets); + } + + [Fact] + public void ConditionalNormalReturnStillDiscoversSkipOnFallthrough() + { + var offsets = DiscoverOffsets( + 0x7FE802A6u, // mflr r31 + 0x2C030000u, // cmpwi r3,0 + 0x4D820020u, // beqlr + 0x3BFF0014u, // addi r31,r31,20 + 0x7FE803A6u, // mtlr r31 + 0x4E800020u);// blr + + Assert.Equal(new[] { 20 }, offsets); + } + + [Fact] + public void SavedNonvolatileLrSurvivesHelperCall() + { + // r31 survives a normal ABI call even though the call replaces LR. + var offsets = DiscoverOffsets( + 0x7FE802A6u, // mflr r31 + 0x48000101u, // bl helper outside this function + 0x3BFF0014u, // addi r31,r31,20 + 0x7FE803A6u, // mtlr r31 + 0x4E800020u);// blr + + Assert.Equal(new[] { 20 }, offsets); + } + + [Fact] + public void ReloadingSavedRegisterAfterMtlrDoesNotEraseSkipReturn() + { + // A hook epilogue restores the caller's r31 after committing its + // adjusted return address to LR. + var offsets = DiscoverOffsets( + 0x7FE802A6u, // mflr r31 + 0x3BFF0014u, // addi r31,r31,20 + 0x7FE803A6u, // mtlr r31 + 0x83E10008u, // lwz r31,8(r1) + 0x4E800020u);// blr + + Assert.Equal(new[] { 20 }, offsets); + } + + [Fact] + public void UnknownLrWriteReplacesEarlierSkipReturn() + { + var offsets = DiscoverOffsets( + 0x7FE802A6u, // mflr r31 + 0x3BFF0014u, // addi r31,r31,20 + 0x7FE803A6u, // mtlr r31 + 0x80010008u, // lwz r0,8(r1) + 0x7C0803A6u, // mtlr r0 + 0x4E800020u);// blr + + Assert.Empty(offsets); + } + + [Fact] + public void UnadjustedRegisterReturnDoesNotAddAContinuation() + { + var offsets = DiscoverOffsets( + 0x7FE802A6u, // mflr r31 + 0x7FE803A6u, // mtlr r31 + 0x4E800020u);// blr + + Assert.Empty(offsets); + } + + [Fact] + public void LoadMultipleWordOverwritesSavedRegistersThroughR31() + { + var offsets = DiscoverOffsets( + 0x7FE802A6u, // mflr r31 + 0x3BFF0014u, // addi r31,r31,20 + 0xBB610008u, // lmw r30,8(r1) + 0x7FE803A6u, // mtlr r31 + 0x4E800020u);// blr + + Assert.Empty(offsets); + } + + [Fact] + public void BlrlCallIsNotTreatedAsReturn() + { + var offsets = DiscoverOffsets( + 0x7FE802A6u, // mflr r31 + 0x4E800021u, // blrl + 0x3BFF0014u, // addi r31,r31,20 + 0x7FE803A6u, // mtlr r31 + 0x4E800020u);// blr + + Assert.Equal(new[] { 20 }, offsets); + } + + [Fact] + public void MflrAfterCallDoesNotTreatClobberedLrAsIncomingLr() + { + var offsets = DiscoverOffsets( + 0x48000101u, // bl helper + 0x7FE802A6u, // mflr r31 + 0x3BFF0014u, // addi r31,r31,20 + 0x7FE803A6u, // mtlr r31 + 0x4E800020u);// blr + + Assert.Empty(offsets); + } + + [Fact] + public void RestoredIncomingLrBeforeCtrSkipStillDiscoversOffset() + { + // The helper replaces LR, but the stack save/restore recovers the + // incoming LR before the hook jumps to the caller's continuation. + var offsets = DiscoverOffsets( + 0x7C0802A6u, // mflr r0 + 0x90010004u, // stw r0,4(r1) + 0x9421FFF0u, // stwu r1,-16(r1) + 0x48000101u, // bl helper outside this function + 0x38210010u, // addi r1,r1,16 + 0x80010004u, // lwz r0,4(r1) + 0x7C0803A6u, // mtlr r0 + 0x7D6802A6u, // mflr r11 + 0x396B0008u, // addi r11,r11,8 + 0x7D6903A6u, // mtctr r11 + 0x4E800420u);// bctr + + Assert.Equal(new[] { 8 }, offsets); + } + + [Fact] + public void BoundedLoopBeforeCtrSkipStillDiscoversOffset() + { + // Updating an LR-derived register in a two-iteration loop must not + // starve analysis of the exit, whose target uses unchanged r31. + var offsets = DiscoverOffsets( + 0x7FE802A6u, // mflr r31 + 0x7FC802A6u, // mflr r30 + 0x38600002u, // li r3,2 + 0x7C6903A6u, // mtctr r3 + 0x3BDE0004u, // addi r30,r30,4 + 0x4200FFFCu, // bdnz -4 + 0x397F0008u, // addi r11,r31,8 + 0x7D6903A6u, // mtctr r11 + 0x4E800420u);// bctr + + Assert.Equal(new[] { 8 }, offsets); + } + + [Fact] + public void UntrackedR1WriteInvalidatesStackTracking() + { + // If r1 is overwritten from an untracked source, previously saved stack slots + // must not be used to recover LR state. + var offsets = DiscoverOffsets( + 0x7FE802A6u, // mflr r31 + 0x3BFF0014u, // addi r31,r31,20 + 0x93E10008u, // stw r31,8(r1) + 0x80230000u, // lwz r1,0(r3) + 0x80010008u, // lwz r0,8(r1) + 0x7C0803A6u, // mtlr r0 + 0x4E800020u);// blr + + Assert.Empty(offsets); + } + + [Fact] + public void LargeStraightLineHandlerStillDiscoversSkipReturn() + { + // The analyzer's global step budget is spent one step per (instruction, + // state) pair, so a long enough handler exhausts it before reaching the + // return and silently reports no continuation at all. Main's linear + // scanner had no budget and always found the offset. + var words = new List + { + 0x7FE802A6u, // mflr r31 + 0x3BFF0014u // addi r31,r31,20 + }; + for (var i = 0; i < 10_010; i++) + { + words.Add(0x60000000u); // nop + } + words.Add(0x7FE803A6u); // mtlr r31 + words.Add(0x4E800020u); // blr + + Assert.Equal(new[] { 20 }, DiscoverOffsets(words.ToArray())); + } + + [Fact] + public void LargeBranchingHandlerStillDiscoversCtrSkip() + { + // Same budget, reached far sooner once the handler branches: this is the + // bctr shape the pre-PR scanner discovered at any function size. + var words = new List { 0x7FE802A6u }; // mflr r31 + for (var i = 0; i < 160; i++) + { + var displacement = (uint)((i + 1) * 4 & 0xFFFF); + words.Add(0x2C030000u); // cmpwi r3,0 + words.Add(0x4182000Cu); // beq +0xC + words.Add(0x3BDF0000u | displacement); // addi r30,r31,disp + words.Add(0x48000008u); // b +8 + words.Add(0x3BBF0000u | displacement); // addi r29,r31,disp + } + words.Add(0x397F0008u); // addi r11,r31,8 + words.Add(0x7D6903A6u); // mtctr r11 + words.Add(0x4E800420u); // bctr + + Assert.Equal(new[] { 8 }, DiscoverOffsets(words.ToArray())); + } + + [Fact] + public void FloatStoreOverSavedSlotInvalidatesStackTracking() + { + // stfd writes 0x10..0x17, which covers the slot the adjusted LR was + // saved to. Only stw/stwu invalidate slots today, so the reload is + // credited with a return address the stack no longer holds. + var offsets = DiscoverOffsets( + 0x7FE802A6u, // mflr r31 + 0x3BFF0014u, // addi r31,r31,20 + 0x93E10014u, // stw r31,0x14(r1) + 0xD8410010u, // stfd f2,0x10(r1) + 0x80010014u, // lwz r0,0x14(r1) + 0x7C0803A6u, // mtlr r0 + 0x4E800020u);// blr + + Assert.Empty(offsets); + } + + [Fact] + public void StoreMultipleOverSavedSlotInvalidatesStackTracking() + { + var offsets = DiscoverOffsets( + 0x7FE802A6u, // mflr r31 + 0x3BFF0014u, // addi r31,r31,20 + 0x93E10008u, // stw r31,8(r1) + 0xBFC10008u, // stmw r30,8(r1) + 0x80010008u, // lwz r0,8(r1) + 0x7C0803A6u, // mtlr r0 + 0x4E800020u);// blr + + Assert.Empty(offsets); + } + + [Fact] + public void StackPointerUpdatePreservesAdjustedLrOffset() + { + var offsets = DiscoverOffsets( + 0x7C2802A6u, // mflr r1 + 0xDC410004u, // stfdu f2,4(r1) + 0x7C2803A6u, // mtlr r1 + 0x4E800020u);// blr + + Assert.Equal(new[] { 4 }, offsets); + } + + [Fact] + public void VolatileRegisterDoesNotSurviveHelperCall() + { + // r3 is caller-saved, so the callee is free to destroy the adjusted + // return address this hook staged before the call. + var offsets = DiscoverOffsets( + 0x7C6802A6u, // mflr r3 + 0x38630014u, // addi r3,r3,20 + 0x48000101u, // bl helper outside this function + 0x7C6803A6u, // mtlr r3 + 0x4E800020u);// blr + + Assert.Empty(offsets); + } + + [Fact] + public void CtrDoesNotSurviveHelperCall() + { + // CTR is volatile across a call for the same reason. + var offsets = DiscoverOffsets( + 0x7FE802A6u, // mflr r31 + 0x3BFF0014u, // addi r31,r31,20 + 0x7FE903A6u, // mtctr r31 + 0x48000101u, // bl helper outside this function + 0x4E800420u);// bctr + + Assert.Empty(offsets); + } + + [Fact] + public void MflrR1InvalidatesOldStackSlots() + { + // After mflr r1, 8(r1) refers to incoming LR + 8, not the old + // stack slot. Its contents are unknown; do not invent a +20 return. + Assert.Empty(DiscoverOffsets( + 0x7FE802A6u, // mflr r31 + 0x3BFF0014u, // addi r31,r31,20 + 0x93E10008u, // stw r31,8(r1) + 0x7C2802A6u, // mflr r1 + 0x80010008u, // lwz r0,8(r1) + 0x7C0803A6u, // mtlr r0 + 0x4E800020u)); // blr + } + + [Fact] + public void AddiR1UpdatesLrRelativeOffset() + { + // Like StackPointerUpdatePreservesAdjustedLrOffset, r1 holds incoming + // LR here. Updating r1 must update that relation as well as stack state. + Assert.Equal(new[] { 4 }, DiscoverOffsets( + 0x7C2802A6u, // mflr r1 + 0x38210004u, // addi r1,r1,4 + 0x7C2803A6u, // mtlr r1 + 0x4E800020u)); // blr + } + + [Theory] + [InlineData(0x38210004u, 4)] // addi r1,r1,4 + [InlineData(0x30210004u, 4)] // addic r1,r1,4 + [InlineData(0x94210004u, 4)] // stwu r1,4(r1) + [InlineData(0xD4410004u, 4)] // stfsu f2,4(r1) + [InlineData(0xDC410004u, 4)] // stfdu f2,4(r1) + [InlineData(0x3821FFFCu, -4)] // addi r1,r1,-4 + public void StackPointerUpdatesPreserveLrRelation(uint update, int expectedOffset) + { + Assert.Equal(new[] { expectedOffset }, DiscoverOffsets( + 0x7C2802A6u, // mflr r1 + update, + 0x7C2803A6u, // mtlr r1 + 0x4E800020u)); // blr + } + + [Theory] + [InlineData(0x7FE1FB78u)] // mr r1,r31 + [InlineData(0x383F0000u)] // addi r1,r31,0 + public void CopyingLrIntoR1PreservesReturnButInvalidatesOldStack(uint copy) + { + var prefix = new uint[] + { + 0x7FE802A6u, // mflr r31 + 0x3BFF0014u, // addi r31,r31,20 + 0x93E10008u, // stw r31,8(r1) + copy, + }; + Assert.Equal(new[] { 20 }, DiscoverOffsets( + prefix.Concat(new uint[] { 0x7C2803A6u, 0x4E800020u }).ToArray())); + Assert.Empty(DiscoverOffsets(prefix.Concat(new uint[] + { + 0x80010008u, // lwz r0,8(r1): no longer the old stack slot + 0x7C0803A6u, // mtlr r0 + 0x4E800020u, + }).ToArray())); + } + + [Fact] + public void StackPointerSelfMovePreservesSavedLr() + { + Assert.Equal(new[] { 20 }, DiscoverOffsets( + 0x7FE802A6u, // mflr r31 + 0x3BFF0014u, // addi r31,r31,20 + 0x93E10008u, // stw r31,8(r1) + 0x7C210B78u, // mr r1,r1 + 0x80010008u, // lwz r0,8(r1) + 0x7C0803A6u, // mtlr r0 + 0x4E800020u)); + } + + [Fact] + public void IncompleteInstructionListDoesNotInventFallthroughAcrossGap() + { + // Defensive incomplete-input test, not a production disassembly trace: + // the missing instruction could overwrite r31 or branch elsewhere. + // Address sorting alone does not establish a fallthrough edge. + var instructions = new[] + { + PpcDecoder.Decode(0x81800000u, 0x7FE802A6u), // mflr r31 + PpcDecoder.Decode(0x81800008u, 0x3BFF0014u), // addi r31,r31,20 + PpcDecoder.Decode(0x8180000Cu, 0x7FE803A6u), // mtlr r31 + PpcDecoder.Decode(0x81800010u, 0x4E800020u), // blr + }; + + Assert.Empty(ContinuationPlanner.DiscoverLrRelativeIndirectJumpOffsets(instructions)); + } + + private static uint AddiR31(int offset) => 0x3BFF0000u | (uint)(offset & 0xFFFF); + + private static int[] DiscoverOffsets(params uint[] words) + { + const uint entry = 0x81800000u; + var memory = new byte[words.Length * 4]; + for (var i = 0; i < words.Length; i++) + { + BinaryPrimitives.WriteUInt32BigEndian(memory.AsSpan(i * 4, 4), words[i]); + } + + var range = AddressRange.FromStartAndSize(entry, (uint)memory.Length); + var image = new ProgramImage(memory, range, range, default, "lr-continuation-test", entry); + using var disassembler = new PpcDisassembler(); + // Use the production reachable-instruction traversal and ordering, + // rather than handing the planner an artificial execution trace. + var instructions = disassembler.DisassembleFunction( + image, entry, maxInstructions: words.Length + 1, maxBytes: memory.Length); + + return ContinuationPlanner.DiscoverLrRelativeIndirectJumpOffsets(instructions) + .Distinct().OrderBy(offset => offset).ToArray(); + } +}