mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-10-19 21:23:51 -04:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4c166a7eb1 | |||
| 29155eefbd | |||
| 46eb6b46f5 | |||
| 4f166e1403 | |||
| 6b4de9fbdb | |||
| 16bfe7b095 |
@@ -244,7 +244,8 @@ foreach ($name in $requiredDependencies) { Copy-Directory (Join-Path $dependency
|
||||
# The translation identity covers the runtime's native registrations through this index (see
|
||||
# ToolkitFingerprint), so it is extracted from exactly the staged runtime sources.
|
||||
$nativeIndex = Join-Path $workspace 'runtime\native-index.json'
|
||||
& $translator emit-native-index --project (Join-Path $workspace 'projects\mkwii\recomp.yml') --out $nativeIndex
|
||||
# The payload has no game inputs, so the project file (which validates them) cannot be loaded here.
|
||||
& $translator emit-native-index --native-source-dir (Join-Path $workspace 'runtime\src') --out $nativeIndex
|
||||
if ($LASTEXITCODE -ne 0) { throw "Native index extraction failed with exit code $LASTEXITCODE." }
|
||||
Assert-File $nativeIndex 'Runtime native index'
|
||||
# A staged runtime that fingerprints differently from the shipped archive would make every
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<!-- The one product version. Release tags must match it. -->
|
||||
<Version>0.2.34</Version>
|
||||
<Version>0.2.36</Version>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@@ -208,13 +208,14 @@ function Get-MkwThirdPartySourceFingerprint([string]$ThirdPartyDirectory) {
|
||||
SHA-256 over runtime/third_party, the vendored sources behind the prebuilt package. The
|
||||
precompiled Crypto++ archive is linked while first-party TUs compile against the same headers,
|
||||
so a workspace whose vendored tree moved on must not consume archives harvested from an older
|
||||
one (same reasoning as the aurora fingerprint below). Unlike that scan this hashes hidden files
|
||||
too, since Build-Installer stages this tree with Copy-Item -Force. Returns '' when absent.
|
||||
one (same reasoning as the aurora fingerprint below), skipping dot-paths by name the same way.
|
||||
Returns '' when absent.
|
||||
#>
|
||||
if ([string]::IsNullOrWhiteSpace($ThirdPartyDirectory)) { return '' }
|
||||
if (-not (Test-Path -LiteralPath $ThirdPartyDirectory -PathType Container)) { return '' }
|
||||
$root = [IO.Path]::GetFullPath($ThirdPartyDirectory).TrimEnd('\')
|
||||
$files = Get-ChildItem -LiteralPath $root -Recurse -File |
|
||||
$files = Get-ChildItem -LiteralPath $root -Recurse -File -Force |
|
||||
Where-Object { -not ($_.FullName.Substring($root.Length + 1) -split '\\' | Where-Object { $_.StartsWith('.') }) } |
|
||||
Sort-Object { $_.FullName.Substring($root.Length + 1).Replace('\', '/') }
|
||||
$sha = [Security.Cryptography.SHA256]::Create()
|
||||
try {
|
||||
@@ -245,13 +246,13 @@ function Get-MkwAuroraSourceFingerprint([string]$AuroraDirectory) {
|
||||
(Join-Path $root 'extern') + '\',
|
||||
(Join-Path $root 'build') + '\'
|
||||
)
|
||||
# Hidden files are skipped deliberately: Build-Installer.ps1 stages this tree
|
||||
# with Copy-Item -Recurse (no -Force), so they never reach the payload and
|
||||
# hashing them would make the two sides disagree by construction.
|
||||
$files = Get-ChildItem -LiteralPath $root -Recurse -File |
|
||||
# Dot-paths (the submodule's .git pointer) are skipped by name, not by the hidden attribute:
|
||||
# the payload zip drops that attribute, so the installed side would otherwise hash them.
|
||||
$files = Get-ChildItem -LiteralPath $root -Recurse -File -Force |
|
||||
Where-Object {
|
||||
$full = $_.FullName
|
||||
-not ($excludedPrefixes | Where-Object { $full.StartsWith($_, [StringComparison]::OrdinalIgnoreCase) })
|
||||
-not ($excludedPrefixes | Where-Object { $full.StartsWith($_, [StringComparison]::OrdinalIgnoreCase) }) -and
|
||||
-not ($full.Substring($root.Length + 1) -split '\\' | Where-Object { $_.StartsWith('.') })
|
||||
} |
|
||||
Sort-Object { $_.FullName.Substring($root.Length + 1).Replace('\', '/') }
|
||||
$sha = [Security.Cryptography.SHA256]::Create()
|
||||
|
||||
@@ -182,7 +182,7 @@ cp "$workspace/Launcher/local-build.sh" "$workspace/Launcher/build-cache.sh" "$a
|
||||
# index, so it is extracted from exactly the bundled runtime sources.
|
||||
echo "Extracting the runtime native index..."
|
||||
"$appdir/usr/bin/translator-cli" emit-native-index \
|
||||
--project "$appdir/workspace/projects/mkwii/recomp.yml" \
|
||||
--native-source-dir "$appdir/workspace/runtime/src" \
|
||||
--out "$appdir/workspace/runtime/native-index.json"
|
||||
[[ -f "$appdir/workspace/runtime/native-index.json" ]] || { echo "build-appimage.sh: the native index was not written" >&2; exit 1; }
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ done
|
||||
# The translation identity (build-cache.sh) covers the runtime's native registrations through this
|
||||
# index, so it is extracted from exactly the packaged runtime sources.
|
||||
if [[ "$host_arch" == arm64 ]]; then host_translator=$translator_arm64; else host_translator=$translator_x86_64; fi
|
||||
"$host_translator" emit-native-index --project "$resources/workspace/projects/mkwii/recomp.yml" \
|
||||
"$host_translator" emit-native-index --native-source-dir "$resources/workspace/runtime/src" \
|
||||
--out "$resources/workspace/runtime/native-index.json" || fail 'the runtime native index could not be extracted'
|
||||
[[ -f "$resources/workspace/runtime/native-index.json" ]] || fail 'the runtime native index was not written'
|
||||
mkdir -p "$resources/workspace/Launcher/macos"
|
||||
|
||||
@@ -372,6 +372,9 @@ static bool HIDAPI_DriverWii_InitDevice(SDL_HIDAPI_Device *device)
|
||||
[==[ x = ((data->rgucAccelerometer[0] << 2) | ((data->rgucBaseButtons[0] >> 5) & 0x03));
|
||||
y = ((data->rgucAccelerometer[1] << 2) | ((data->rgucBaseButtons[1] >> 4) & 0x02));
|
||||
z = ((data->rgucAccelerometer[2] << 2) | ((data->rgucBaseButtons[1] >> 5) & 0x02));
|
||||
if (x == 0 && y == 0 && z == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
values[0] = -(((float)x - ctx->m_fAccelZeroG[0]) / ctx->m_fAccelPerG[0]) * SDL_STANDARD_GRAVITY;
|
||||
values[1] = (((float)z - ctx->m_fAccelZeroG[2]) / ctx->m_fAccelPerG[2]) * SDL_STANDARD_GRAVITY;
|
||||
|
||||
@@ -93,12 +93,20 @@ extern "C" int32_t WUDGetStatus_HLE()
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(801CDB84, WUDGetStatus_HLE, int32_t, (), ());
|
||||
|
||||
// WPADGetDataFormat reads the per-channel format set by WPADSetDataFormat. Can't reuse the
|
||||
// translated SDK implementation: it dereferences Bluetooth control blocks that HLE'd WPADInit
|
||||
// never constructs.
|
||||
// WPADGetDataFormat: the format KPAD runs the attached extension in (WPAD_FMT_*_ACC_DPD).
|
||||
// WPADClampStick keys off it. Can't reuse the translated SDK implementation: it dereferences
|
||||
// Bluetooth control blocks that HLE'd WPADInit never constructs.
|
||||
extern "C" int32_t WPADGetDataFormat_HLE(uint32_t chan)
|
||||
{
|
||||
return g_state.contract.GetDataFormat(chan);
|
||||
const int32_t status = g_state.contract.GetDataFormat(chan);
|
||||
if (status < 0) {
|
||||
return status;
|
||||
}
|
||||
switch (WiiRemoteInput::EffectiveKind(chan)) {
|
||||
case WiiRemoteInput::Kind::RemoteWithNunchuk: return 5;
|
||||
case WiiRemoteInput::Kind::RemoteWithClassic: return 8;
|
||||
default: return 2;
|
||||
}
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(801C0B54, WPADGetDataFormat_HLE, int32_t, (uint32_t chan), (chan));
|
||||
|
||||
|
||||
@@ -144,8 +144,8 @@ struct LastAcc {
|
||||
std::array<LastAcc, PAD_MAX_CONTROLLERS> g_lastAcc{};
|
||||
std::array<LastAcc, PAD_MAX_CONTROLLERS> g_lastNunchukAcc{};
|
||||
|
||||
// Any axis beyond this is not a reading the remote's +-3 g sensor can produce.
|
||||
constexpr float kMaxPlausibleRemoteG = 4.0f;
|
||||
// A zeroed remote report decodes to minus its zero-g point, at least this far out on every axis.
|
||||
constexpr float kZeroReportMinG = 4.0f;
|
||||
|
||||
// Case-sensitive substring test that tolerates a null name.
|
||||
bool NameContains(const char* name, const char* needle) {
|
||||
@@ -250,10 +250,11 @@ bool ReadSdlAccel(SDL_Gamepad* gamepad, SDL_SensorType sensor, float* sdl) {
|
||||
|
||||
// True for a sample that cannot have come from the sensor. Over Bluetooth on
|
||||
// Windows the remote delivers, a few times a minute, a report whose
|
||||
// accelerometer bytes are all zero; SDL decodes that as -0x200 on every axis,
|
||||
// i.e. (+5.12, -5.12, -5.12) g for the remote (100 units/g) and
|
||||
// (+2.56, -2.56, -2.56) g for the Nunchuk (200 units/g). Handed to the game as
|
||||
// is, one such frame is a full-lock steer plus a 9 g "shake". `g` is the
|
||||
// accelerometer bytes are all zero; SDL decodes that as (+a, -b, -c) g with
|
||||
// a, b, c the zero-g point over the units per g of each axis: about 5 g for the
|
||||
// remote (factory calibration) and exactly 2.56 g for the Nunchuk (200 units/g).
|
||||
// Handed to the game as is, one such frame is a full-lock steer plus a 9 g
|
||||
// "shake". Single axes past 4 g are real readings of a hard shake. `g` is the
|
||||
// uncorrected SDL sample.
|
||||
bool IsGlitchedSample(SDL_SensorType sensor, const float* g) {
|
||||
// An exact zero vector is SDL's sensor buffer before the first report, not
|
||||
@@ -261,16 +262,11 @@ bool IsGlitchedSample(SDL_SensorType sensor, const float* g) {
|
||||
if (g[0] == 0.0f && g[1] == 0.0f && g[2] == 0.0f) {
|
||||
return true;
|
||||
}
|
||||
const float zero = sensor == SDL_SENSOR_ACCEL ? 5.12f : 2.56f;
|
||||
if (std::fabs(g[0] - zero) < 0.03f && std::fabs(g[1] + zero) < 0.03f && std::fabs(g[2] + zero) < 0.03f) {
|
||||
return true;
|
||||
}
|
||||
if (sensor == SDL_SENSOR_ACCEL) {
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
if (std::fabs(g[i]) > kMaxPlausibleRemoteG) return true;
|
||||
}
|
||||
return g[0] > kZeroReportMinG && g[1] < -kZeroReportMinG && g[2] < -kZeroReportMinG;
|
||||
}
|
||||
return false;
|
||||
constexpr float zero = 2.56f;
|
||||
return std::fabs(g[0] - zero) < 0.03f && std::fabs(g[1] + zero) < 0.03f && std::fabs(g[2] + zero) < 0.03f;
|
||||
}
|
||||
|
||||
// One accelerometer sample in g, SDL's sensor frame. The remote's own axes are
|
||||
|
||||
@@ -69,9 +69,24 @@ public static class ModFunctionDiscovery
|
||||
|
||||
// Kamek modules frequently register callbacks through static tables or lists that are
|
||||
// walked indirectly by module startup code. Seed those native translations ahead of time.
|
||||
// The Kamek blob is linked at offset zero, so a word inside it only holds a module address
|
||||
// when a pointer relocation wrote one there; anything else is an instruction or data that
|
||||
// happens to encode one. Images appended past the blob are not described by chunk commands.
|
||||
var pointerRelocations = chunk.Commands
|
||||
.Where(command => command.AddressIsRelative &&
|
||||
command.Arguments.Count > 0 &&
|
||||
command.Id is KamekCommandId.Addr32 or KamekCommandId.CondWritePointer)
|
||||
.Select(command => (command.Address, KamekAddress.Resolve(command.Arguments[0], moduleGuestBase)))
|
||||
.ToHashSet();
|
||||
for (var offset = 0; offset + 4 <= relocatedModuleImage.Length; offset += 4)
|
||||
{
|
||||
var value = BinaryPrimitives.ReadUInt32BigEndian(relocatedModuleImage.AsSpan(offset, 4));
|
||||
// Comparing the value also rejects conditional writes that were skipped.
|
||||
if ((uint)offset < chunk.CodeSize && !pointerRelocations.Contains(((uint)offset, value)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (LooksLikeFunctionPointerTarget(value, moduleGuestBase, moduleImageEnd, relocatedModuleImage))
|
||||
{
|
||||
AddImageAddress(value, "module data pointer");
|
||||
|
||||
@@ -28,7 +28,7 @@ public sealed class ModFunctionDiscoveryTests
|
||||
ctorEnd: 0,
|
||||
chunkSize: (uint)(KamekChunk.HeaderSize + code.Length),
|
||||
codeBlob: code,
|
||||
commands: []);
|
||||
commands: [PointerRelocation(0x40, 0x50)]);
|
||||
|
||||
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code)
|
||||
.ToDictionary(start => start.Address, start => start.Reason);
|
||||
@@ -368,7 +368,7 @@ public sealed class ModFunctionDiscoveryTests
|
||||
ctorEnd: 0,
|
||||
chunkSize: (uint)(code.Length + KamekChunk.HeaderSize),
|
||||
codeBlob: code,
|
||||
commands: []);
|
||||
commands: [PointerRelocation(0x20, 0x100)]);
|
||||
|
||||
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code)
|
||||
.ToDictionary(start => start.Address, start => start.Reason);
|
||||
@@ -381,7 +381,7 @@ public sealed class ModFunctionDiscoveryTests
|
||||
{
|
||||
const uint moduleBase = 0x81200000u;
|
||||
var relocatedImage = new byte[0x300];
|
||||
WriteU32(relocatedImage, 0x20, moduleBase + 0x200);
|
||||
WriteU32(relocatedImage, 0x280, moduleBase + 0x200); // appended pointer table
|
||||
WriteU32(relocatedImage, 0x200, 0x38600001u); // li r3,1
|
||||
WriteU32(relocatedImage, 0x204, 0x4E800020u); // blr
|
||||
|
||||
@@ -655,6 +655,450 @@ public sealed class ModFunctionDiscoveryTests
|
||||
Assert.Equal("module leaf boundary scan", starts[moduleBase + 0x43004]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DiscoversVtableOnlyTailCallFunctionNextToInstructionThatEncodesItsTail()
|
||||
{
|
||||
const uint moduleBase = 0x81840000u;
|
||||
var code = new byte[0x10000];
|
||||
|
||||
// Virtual call whose lwz encodes the address of the tail branch below.
|
||||
WriteU32(code, 0x0FF0, 0x9421FFF0u); // stwu r1,-0x10(r1)
|
||||
WriteU32(code, 0x0FF4, 0x7C0802A6u); // mflr r0
|
||||
WriteU32(code, 0x0FF8, 0x8061000Cu); // lwz r3,0xc(r1)
|
||||
WriteU32(code, 0x0FFC, 0x8184BB50u); // lwz r12,-0x44b0(r4)
|
||||
WriteU32(code, 0x1000, 0x7D8903A6u); // mtctr r12
|
||||
WriteU32(code, 0x1004, 0x4E800421u); // bctrl
|
||||
WriteU32(code, 0x1008, 0x38210010u); // addi r1,r1,0x10
|
||||
WriteU32(code, 0x100C, 0x4E800020u); // blr
|
||||
|
||||
WriteU32(code, 0xBAF0, 0x9421FFF0u); // stwu r1,-0x10(r1)
|
||||
WriteU32(code, 0xBAF4, 0x7C0802A6u); // mflr r0
|
||||
WriteU32(code, 0xBAF8, 0x80010014u); // lwz r0,0x14(r1)
|
||||
WriteU32(code, 0xBAFC, 0x7C0803A6u); // mtlr r0
|
||||
WriteU32(code, 0xBB00, 0x38210010u); // addi r1,r1,0x10
|
||||
WriteU32(code, 0xBB04, 0x4E800020u); // blr
|
||||
|
||||
// Frameless virtual reached only through the vtable; it ends in a tail call.
|
||||
WriteU32(code, 0xBB08, 0x38000008u); // li r0,8
|
||||
WriteU32(code, 0xBB0C, 0x3880FFFFu); // li r4,-1
|
||||
WriteU32(code, 0xBB10, 0x90832AB4u); // stw r4,0x2ab4(r3)
|
||||
WriteU32(code, 0xBB14, 0x38A00000u); // li r5,0
|
||||
WriteU32(code, 0xBB18, 0x80C3072Cu); // lwz r6,0x72c(r3)
|
||||
WriteU32(code, 0xBB1C, 0x7C0903A6u); // mtctr r0
|
||||
WriteU32(code, 0xBB20, 0x80860000u); // lwz r4,0(r6)
|
||||
WriteU32(code, 0xBB24, 0x38C60004u); // addi r6,r6,4
|
||||
WriteU32(code, 0xBB28, 0x98A401E9u); // stb r5,0x1e9(r4)
|
||||
WriteU32(code, 0xBB2C, 0x4200FFF4u); // bdnz -0xc
|
||||
WriteU32(code, 0xBB30, 0x3C80818Au); // lis r4,0x818a
|
||||
WriteU32(code, 0xBB34, 0x808441F8u); // lwz r4,0x41f8(r4)
|
||||
WriteU32(code, 0xBB38, 0x80040024u); // lwz r0,0x24(r4)
|
||||
WriteU32(code, 0xBB3C, 0x5400103Au); // slwi r0,r0,2
|
||||
WriteU32(code, 0xBB40, 0x7C000034u); // cntlzw r0,r0
|
||||
WriteU32(code, 0xBB44, 0x5400D97Eu); // srwi r0,r0,5
|
||||
WriteU32(code, 0xBB48, 0x980328E1u); // stb r0,0x28e1(r3)
|
||||
WriteU32(code, 0xBB4C, 0x98032699u); // stb r0,0x2699(r3)
|
||||
WriteU32(code, 0xBB50, EncodeBranch(moduleBase + 0xBB50, 0x80841110u)); // b base code
|
||||
|
||||
// Hook target that follows directly.
|
||||
WriteU32(code, 0xBB54, 0x3CA0818Au); // lis r5,0x818a
|
||||
WriteU32(code, 0xBB58, 0x80A541F8u); // lwz r5,0x41f8(r5)
|
||||
WriteU32(code, 0xBB5C, 0x80050024u); // lwz r0,0x24(r5)
|
||||
WriteU32(code, 0xBB60, 0x5400103Bu); // slwi. r0,r0,2
|
||||
WriteU32(code, 0xBB64, 0x41820008u); // beq +8
|
||||
WriteU32(code, 0xBB68, 0x38800001u); // li r4,1
|
||||
WriteU32(code, 0xBB6C, EncodeBranch(moduleBase + 0xBB6C, 0x805F0DB4u)); // b base code
|
||||
|
||||
WriteU32(code, 0xBB70, 0x9421FFC0u); // stwu r1,-0x40(r1)
|
||||
WriteU32(code, 0xBB74, 0x7C0802A6u); // mflr r0
|
||||
WriteU32(code, 0xBB78, 0x4E800020u); // blr
|
||||
|
||||
WriteU32(code, 0xF000, moduleBase + 0xBB70); // vtable
|
||||
WriteU32(code, 0xF004, moduleBase + 0xBB08);
|
||||
|
||||
var chunk = CodeChunk(
|
||||
code,
|
||||
PointerRelocation(0xF000, 0xBB70),
|
||||
PointerRelocation(0xF004, 0xBB08),
|
||||
AbsoluteCommand(KamekCommandId.BranchLink, 0x80601234u, 0xBB54));
|
||||
|
||||
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code)
|
||||
.ToDictionary(start => start.Address, start => start.Reason);
|
||||
|
||||
Assert.Equal("module leaf boundary scan", starts[moduleBase + 0xBB08]);
|
||||
Assert.Equal("BranchLink", starts[moduleBase + 0xBB54]);
|
||||
Assert.Equal("module data pointer", starts[moduleBase + 0xBB70]);
|
||||
Assert.False(starts.ContainsKey(moduleBase + 0xBB50));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IgnoresUnrelocatedInstructionWordThatEncodesTrampolineAddress()
|
||||
{
|
||||
const uint moduleBase = 0x81840000u;
|
||||
var code = new byte[0x10000];
|
||||
WriteU32(code, 0x100, 0x8184BB50u); // lwz r12,-0x44b0(r4)
|
||||
WriteU32(code, 0xBB4C, 0x98032699u); // stb r0,0x2699(r3)
|
||||
WriteU32(code, 0xBB50, EncodeBranch(moduleBase + 0xBB50, 0x80841110u)); // b base code
|
||||
|
||||
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(CodeChunk(code), moduleBase, code);
|
||||
|
||||
Assert.DoesNotContain(starts, start => start.Address == moduleBase + 0xBB50);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IgnoresUnrelocatedWordThatEncodesFunctionEpilogueAddress()
|
||||
{
|
||||
const uint moduleBase = 0x81840000u;
|
||||
var code = new byte[0x400];
|
||||
WriteU32(code, 0x100, 0x81840220u); // lwz r12,0x220(r4)
|
||||
WriteU32(code, 0x200, 0x9421FFF0u); // stwu r1,-0x10(r1)
|
||||
WriteU32(code, 0x204, 0x7C0802A6u); // mflr r0
|
||||
WriteU32(code, 0x208, 0x93C10008u); // stw r30,8(r1)
|
||||
WriteU32(code, 0x20C, 0x90010014u); // stw r0,0x14(r1)
|
||||
WriteU32(code, 0x210, 0x38630001u); // addi r3,r3,1
|
||||
WriteU32(code, 0x214, 0x38630001u); // addi r3,r3,1
|
||||
WriteU32(code, 0x218, 0x80010014u); // lwz r0,0x14(r1)
|
||||
WriteU32(code, 0x21C, 0x38630001u); // addi r3,r3,1
|
||||
WriteU32(code, 0x220, 0x83C10008u); // lwz r30,8(r1)
|
||||
WriteU32(code, 0x224, 0x7C0803A6u); // mtlr r0
|
||||
WriteU32(code, 0x228, 0x38210010u); // addi r1,r1,0x10
|
||||
WriteU32(code, 0x22C, 0x4E800020u); // blr
|
||||
|
||||
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(CodeChunk(code), moduleBase, code)
|
||||
.ToDictionary(start => start.Address, start => start.Reason);
|
||||
|
||||
Assert.Equal("module prologue scan", starts[moduleBase + 0x200]);
|
||||
Assert.False(starts.ContainsKey(moduleBase + 0x220));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DoesNotSplitFunctionAtAnyAddressEncodedByUnrelocatedInstructions()
|
||||
{
|
||||
const uint moduleBase = 0x81840000u;
|
||||
var code = new byte[0x3000];
|
||||
|
||||
WriteU32(code, 0x2000, 0x9421FFF0u); // stwu r1,-0x10(r1)
|
||||
WriteU32(code, 0x2004, 0x7C0802A6u); // mflr r0
|
||||
WriteU32(code, 0x2008, 0x90010014u); // stw r0,0x14(r1)
|
||||
for (var offset = 0x200C; offset < 0x20F0; offset += 4)
|
||||
{
|
||||
WriteU32(code, offset, 0x38630001u); // addi r3,r3,1
|
||||
}
|
||||
WriteU32(code, 0x20F0, 0x80010014u); // lwz r0,0x14(r1)
|
||||
WriteU32(code, 0x20F4, 0x7C0803A6u); // mtlr r0
|
||||
WriteU32(code, 0x20F8, 0x38210010u); // addi r1,r1,0x10
|
||||
WriteU32(code, 0x20FC, 0x4E800020u); // blr
|
||||
|
||||
// lwz r12,disp(r4) for every word of the function body.
|
||||
for (var offset = 0x2004; offset < 0x2100; offset += 4)
|
||||
{
|
||||
WriteU32(code, 0x100 + (offset - 0x2004), 0x81840000u | (uint)offset);
|
||||
}
|
||||
|
||||
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(CodeChunk(code), moduleBase, code);
|
||||
|
||||
Assert.Contains(starts, start => start.Address == moduleBase + 0x2000);
|
||||
Assert.DoesNotContain(starts, start => start.Address > moduleBase + 0x2000 && start.Address < moduleBase + 0x2100);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DiscoversRelocatedPointerToFunctionThatOnlyLooksLikeAnEpilogue()
|
||||
{
|
||||
const uint moduleBase = 0x81840000u;
|
||||
var code = new byte[0x400];
|
||||
WriteU32(code, 0x100, moduleBase + 0x220);
|
||||
WriteU32(code, 0x21C, 0x38630001u); // addi r3,r3,1
|
||||
WriteU32(code, 0x220, 0x83C10008u); // lwz r30,8(r1)
|
||||
WriteU32(code, 0x224, 0x38210010u); // addi r1,r1,0x10
|
||||
WriteU32(code, 0x228, 0x4E800020u); // blr
|
||||
|
||||
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(CodeChunk(code, PointerRelocation(0x100, 0x220)), moduleBase, code)
|
||||
.ToDictionary(start => start.Address, start => start.Reason);
|
||||
|
||||
Assert.Equal("module data pointer", starts[moduleBase + 0x220]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DiscoversConditionallyWrittenModulePointer()
|
||||
{
|
||||
const uint moduleBase = 0x81200000u;
|
||||
var code = new byte[0x80];
|
||||
WriteU32(code, 0x40, moduleBase + 0x50);
|
||||
WriteU32(code, 0x4C, 0x38630001u); // addi r3,r3,1
|
||||
WriteU32(code, 0x50, 0x4E800020u); // blr
|
||||
|
||||
var chunk = CodeChunk(code, RelativeCommand(KamekCommandId.CondWritePointer, 0x40, 0x50, 0));
|
||||
|
||||
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code)
|
||||
.ToDictionary(start => start.Address, start => start.Reason);
|
||||
|
||||
Assert.Equal("module data pointer", starts[moduleBase + 0x50]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IgnoresSkippedConditionalPointerWrite()
|
||||
{
|
||||
const uint moduleBase = 0x81840000u;
|
||||
var code = new byte[0x400];
|
||||
WriteU32(code, 0x100, 0x81840220u); // lwz r12,0x220(r4), expected value did not match
|
||||
WriteU32(code, 0x21C, 0x38630001u); // addi r3,r3,1
|
||||
WriteU32(code, 0x220, 0x38600001u); // li r3,1
|
||||
WriteU32(code, 0x224, 0x4E800020u); // blr
|
||||
WriteU32(code, 0x300, 0x38600002u); // li r3,2
|
||||
WriteU32(code, 0x304, 0x4E800020u); // blr
|
||||
|
||||
var chunk = CodeChunk(code, RelativeCommand(KamekCommandId.CondWritePointer, 0x100, 0x300, 0x12345678u));
|
||||
|
||||
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code);
|
||||
|
||||
Assert.DoesNotContain(starts, start => start.Address == moduleBase + 0x220);
|
||||
Assert.DoesNotContain(starts, start => start.Address == moduleBase + 0x300);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IgnoresPointerRelocationOverwrittenByLaterWrite()
|
||||
{
|
||||
const uint moduleBase = 0x81840000u;
|
||||
var code = new byte[0x400];
|
||||
WriteU32(code, 0x100, 0x81840220u); // Write32 result applied after the relocation
|
||||
WriteU32(code, 0x21C, 0x38630001u); // addi r3,r3,1
|
||||
WriteU32(code, 0x220, 0x38600001u); // li r3,1
|
||||
WriteU32(code, 0x224, 0x4E800020u); // blr
|
||||
WriteU32(code, 0x2FC, 0x38630001u); // addi r3,r3,1
|
||||
WriteU32(code, 0x300, 0x38600002u); // li r3,2
|
||||
WriteU32(code, 0x304, 0x4E800020u); // blr
|
||||
|
||||
var chunk = CodeChunk(
|
||||
code,
|
||||
PointerRelocation(0x100, 0x300),
|
||||
RelativeCommand(KamekCommandId.Write32, 0x100, 0x81840220u));
|
||||
|
||||
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code);
|
||||
|
||||
Assert.DoesNotContain(starts, start => start.Address == moduleBase + 0x220);
|
||||
Assert.DoesNotContain(starts, start => start.Address == moduleBase + 0x300);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(KamekCommandId.Addr16Lo, 2u)]
|
||||
[InlineData(KamekCommandId.Addr16Ha, 2u)]
|
||||
[InlineData(KamekCommandId.Addr16Hi, 2u)]
|
||||
[InlineData(KamekCommandId.Addr16Lo, 0u)]
|
||||
[InlineData(KamekCommandId.Addr16Ha, 0u)]
|
||||
[InlineData(KamekCommandId.Addr16Hi, 0u)]
|
||||
[InlineData(KamekCommandId.Write32, 0u)]
|
||||
[InlineData(KamekCommandId.CondWrite32, 0u)]
|
||||
public void IgnoresWordWhoseOnlyRelocationIsNotAPointerWrite(KamekCommandId id, uint addressDelta)
|
||||
{
|
||||
const uint moduleBase = 0x81840000u;
|
||||
var code = new byte[0x400];
|
||||
WriteU32(code, 0x100, 0x81840220u); // lwz r12,0x220(r4)
|
||||
WriteU32(code, 0x21C, 0x38630001u); // addi r3,r3,1
|
||||
WriteU32(code, 0x220, 0x38600001u); // li r3,1
|
||||
WriteU32(code, 0x224, 0x4E800020u); // blr
|
||||
|
||||
var chunk = CodeChunk(code, RelativeCommand(id, 0x100 + addressDelta, 0x220));
|
||||
|
||||
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code);
|
||||
|
||||
Assert.DoesNotContain(starts, start => start.Address == moduleBase + 0x220 && start.Reason == "module data pointer");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IgnoresAbsolutePointerWriteWhenGatingModuleWords()
|
||||
{
|
||||
const uint moduleBase = 0x81840000u;
|
||||
var code = new byte[0x400];
|
||||
WriteU32(code, 0x100, 0x81840220u); // lwz r12,0x220(r4)
|
||||
WriteU32(code, 0x21C, 0x38630001u); // addi r3,r3,1
|
||||
WriteU32(code, 0x220, 0x38600001u); // li r3,1
|
||||
WriteU32(code, 0x224, 0x4E800020u); // blr
|
||||
|
||||
// A base-game pointer patch whose address field happens to match the module offset.
|
||||
var chunk = CodeChunk(code, AbsoluteCommand(KamekCommandId.WritePointer, 0x100, 0x300));
|
||||
|
||||
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code);
|
||||
|
||||
Assert.DoesNotContain(starts, start => start.Address == moduleBase + 0x220);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HonorsPointerRelocationInLastWordOfCode()
|
||||
{
|
||||
const uint moduleBase = 0x81200000u;
|
||||
var code = new byte[0x100];
|
||||
WriteU32(code, 0xFC, moduleBase + 0x50);
|
||||
WriteU32(code, 0x50, 0x4E800020u); // blr
|
||||
|
||||
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(CodeChunk(code, PointerRelocation(0xFC, 0x50)), moduleBase, code)
|
||||
.ToDictionary(start => start.Address, start => start.Reason);
|
||||
|
||||
Assert.Equal("module data pointer", starts[moduleBase + 0x50]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IgnoresUnrelocatedModuleAddressInLastWordOfCode()
|
||||
{
|
||||
const uint moduleBase = 0x81200000u;
|
||||
var code = new byte[0x100];
|
||||
WriteU32(code, 0xFC, moduleBase + 0x50);
|
||||
WriteU32(code, 0x50, 0x4E800020u); // blr
|
||||
|
||||
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(CodeChunk(code), moduleBase, code);
|
||||
|
||||
Assert.DoesNotContain(starts, start => start.Address == moduleBase + 0x50);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScansFirstAppendedWordWithoutRelocations()
|
||||
{
|
||||
const uint moduleBase = 0x81200000u;
|
||||
var relocatedImage = new byte[0x200];
|
||||
WriteU32(relocatedImage, 0x50, 0x4E800020u); // blr
|
||||
WriteU32(relocatedImage, 0x100, moduleBase + 0x50); // first appended word
|
||||
|
||||
var chunk = new KamekChunk(
|
||||
index: 0,
|
||||
fileOffset: 0,
|
||||
bssSize: 0,
|
||||
codeSize: 0x100,
|
||||
ctorStart: 0,
|
||||
ctorEnd: 0,
|
||||
chunkSize: (uint)(0x100 + KamekChunk.HeaderSize),
|
||||
codeBlob: relocatedImage[..0x100],
|
||||
commands: []);
|
||||
|
||||
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, relocatedImage)
|
||||
.ToDictionary(start => start.Address, start => start.Reason);
|
||||
|
||||
Assert.Equal("module data pointer", starts[moduleBase + 0x50]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IgnoresUnrelocatedCodeWordPointingIntoAppendedImage()
|
||||
{
|
||||
const uint moduleBase = 0x81200000u;
|
||||
var relocatedImage = new byte[0x300];
|
||||
WriteU32(relocatedImage, 0x20, moduleBase + 0x200);
|
||||
WriteU32(relocatedImage, 0x1FC, 0x38630001u); // addi r3,r3,1
|
||||
WriteU32(relocatedImage, 0x200, 0x38600001u); // li r3,1
|
||||
WriteU32(relocatedImage, 0x204, 0x4E800020u); // blr
|
||||
|
||||
var chunk = new KamekChunk(
|
||||
index: 0,
|
||||
fileOffset: 0,
|
||||
bssSize: 0,
|
||||
codeSize: 0x100,
|
||||
ctorStart: 0,
|
||||
ctorEnd: 0,
|
||||
chunkSize: (uint)(0x100 + KamekChunk.HeaderSize),
|
||||
codeBlob: relocatedImage[..0x100],
|
||||
commands: []);
|
||||
|
||||
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, relocatedImage);
|
||||
|
||||
Assert.DoesNotContain(starts, start => start.Address == moduleBase + 0x200);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StillRejectsRelocatedPointersToData()
|
||||
{
|
||||
const uint moduleBase = 0x81200000u;
|
||||
var code = new byte[0x200];
|
||||
"control\0"u8.ToArray().CopyTo(code, 0x80);
|
||||
WriteU32(code, 0x20, moduleBase + 0x80);
|
||||
WriteU32(code, 0x24, moduleBase + 0x180); // zero word
|
||||
|
||||
var chunk = CodeChunk(code, PointerRelocation(0x20, 0x80), PointerRelocation(0x24, 0x180));
|
||||
|
||||
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code);
|
||||
|
||||
Assert.DoesNotContain(starts, start => start.Address == moduleBase + 0x80);
|
||||
Assert.DoesNotContain(starts, start => start.Address == moduleBase + 0x180);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DiscoversCtorEntriesWithAndWithoutPointerRelocations()
|
||||
{
|
||||
const uint moduleBase = 0x81200000u;
|
||||
var code = new byte[0x100];
|
||||
WriteU32(code, 0x40, moduleBase + 0x60);
|
||||
WriteU32(code, 0x44, moduleBase + 0x70);
|
||||
WriteU32(code, 0x5C, 0x38630001u); // addi r3,r3,1
|
||||
WriteU32(code, 0x60, 0x38600000u); // li r3,0
|
||||
WriteU32(code, 0x64, 0x4E800020u); // blr
|
||||
WriteU32(code, 0x6C, 0x38630001u); // addi r3,r3,1
|
||||
WriteU32(code, 0x70, 0x38600001u); // li r3,1
|
||||
WriteU32(code, 0x74, 0x4E800020u); // blr
|
||||
|
||||
var chunk = new KamekChunk(
|
||||
index: 0,
|
||||
fileOffset: 0,
|
||||
bssSize: 0,
|
||||
codeSize: (uint)code.Length,
|
||||
ctorStart: 0x40,
|
||||
ctorEnd: 0x48,
|
||||
chunkSize: (uint)(code.Length + KamekChunk.HeaderSize),
|
||||
codeBlob: code,
|
||||
commands: [PointerRelocation(0x40, 0x60)]);
|
||||
|
||||
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code)
|
||||
.ToDictionary(start => start.Address, start => start.Reason);
|
||||
|
||||
Assert.Equal("ctor", starts[moduleBase + 0x60]);
|
||||
Assert.Equal("ctor", starts[moduleBase + 0x70]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DiscoversHookAndRelocationBranchTargets()
|
||||
{
|
||||
const uint moduleBase = 0x81200000u;
|
||||
var code = new byte[0x200];
|
||||
WriteU32(code, 0x5C, 0x38630001u); // addi r3,r3,1
|
||||
WriteU32(code, 0x60, 0x38600001u); // li r3,1
|
||||
WriteU32(code, 0x64, 0x4E800020u); // blr
|
||||
WriteU32(code, 0x9C, 0x38630001u); // addi r3,r3,1
|
||||
WriteU32(code, 0xA0, 0x38600002u); // li r3,2
|
||||
WriteU32(code, 0xA4, 0x4E800020u); // blr
|
||||
WriteU32(code, 0xDC, 0x38630001u); // addi r3,r3,1
|
||||
WriteU32(code, 0xE0, 0x38600003u); // li r3,3
|
||||
WriteU32(code, 0xE4, 0x4E800020u); // blr
|
||||
WriteU32(code, 0x100, EncodeBranch(moduleBase + 0x100, 0x80001000u) | 1u); // bl base code
|
||||
|
||||
var chunk = CodeChunk(
|
||||
code,
|
||||
AbsoluteCommand(KamekCommandId.Branch, 0x80001234u, 0x60),
|
||||
AbsoluteCommand(KamekCommandId.BranchLink, 0x80001238u, 0xA0),
|
||||
RelativeCommand(KamekCommandId.Rel24, 0x100, 0xE0));
|
||||
|
||||
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code)
|
||||
.ToDictionary(start => start.Address, start => start.Reason);
|
||||
|
||||
Assert.Equal("Branch", starts[moduleBase + 0x60]);
|
||||
Assert.Equal("BranchLink", starts[moduleBase + 0xA0]);
|
||||
Assert.Equal("Rel24", starts[moduleBase + 0xE0]);
|
||||
}
|
||||
|
||||
private static KamekChunk CodeChunk(byte[] code, params KamekCommand[] commands) =>
|
||||
new(
|
||||
index: 0,
|
||||
fileOffset: 0,
|
||||
bssSize: 0,
|
||||
codeSize: (uint)code.Length,
|
||||
ctorStart: 0,
|
||||
ctorEnd: 0,
|
||||
chunkSize: (uint)(code.Length + KamekChunk.HeaderSize),
|
||||
codeBlob: code,
|
||||
commands: commands);
|
||||
|
||||
private static KamekCommand RelativeCommand(KamekCommandId id, uint offset, params uint[] arguments) =>
|
||||
new(0, ((uint)id << 24) | offset, id, false, offset, arguments);
|
||||
|
||||
private static KamekCommand AbsoluteCommand(KamekCommandId id, uint address, params uint[] arguments) =>
|
||||
new(0, ((uint)id << 24) | 0x00FFFFFEu, id, true, address, arguments);
|
||||
|
||||
private static KamekCommand PointerRelocation(uint offset, uint target) =>
|
||||
RelativeCommand(KamekCommandId.Addr32, offset, target);
|
||||
|
||||
private static void WriteU32(byte[] data, int offset, uint value) =>
|
||||
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(offset, 4), value);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user