mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 05:00:27 +02:00
Merge upstream/main (v0.2.32) into openxr-work
Brings in the keyboard/mouse rebinding overhaul (#162), the Kamek skip-return hook fixes (#182, #218), the exit button and controller LED fix (#221), the autohide-cursor and mute hotkey fix (#211), the Linux --sysroot plumbing (#224) and the switch to the theofficialgman dawn-build fork (#215). Conflicts resolved to keep the VR integration intact: - settings_overlay.cpp/.h: kept both new declarations. The controller rebinding UI takes upstream's click-to-rebind widgets wholesale - our only edit there was wrapping the combo width in Scaled(), and upstream's bindingWidth is already font-relative, so the headset panel still scales. Kept our DrawResolutionMenu() extraction (the VR panel reuses it) while adopting upstream's DrawExitPrompt() and its new DrawTopBar() prologue; kept our Diagnostics menu alongside upstream's exit-button width math. HandleEvents merges both keyboard paths, with the VR recenter hotkey now guarded by !g_rebind.active so it cannot fire while a binding is being captured. - AuroraDawnProvider.cmake: dropped our now-dead Android hash block. Upstream restructured the pins into an if/elseif chain that already covers android/aarch64, with the digest for the new dawn-build fork; our leftover block was unreachable and carried the old encounter digest. - Version plumbing (Build-Installer.ps1, Setup.Windows Program.cs and csproj): kept this fork's own line, which is 0.2.39 and centralised in Launcher/Directory.Build.props, rather than regressing to upstream's hardcoded 0.2.32. Verified: translator 654/654; runtime ctest 14/14 including every VR test; WiiCompiled and RetroRewind link; aurora gx_fifo_tests 262/263, the one failure being the TevRegisterLiveness case already documented as pre-existing on this branch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
commit
ee5df403ac
29 files changed
+2280
-263
No files matched your search
@@ -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,
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Text.Json;
|
||||
using Translator.Cli.Configuration;
|
||||
using Translator.Core.Build;
|
||||
using Translator.Core.Mods;
|
||||
using Translator.Core.Parsing.Kamek;
|
||||
|
||||
namespace Translator.Tests;
|
||||
|
||||
public sealed class KamekLrContinuationIntegrationTests
|
||||
{
|
||||
private const uint Caller = 0x80004000;
|
||||
private const uint Module = 0x80010000;
|
||||
private const uint Hook = Module + 0x40;
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(1)]
|
||||
[InlineData(2)]
|
||||
public void SkipReturnThroughKnownTailCallsRetainsCallerDispatch(int tailDepth)
|
||||
{
|
||||
var bundle = Translate(tailDepth, SkipReturn(20));
|
||||
AssertResumeDispatch(Source(bundle, Caller), Caller, Caller + 24);
|
||||
Assert.Contains(bundle.Entries, entry => entry.EntryPoint == Caller + 24 &&
|
||||
entry.VirtualPath.Contains("rr_continue_"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(1)]
|
||||
[InlineData(2)]
|
||||
public void OrdinaryReturnsThroughKnownTailCallsStayLightweight(int tailDepth)
|
||||
{
|
||||
var bundle = Translate(tailDepth, [0x38630001u, 0x4E800020u]); // addi r3,r3,1; blr
|
||||
var caller = Source(bundle, Caller);
|
||||
Assert.DoesNotContain("switch (ctx->lr)", caller);
|
||||
Assert.DoesNotContain("if (ctx->lr !=", caller);
|
||||
Assert.DoesNotContain(bundle.Entries, entry => entry.VirtualPath.Contains("rr_continue_"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DirectSkipReturnRegistersTheAdjustedBaseAddress()
|
||||
{
|
||||
var bundle = Translate(0, SkipReturn(20));
|
||||
AssertResumeDispatch(Source(bundle, Caller), Caller, Caller + 24);
|
||||
Assert.Contains(bundle.Entries, entry => entry.EntryPoint == Caller + 24 &&
|
||||
entry.VirtualPath.Contains("rr_continue_"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CtrSkipRetainsDispatchAndRegistersItsContinuation()
|
||||
{
|
||||
var bundle = Translate(0,
|
||||
[
|
||||
0x7D8802A6u, // mflr r12
|
||||
0x398C0014u, // addi r12,r12,20
|
||||
0x7D8903A6u, // mtctr r12
|
||||
0x4E800420u // bctr
|
||||
]);
|
||||
AssertResumeDispatch(Source(bundle, Caller), Caller, Caller + 24);
|
||||
Assert.Contains(bundle.Entries, entry => entry.EntryPoint == Caller + 24 &&
|
||||
entry.VirtualPath.Contains("rr_continue_"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalReturnArmDoesNotHideTailCalledSkipReturn()
|
||||
{
|
||||
var bundle = Translate(1, SkipReturn(20), conditionalWrapper: true);
|
||||
AssertResumeDispatch(Source(bundle, Caller), Caller, Caller + 24);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(1)]
|
||||
public void SharedHookRetainsResumeDispatchAtEveryCallSite(int tailDepth)
|
||||
{
|
||||
var bundle = Translate(tailDepth, SkipReturn(20), sharedTarget: true);
|
||||
AssertResumeDispatch(Source(bundle, Caller), Caller, Caller + 24);
|
||||
AssertResumeDispatch(Source(bundle, Caller + 0x40), Caller + 0x40, Caller + 0x58);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(1)]
|
||||
public void SkipReturnHandlingDoesNotSpreadToAnOrdinaryPatchedCaller(int tailDepth)
|
||||
{
|
||||
var bundle = Translate(tailDepth, SkipReturn(20), companionBody: [0x38630001u, 0x4E800020u]);
|
||||
var ordinaryCaller = Source(bundle, Caller + 0x40);
|
||||
Assert.DoesNotContain("switch (ctx->lr)", ordinaryCaller);
|
||||
Assert.DoesNotContain("if (ctx->lr !=", ordinaryCaller);
|
||||
AssertResumeDispatch(Source(bundle, Caller), Caller, Caller + 24);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(20)]
|
||||
public void SavedAndRestoredLrDistinguishesOrdinaryAndSkipReturns(int offset)
|
||||
{
|
||||
var bundle = Translate(0,
|
||||
[
|
||||
0x7D8802A6u, // mflr r12
|
||||
0x91810004u, // stw r12,4(r1)
|
||||
0x81810004u, // lwz r12,4(r1)
|
||||
0x398C0000u | (ushort)offset, // addi r12,r12,offset
|
||||
0x7D8803A6u, // mtlr r12
|
||||
0x4E800020u // blr
|
||||
]);
|
||||
var caller = Source(bundle, Caller);
|
||||
if (offset == 0)
|
||||
Assert.DoesNotContain("if (ctx->lr !=", caller);
|
||||
else
|
||||
AssertResumeDispatch(caller, Caller, Caller + 4 + (uint)offset);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CappedAnalysisDoesNotClassifyAHookAsAnOrdinaryReturn()
|
||||
{
|
||||
var bundle = Translate(0, SyntheticLrHookFactory.ManyOrdinaryReturnPaths());
|
||||
AssertResumeDispatch(Source(bundle, Caller), Caller, Caller + 24);
|
||||
Assert.Contains(bundle.Entries, entry => entry.EntryPoint == Caller + 24 &&
|
||||
entry.VirtualPath.Contains("rr_continue_"));
|
||||
}
|
||||
|
||||
private static void AssertResumeDispatch(string source, uint callSite, uint continuation)
|
||||
{
|
||||
var call = source.IndexOf($"InvokeDirectCpu<0x{Hook:X8}u>(ctx);", StringComparison.Ordinal);
|
||||
Assert.True(call >= 0, source);
|
||||
var guard = source.IndexOf($"if (ctx->lr != 0x{callSite + 4:X8}u)", call, StringComparison.Ordinal);
|
||||
Assert.True(guard > call, $"The hook must dispatch its adjusted return address.\n{source}");
|
||||
Assert.Contains($"case 0x{continuation:X8}u:", source[guard..]);
|
||||
Assert.Contains($"goto loc_{continuation:X8};", source[guard..]);
|
||||
Assert.Contains($"loc_{continuation:X8}:", source);
|
||||
}
|
||||
|
||||
private static uint[] SkipReturn(int offset) =>
|
||||
[
|
||||
0x7D8802A6u, // mflr r12
|
||||
0x398C0000u | (ushort)offset, // addi r12,r12,offset
|
||||
0x7D8803A6u, // mtlr r12
|
||||
0x4E800020u // blr
|
||||
];
|
||||
|
||||
private static string Source(TranslationSourceBundle bundle, uint address) =>
|
||||
Assert.Single(bundle.Entries.Where(entry => entry.EntryPoint == address &&
|
||||
entry.VirtualPath.StartsWith("overlays/", StringComparison.Ordinal))).Source;
|
||||
|
||||
private static TranslationSourceBundle Translate(int tailDepth, uint[] body,
|
||||
bool conditionalWrapper = false, bool sharedTarget = false, uint[]? companionBody = null)
|
||||
{
|
||||
var root = Path.Combine(Path.GetTempPath(), $"kamek-lr-integration-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(root);
|
||||
try
|
||||
{
|
||||
uint[] caller = [0x60000000u, 0x38630001u, 0x38630001u, 0x38630001u,
|
||||
0x38630001u, 0x38630001u, 0x60000000u, 0x4E800020u];
|
||||
var hasSecondCaller = sharedTarget || companionBody is not null;
|
||||
var baseWords = Enumerable.Repeat(0x4E800020u, hasSecondCaller ? 24 : 8).ToArray();
|
||||
caller.CopyTo(baseWords, 0);
|
||||
if (hasSecondCaller) caller.CopyTo(baseWords, 16);
|
||||
var baseBytes = Words(baseWords);
|
||||
File.WriteAllBytes(Path.Combine(root, "main.dol"), SyntheticDolFactory.CreateBytes(
|
||||
Caller, sections: [SyntheticDolFactory.Text(0, Caller, baseWords)]));
|
||||
File.WriteAllBytes(Path.Combine(root, "base.bin"), baseBytes);
|
||||
// translate-mod requires a REL; this fixture has no REL code or relocations.
|
||||
File.WriteAllBytes(Path.Combine(root, "empty.rel"), new byte[0x48]);
|
||||
var functions = new List<BaseFunctionRangeMetadata>
|
||||
{
|
||||
new(Caller, Caller + 32, "caller", ".text", 0, "synthetic", ["Executable"])
|
||||
};
|
||||
if (hasSecondCaller)
|
||||
functions.Add(new(Caller + 0x40, Caller + 0x60, "second_caller", ".text", 0x40,
|
||||
"synthetic", ["Executable"]));
|
||||
var manifest = new BaseManifest("synthetic", 1, "TEST01", "P", "", 0,
|
||||
[new BaseSectionMetadata(".text", "main.dol", Caller, Caller + (uint)baseBytes.Length,
|
||||
true, false, "base.bin", 0)],
|
||||
functions, "ranges.json");
|
||||
var manifestPath = Path.Combine(root, "base.json");
|
||||
File.WriteAllText(manifestPath, JsonSerializer.Serialize(manifest));
|
||||
var projectPath = Path.Combine(root, "recomp.yml");
|
||||
Directory.CreateDirectory(Path.Combine(root, "native"));
|
||||
Directory.CreateDirectory(Path.Combine(root, "generated", "functions"));
|
||||
File.WriteAllText(projectPath, """
|
||||
schema_version: 1
|
||||
project:
|
||||
id: kamek-lr-test
|
||||
memory:
|
||||
base: 0x80000000
|
||||
size: 0x00020000
|
||||
sda_base: 0x80002000
|
||||
sda2_base: 0x80003000
|
||||
inputs:
|
||||
dol:
|
||||
path: main.dol
|
||||
rel:
|
||||
path: empty.rel
|
||||
load_address: 0x80008000
|
||||
runtime:
|
||||
native_registration_root: native
|
||||
output:
|
||||
root: generated
|
||||
""");
|
||||
|
||||
var moduleSize = companionBody is null ? 0x40 * (tailDepth + 1) + body.Length * 4
|
||||
: 0x300 + companionBody.Length * 4;
|
||||
var moduleWords = Enumerable.Repeat(0x4E800020u, moduleSize / 4).ToArray();
|
||||
body.CopyTo(moduleWords, (0x40 * (tailDepth + 1)) / 4);
|
||||
var commands = new List<uint> { ((uint)KamekCommandId.BranchLink << 24) | 0x00FFFFFEu, Caller, Hook - Module };
|
||||
if (hasSecondCaller)
|
||||
commands.AddRange([((uint)KamekCommandId.BranchLink << 24) | 0x00FFFFFEu,
|
||||
Caller + 0x40, sharedTarget ? Hook - Module : 0x300u]);
|
||||
companionBody?.CopyTo(moduleWords, 0x300 / 4);
|
||||
for (var depth = 0; depth < tailDepth; depth++)
|
||||
{
|
||||
var offset = 0x40u * (uint)(depth + 1);
|
||||
var branchOffset = offset;
|
||||
if (conditionalWrapper && depth == 0)
|
||||
{
|
||||
moduleWords[offset / 4] = 0x2C030000u; // cmpwi r3,0
|
||||
moduleWords[offset / 4 + 1] = 0x4D820020u; // beqlr
|
||||
branchOffset += 8;
|
||||
}
|
||||
// Explicit Kamek branch targets make every helper a known function boundary.
|
||||
commands.Add(((uint)KamekCommandId.Branch << 24) | branchOffset);
|
||||
commands.Add(offset + 0x40);
|
||||
}
|
||||
var code = Words(moduleWords);
|
||||
var commandBytes = Words(commands.ToArray());
|
||||
var pul = new byte[KamekChunk.HeaderSize + code.Length + commandBytes.Length];
|
||||
Write(pul, 0, KamekChunk.Magic0);
|
||||
Write(pul, 4, KamekChunk.Magic1);
|
||||
Write(pul, 12, (uint)code.Length);
|
||||
Write(pul, 24, (uint)pul.Length);
|
||||
code.CopyTo(pul, KamekChunk.HeaderSize);
|
||||
commandBytes.CopyTo(pul, KamekChunk.HeaderSize + code.Length);
|
||||
var pulPath = Path.Combine(root, "Code.pul");
|
||||
File.WriteAllBytes(pulPath, pul);
|
||||
var output = Path.Combine(root, "mod");
|
||||
string[] args = ["translate-mod", "--project", projectPath, "--code-pul", pulPath,
|
||||
"--base-manifest", manifestPath, "--out", output, "--module-guest-base", $"0x{Module:X8}",
|
||||
"--module-link-base", $"0x{Module:X8}", "--skip-retro-wfc", "--emit-cpp", "--threads", "1"];
|
||||
var entryPoint = typeof(TranslationProjectConfig).Assembly.EntryPoint!;
|
||||
var exitCode = Assert.IsType<int>(entryPoint.Invoke(null, [args]));
|
||||
Assert.Equal(0, exitCode);
|
||||
return TranslationSourceBundle.Read(Path.Combine(output, "translated_sources.bin"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(root, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] Words(uint[] words)
|
||||
{
|
||||
var bytes = new byte[words.Length * 4];
|
||||
for (var index = 0; index < words.Length; index++) Write(bytes, index * 4, words[index]);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static void Write(byte[] bytes, int offset, uint value) =>
|
||||
BinaryPrimitives.WriteUInt32BigEndian(bytes.AsSpan(offset, 4), value);
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
using Translator.Core.Disassembly;
|
||||
using Translator.Core.Mods;
|
||||
|
||||
namespace Translator.Tests;
|
||||
|
||||
public sealed class LrContinuationAnalysisTests
|
||||
{
|
||||
private const uint Entry = 0x80010000;
|
||||
private const uint Tail = Entry - 0x40;
|
||||
|
||||
[Fact]
|
||||
public void TailHelperUsesTheWrappersLrState()
|
||||
{
|
||||
var result = Analyze(new Dictionary<uint, uint[]>
|
||||
{
|
||||
[Entry] = [0x7D8802A6, 0x398C0014, 0x4BFFFFB8], // mflr; addi +20; b Tail
|
||||
[Tail] = [0x7D8803A6, 0x4E800020] // mtlr r12; blr
|
||||
});
|
||||
Assert.True(result.IsComplete);
|
||||
Assert.Equal(new[] { 20 }, result.Offsets);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TailHelperUsesTheWrappersStackFrame()
|
||||
{
|
||||
var result = Analyze(new Dictionary<uint, uint[]>
|
||||
{
|
||||
[Entry] = [0x7D8802A6, 0x9421FFF0, 0x91810014, 0x4BFFFFB4],
|
||||
[Tail] = [0x38210010, 0x81810004, 0x398C0014, 0x7D8803A6, 0x4E800020]
|
||||
});
|
||||
Assert.True(result.IsComplete);
|
||||
Assert.Equal(new[] { 20 }, result.Offsets);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OrdinaryTailCycleTerminatesWithoutInventingOffsets()
|
||||
{
|
||||
var result = Analyze(new Dictionary<uint, uint[]>
|
||||
{
|
||||
[Entry] = [0x4BFFFFC0],
|
||||
[Tail] = [0x48000040]
|
||||
});
|
||||
Assert.True(result.IsComplete);
|
||||
Assert.False(result.MaySkipReturn);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdjustingTailCycleReportsIncompleteAnalysis()
|
||||
{
|
||||
var result = Analyze(new Dictionary<uint, uint[]>
|
||||
{
|
||||
[Entry] = [0x7D8802A6, 0x4800003C],
|
||||
[Entry + 0x40] = [0x398C0004, 0x4BFFFFFC]
|
||||
});
|
||||
Assert.False(result.IsComplete);
|
||||
Assert.True(result.WasTruncated);
|
||||
Assert.True(result.MaySkipReturn);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UndecodableTailDoesNotProveAnOrdinaryReturn()
|
||||
{
|
||||
var result = Analyze(new Dictionary<uint, uint[]> { [Entry] = [0x4BFFFFC0] });
|
||||
Assert.False(result.IsComplete);
|
||||
Assert.True(result.WasTruncated);
|
||||
Assert.True(result.MaySkipReturn);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalCallDoesNotInheritItsCalleesSkipOffset()
|
||||
{
|
||||
var visited = new List<uint>();
|
||||
var result = LrContinuationAnalysis.Analyze(Entry, address =>
|
||||
{
|
||||
visited.Add(address);
|
||||
return Decode(address, [0x7FE802A6, 0x4BFFFFBD, 0x7FE803A6, 0x4E800020]);
|
||||
});
|
||||
Assert.Equal(new[] { Entry }, visited);
|
||||
Assert.True(result.IsComplete);
|
||||
Assert.False(result.MaySkipReturn);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnknownIndirectTailIsIncomplete()
|
||||
{
|
||||
var result = Analyze(new Dictionary<uint, uint[]> { [Entry] = [0x7D8903A6, 0x4E800420] });
|
||||
Assert.False(result.IsComplete);
|
||||
Assert.False(result.WasTruncated);
|
||||
Assert.True(result.MaySkipReturn);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConditionalCallDoesNotTraverseATargetAlsoUsedByATailBranch()
|
||||
{
|
||||
var result = Analyze(new Dictionary<uint, uint[]>
|
||||
{
|
||||
[Entry] = [0x7FE802A6, 0x4182FFBD, 0x3BE00000, 0x4BFFFFB4],
|
||||
[Tail] = [0x3BFF0014, 0x7FE803A6, 0x4E800020]
|
||||
});
|
||||
Assert.Empty(result.Offsets);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TailDiscoveryBudgetDoesNotProveAnOrdinaryReturn()
|
||||
{
|
||||
var calls = 0;
|
||||
var result = LrContinuationAnalysis.Analyze(Entry, address =>
|
||||
{
|
||||
calls++;
|
||||
return Decode(address, [0x48000040]);
|
||||
});
|
||||
Assert.InRange(calls, 1, 256);
|
||||
Assert.False(result.IsComplete);
|
||||
Assert.True(result.WasTruncated);
|
||||
Assert.True(result.MaySkipReturn);
|
||||
}
|
||||
|
||||
private static LrContinuationAnalysis Analyze(Dictionary<uint, uint[]> functions) =>
|
||||
LrContinuationAnalysis.Analyze(Entry, address => functions.TryGetValue(address, out var words)
|
||||
? Decode(address, words)
|
||||
: throw new InvalidOperationException("No synthetic function at this address."));
|
||||
|
||||
private static PpcInstruction[] Decode(uint address, uint[] words) =>
|
||||
words.Select((word, index) => PpcDecoder.Decode(address + (uint)index * 4, word)).ToArray();
|
||||
}
|
||||
@@ -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<IrValue>()),
|
||||
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<string, ValueRepresentation>
|
||||
{
|
||||
["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<uint> { 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using Translator.Core.Disassembly;
|
||||
using Translator.Core.Mods;
|
||||
|
||||
namespace Translator.Tests;
|
||||
|
||||
public sealed class LrRelativeAnalysisCompletenessTests
|
||||
{
|
||||
[Fact]
|
||||
public void EmptyResultReportsWhenPathStatesWereDropped()
|
||||
{
|
||||
var capped = false;
|
||||
var offsets = Analyze(SyntheticLrHookFactory.ManyOrdinaryReturnPaths(), () => capped = true);
|
||||
|
||||
Assert.Empty(offsets);
|
||||
Assert.True(capped, "An empty result from capped exploration must not prove an ordinary return.");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(20)]
|
||||
public void ExhaustiveAnalysisDoesNotReportAStateCap(int offset)
|
||||
{
|
||||
var capped = false;
|
||||
var offsets = Analyze(
|
||||
[
|
||||
0x7D8802A6u, // mflr r12
|
||||
0x398C0000u | (ushort)offset, // addi r12,r12,offset
|
||||
0x7D8803A6u, // mtlr r12
|
||||
0x4E800020u // blr
|
||||
], () => capped = true);
|
||||
|
||||
Assert.False(capped);
|
||||
Assert.Equal(offset == 0 ? Array.Empty<int>() : [offset], offsets);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CappedArmDoesNotDiscardAnOffsetFoundOnAnotherArm()
|
||||
{
|
||||
var capped = false;
|
||||
var offsets = Analyze(
|
||||
[
|
||||
0x7D8802A6u, // +00: mflr r12
|
||||
0x2C030000u, // +04: cmpwi r3,0
|
||||
0x41820010u, // +08: beq +0x18
|
||||
0x398C0014u, // +0C: addi r12,r12,20
|
||||
0x7D8803A6u, // +10: mtlr r12
|
||||
0x4E800020u, // +14: blr
|
||||
.. SyntheticLrHookFactory.ManyOrdinaryReturnPaths() // +18
|
||||
], () => capped = true);
|
||||
|
||||
Assert.True(capped);
|
||||
Assert.Equal(new[] { 20 }, offsets);
|
||||
}
|
||||
|
||||
private static int[] Analyze(uint[] words, Action onStateCapExceeded)
|
||||
{
|
||||
var instructions = words.Select((word, index) =>
|
||||
PpcDecoder.Decode(0x80010000u + (uint)index * 4, word)).ToArray();
|
||||
return ContinuationPlanner.DiscoverLrRelativeIndirectJumpOffsets(instructions, onStateCapExceeded).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
internal static class SyntheticLrHookFactory
|
||||
{
|
||||
public static uint[] ManyOrdinaryReturnPaths()
|
||||
{
|
||||
// 1,024 possible states exceed the 512-state cap without an unbounded loop.
|
||||
var words = new List<uint> { 0x7FE802A6u }; // mflr r31
|
||||
for (uint register = 3; register <= 12; register++)
|
||||
{
|
||||
words.Add(0x2C000000u | (register << 16)); // cmpwi rN,0
|
||||
words.Add(0x41820008u); // beq +8
|
||||
words.Add(0x38000004u | (register << 21) | (31u << 16)); // addi rN,r31,4
|
||||
}
|
||||
words.Add(0x4E800020u); // blr with unchanged LR
|
||||
return words.ToArray();
|
||||
}
|
||||
}
|
||||
@@ -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<uint>
|
||||
{
|
||||
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<uint> { 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();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user