Files
Michael G 25c69ae28e 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>
2026-09-10 15:51:16 +02:00

212 lines
7.5 KiB
C#

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;
using Xunit;
namespace Translator.Tests;
public class ContinuationPlannerTests
{
[Fact]
public void AddModuleTailJumpContinuationsAddsInternalBaseTarget()
{
const uint moduleBase = 0x81200000u;
var moduleImage = new byte[0x20];
WriteU32(moduleImage, 0x00, 0x3D80807Eu); // lis r12,0x807E
WriteU32(moduleImage, 0x04, 0x618C3064u); // ori r12,r12,0x3064
WriteU32(moduleImage, 0x08, 0x7D8903A6u); // mtctr r12
WriteU32(moduleImage, 0x0C, 0x4E800420u); // bctr
var plan = ContinuationPlanner.Build(EmptyChunk(), TestManifest(), moduleBase);
var result = ContinuationPlanner.AddModuleTailJumpContinuations(
plan,
TestManifest(),
moduleBase,
moduleImage);
var entry = Assert.Single(result.Entries);
Assert.Equal(0x807E3064u, entry.Address);
Assert.Equal(0x807E2D18u, entry.ContainingFunctionStart);
Assert.Equal(0x807E30ACu, entry.ContainingFunctionEnd);
Assert.Equal(0x8120000Cu, entry.SourceCommandAddress);
Assert.Contains("tail jump", entry.Reason);
}
[Fact]
public void AddModuleTailJumpContinuationsSupportsSignedAddiLowImmediate()
{
const uint moduleBase = 0x81200000u;
var moduleImage = new byte[0x20];
WriteU32(moduleImage, 0x00, 0x3D80807Fu); // lis r12,0x807F
WriteU32(moduleImage, 0x04, 0x398CFFF0u); // addi r12,r12,-0x10
WriteU32(moduleImage, 0x08, 0x7D8903A6u); // mtctr r12
WriteU32(moduleImage, 0x0C, 0x4E800420u); // bctr
var manifest = new BaseManifest(
"test",
1,
"RMCP01",
"P",
"",
0,
[
new BaseSectionMetadata(".text", "StaticR.rel", 0x807EFFF0u, 0x807F0100u, true, false, "base_text.bin", 0)
],
[
new BaseFunctionRangeMetadata(0x807EFFE0u, 0x807F0100u, "func_807EFFE0", ".text", 0, "test", ["Executable"])
],
"ranges.json");
var plan = ContinuationPlanner.Build(EmptyChunk(), manifest, moduleBase);
var result = ContinuationPlanner.AddModuleTailJumpContinuations(plan, manifest, moduleBase, moduleImage);
var entry = Assert.Single(result.Entries);
Assert.Equal(0x807EFFF0u, entry.Address);
Assert.Equal(0x807EFFE0u, entry.ContainingFunctionStart);
}
[Fact]
public void AddModuleTailJumpContinuationsIgnoresFunctionEntries()
{
const uint moduleBase = 0x81200000u;
var moduleImage = new byte[0x20];
WriteU32(moduleImage, 0x00, 0x3D80807Eu); // lis r12,0x807E
WriteU32(moduleImage, 0x04, 0x618C2D18u); // ori r12,r12,0x2D18
WriteU32(moduleImage, 0x08, 0x7D8903A6u); // mtctr r12
WriteU32(moduleImage, 0x0C, 0x4E800420u); // bctr
var manifest = TestManifest();
var plan = ContinuationPlanner.Build(EmptyChunk(), manifest, moduleBase);
var result = ContinuationPlanner.AddModuleTailJumpContinuations(plan, manifest, moduleBase, moduleImage);
Assert.Empty(result.Entries);
}
[Fact]
public void AddRetroWfcExecutableHookContinuationsAddsInternalBaseTarget()
{
var manifest = new BaseManifest(
"test",
1,
"RMCP01",
"P",
"",
0,
[
new BaseSectionMetadata(".text", "main.dol", 0x800F164Cu, 0x800F1BC8u, true, false, "base_text.bin", 0)
],
[
new BaseFunctionRangeMetadata(0x800F164Cu, 0x800F1BC8u, "func_800F164C", ".text", 0, "test", ["Executable"])
],
"ranges.json");
var hook = new RetroWfcExecutableHookPlan(
5,
"executableHookWithContinuation",
"branchCtr",
0x800F164Cu,
4,
0x800F164Cu,
0x800F1BC8u,
".text",
["support.hostnameRewrite.gethostbyname"],
"overlayHookPatch",
0x8179E1A4u,
"support.hostnameRewrite.gethostbyname",
"moduleFunction",
null,
0x800F165Cu);
var plan = ContinuationPlanner.Build(EmptyChunk(), manifest, 0x81200000u);
var result = ContinuationPlanner.AddRetroWfcExecutableHookContinuations(plan, manifest, [hook]);
var entry = Assert.Single(result.Entries);
Assert.Equal(0x800F165Cu, entry.Address);
Assert.Equal(0x800F164Cu, entry.ContainingFunctionStart);
Assert.Equal(0x800F1BC8u, entry.ContainingFunctionEnd);
Assert.Equal(0x800F164Cu, entry.SourceCommandAddress);
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,
0,
0,
0,
0,
0,
KamekChunk.HeaderSize,
[],
[]);
private static BaseManifest TestManifest() =>
new(
"test",
1,
"RMCP01",
"P",
"",
0,
[
new BaseSectionMetadata(".text", "StaticR.rel", 0x807E2D18u, 0x807E30ACu, true, false, "base_text.bin", 0)
],
[
new BaseFunctionRangeMetadata(0x807E2D18u, 0x807E30ACu, "func_807E2D18", ".text", 0, "test", ["Executable"])
],
"ranges.json");
private static void WriteU32(byte[] data, int offset, uint value) =>
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(offset, 4), value);
}