IOS LLE: shadow comparison for REV_WATCH (StarletOS phase 11 H1)
Reads and IPC-reply audits compare each watched range with what the Starlet and the devices wrote, revealing other writers; register-list transfers in a watched virtual range log their translation. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
76a133f3a4
commit
ba5b6de0d0
7 files changed
+192
-16
No files matched your search
@@ -12,6 +12,7 @@
|
||||
#include <utility>
|
||||
|
||||
#include "Common/ChunkFile.h"
|
||||
#include "Common/Logging/Log.h"
|
||||
#include "Common/PerformanceTrace.h"
|
||||
#if defined(_M_X86_64)
|
||||
// ARMCore owns ARMJitX64 by value through unique_ptr construction in this translation unit, so a
|
||||
@@ -36,9 +37,24 @@ constexpr u32 ModeValue(ARMCore::Mode mode)
|
||||
ARMCore::ARMCore(ARMBus& bus, bool profile_jit_memory)
|
||||
: m_bus(bus), m_profile_jit_memory(profile_jit_memory)
|
||||
{
|
||||
m_watch_ranges = m_bus.GetWatchRanges();
|
||||
Reset();
|
||||
}
|
||||
|
||||
void ARMCore::TraceWatchedBlockTransfer(u32 address, u32 size, bool load) const
|
||||
{
|
||||
// StarletOS phase 11 (H1): the virtual range of a register-list transfer and the physical
|
||||
// address the interpreter translates its first word to. A difference shows a transfer that
|
||||
// does not read or write where the program addressed it.
|
||||
const bool overlaps = std::ranges::any_of(m_watch_ranges, [&](const auto& range) {
|
||||
return u64{address} < u64{range.first} + range.second && u64{address} + size > range.first;
|
||||
});
|
||||
if (!overlaps)
|
||||
return;
|
||||
NOTICE_LOG_FMT(IOS, "REVWATCH {} {:08x}->{:08x}/{} PC {:08x}", load ? "LDM" : "STM", address,
|
||||
TranslateVirtualAddress(address), size, m_instruction_address);
|
||||
}
|
||||
|
||||
ARMCore::~ARMCore() = default;
|
||||
|
||||
void ARMCore::SetJitEnabled(bool enabled)
|
||||
@@ -2252,6 +2268,8 @@ void ARMCore::ExecuteBlockDataTransfer(u32 instruction)
|
||||
address = preindex ? base + 4 : base;
|
||||
else
|
||||
address = preindex ? base - count * 4 : base - (count - 1) * 4;
|
||||
if (!m_watch_ranges.empty())
|
||||
TraceWatchedBlockTransfer(address, count * 4, load);
|
||||
|
||||
u32 loaded_pc = 0;
|
||||
bool pc_loaded = false;
|
||||
|
||||
@@ -311,6 +311,8 @@ private:
|
||||
void ExecuteSingleDataTransfer(u32 instruction);
|
||||
void ExecuteHalfwordDataTransfer(u32 instruction);
|
||||
void ExecuteBlockDataTransfer(u32 instruction);
|
||||
// Phase 11 H1: a register-list transfer overlapping a watched range, with its translation.
|
||||
void TraceWatchedBlockTransfer(u32 address, u32 size, bool load) const;
|
||||
void ExecuteMultiply(u32 instruction);
|
||||
void ExecuteMultiplyLong(u32 instruction);
|
||||
void ExecuteBranch(u32 instruction);
|
||||
@@ -369,6 +371,8 @@ private:
|
||||
u32 m_memory_poll_address = 0;
|
||||
bool m_pc_written = false;
|
||||
u32 m_instruction_address = 0;
|
||||
// Phase 11 H1: the bus's watched ranges (REV_WATCH), empty when off.
|
||||
std::vector<std::pair<u32, u32>> m_watch_ranges;
|
||||
u32 m_last_undefined_instruction = 0;
|
||||
u64 m_executed_instructions = 0;
|
||||
u64 m_memory_poll_entry_count = 0;
|
||||
|
||||
@@ -1476,6 +1476,15 @@ bool ARMJitX64::EmitARMBlockTransfer(u32 instruction, u32 address, bool* termina
|
||||
}
|
||||
|
||||
std::vector<FixupBranch> slow_paths;
|
||||
// StarletOS phase 11 (H1): a transfer whose virtual range overlaps a watched range runs through
|
||||
// the architectural helper, so the interpreter logs its translation.
|
||||
for (const auto& [start, size] : m_watch_ranges)
|
||||
{
|
||||
MOV(32, R(EDX), R(EAX));
|
||||
SUB(32, R(EDX), Imm32(start - (range_size - 1)));
|
||||
CMP(32, R(EDX), Imm32(size + range_size - 1));
|
||||
slow_paths.push_back(J_CC(CC_B, Jump::Near));
|
||||
}
|
||||
if ((m_core.m_cp15.control & 1U) != 0)
|
||||
{
|
||||
MOV(32, R(EDX), R(EAX));
|
||||
|
||||
@@ -476,7 +476,11 @@ StarletMemory::StarletMemory(Core::System& system) : m_system(system)
|
||||
ERROR_LOG_FMT(IOS, "REV_WATCH: ignoring range '{}'", range);
|
||||
}
|
||||
for (const auto& [start, size] : m_watch_ranges)
|
||||
{
|
||||
NOTICE_LOG_FMT(IOS, "REVWATCH range {:08x}+{:x}", start, size);
|
||||
m_watch_shadow.emplace_back(size);
|
||||
m_watch_known.emplace_back(size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -495,21 +499,116 @@ bool StarletMemory::WatchHit(u32 address, u32 size) const
|
||||
return false;
|
||||
}
|
||||
|
||||
void StarletMemory::WatchCPU(u32 address, u32 size, u32 value) const
|
||||
void StarletMemory::WatchCPU(u32 address, u32 size, u32 value)
|
||||
{
|
||||
if (!WatchHit(address, size))
|
||||
return;
|
||||
NOTICE_LOG_FMT(IOS, "REVWATCH CPU {:08x}/{} {:08x} PC {:08x} LR {:08x}", address, size, value,
|
||||
m_watch_core ? m_watch_core->GetInstructionAddress() : 0,
|
||||
m_watch_core ? m_watch_core->GetRegister(14) : 0);
|
||||
// The bus stores scalars big-endian.
|
||||
std::array<u8, 4> bytes{};
|
||||
for (u32 i = 0; i < size; ++i)
|
||||
bytes[i] = static_cast<u8>(value >> (8 * (size - 1 - i)));
|
||||
WatchRecord(address, bytes.data(), size);
|
||||
}
|
||||
|
||||
void StarletMemory::WatchDMA(u32 address, u32 size) const
|
||||
void StarletMemory::WatchDMA(u32 address, const u8* data, u32 size)
|
||||
{
|
||||
if (!WatchHit(address, size))
|
||||
return;
|
||||
NOTICE_LOG_FMT(IOS, "REVWATCH DMA {:08x}/{:x} PC {:08x}", address, size,
|
||||
m_watch_core ? m_watch_core->GetInstructionAddress() : 0);
|
||||
WatchRecord(address, data, size);
|
||||
}
|
||||
|
||||
void StarletMemory::WatchRecord(u32 address, const u8* data, u32 size)
|
||||
{
|
||||
for (size_t range = 0; range < m_watch_ranges.size(); ++range)
|
||||
{
|
||||
const auto [start, length] = m_watch_ranges[range];
|
||||
for (u32 i = 0; i < size; ++i)
|
||||
{
|
||||
const u32 at = address + i;
|
||||
if (at >= start && at - start < length)
|
||||
{
|
||||
m_watch_shadow[range][at - start] = data[i];
|
||||
m_watch_known[range][at - start] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void StarletMemory::WatchRead(u32 address, u32 size)
|
||||
{
|
||||
// Phase 11 H1: a byte the Starlet reads that differs from what the Starlet or a device last
|
||||
// wrote there was changed by another writer. Log it with the reader, then adopt the new value.
|
||||
const u8* memory = m_system.GetMemory().GetPointerForRange(address, size);
|
||||
if (memory == nullptr)
|
||||
return;
|
||||
for (size_t range = 0; range < m_watch_ranges.size(); ++range)
|
||||
{
|
||||
const auto [start, length] = m_watch_ranges[range];
|
||||
u64 read = 0;
|
||||
u64 expected = 0;
|
||||
bool differs = false;
|
||||
for (u32 i = 0; i < size; ++i)
|
||||
{
|
||||
const u32 at = address + i;
|
||||
const bool inside = at >= start && at - start < length;
|
||||
const u8 shadow = inside ? m_watch_shadow[range][at - start] : memory[i];
|
||||
read = (read << 8) | memory[i];
|
||||
expected = (expected << 8) | shadow;
|
||||
if (inside && m_watch_known[range][at - start] && shadow != memory[i])
|
||||
{
|
||||
differs = true;
|
||||
m_watch_shadow[range][at - start] = memory[i];
|
||||
}
|
||||
}
|
||||
if (differs)
|
||||
{
|
||||
NOTICE_LOG_FMT(IOS,
|
||||
"REVWATCH DIFF {:08x}/{} read {:0{}x} expected {:0{}x} PC {:08x} LR {:08x}",
|
||||
address, size, read, size * 2, expected, size * 2,
|
||||
m_watch_core ? m_watch_core->GetInstructionAddress() : 0,
|
||||
m_watch_core ? m_watch_core->GetRegister(14) : 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void StarletMemory::WatchAudit()
|
||||
{
|
||||
// Phase 11 H1, at each IPC reply: every run of written bytes that guest memory no longer
|
||||
// holds (at most 16 runs logged per reply), then adopt the new values.
|
||||
u32 runs = 0;
|
||||
for (size_t range = 0; range < m_watch_ranges.size(); ++range)
|
||||
{
|
||||
const auto [start, length] = m_watch_ranges[range];
|
||||
const u8* memory = m_system.GetMemory().GetPointerForRange(start, length);
|
||||
if (memory == nullptr)
|
||||
continue;
|
||||
auto& shadow = m_watch_shadow[range];
|
||||
const auto& known = m_watch_known[range];
|
||||
for (u32 i = 0; i < length;)
|
||||
{
|
||||
if (!known[i] || shadow[i] == memory[i])
|
||||
{
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
const u32 first = i;
|
||||
while (i < length && known[i] && shadow[i] != memory[i])
|
||||
{
|
||||
shadow[i] = memory[i];
|
||||
++i;
|
||||
}
|
||||
if (++runs <= 16)
|
||||
{
|
||||
NOTICE_LOG_FMT(IOS, "REVWATCH AUDIT {:08x}+{:x} now {:02x}", start + first, i - first,
|
||||
memory[first]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool StarletMemory::Init(const std::string& dump_directory, std::string* error)
|
||||
@@ -1239,7 +1338,7 @@ u8 StarletMemory::ReadDMA8(u32 address) const
|
||||
void StarletMemory::WriteDMA8(u32 address, u8 value)
|
||||
{
|
||||
if (!m_watch_ranges.empty() && !m_watch_in_dma_buffer)
|
||||
WatchDMA(address, 1);
|
||||
WatchDMA(address, &value, 1);
|
||||
if (address >= SRAM_BASE && address < SRAM_BASE + SRAM_WINDOW_SIZE)
|
||||
{
|
||||
const u32 offset = address - SRAM_BASE;
|
||||
@@ -1292,7 +1391,7 @@ void StarletMemory::WriteDMABuffer(u32 address, std::span<const u8> buffer)
|
||||
if (buffer.empty())
|
||||
return;
|
||||
if (!m_watch_ranges.empty())
|
||||
WatchDMA(address, static_cast<u32>(buffer.size()));
|
||||
WatchDMA(address, buffer.data(), static_cast<u32>(buffer.size()));
|
||||
if (u8* memory = GetDMAMemoryPointer(address, buffer.size()))
|
||||
{
|
||||
std::memcpy(memory, buffer.data(), buffer.size());
|
||||
@@ -3611,6 +3710,8 @@ void StarletMemory::WriteRegister(u32 address, u32 value)
|
||||
|
||||
u8 StarletMemory::Read8(u32 address)
|
||||
{
|
||||
if (!m_watch_ranges.empty() && WatchHit(address, 1))
|
||||
WatchRead(address, 1);
|
||||
if (IsBootROMAddress(address))
|
||||
return m_boot_rom[GetBootROMOffset(address)];
|
||||
|
||||
@@ -3732,6 +3833,8 @@ u8 StarletMemory::Read8(u32 address)
|
||||
|
||||
u16 StarletMemory::Read16(u32 address)
|
||||
{
|
||||
if (!m_watch_ranges.empty() && WatchHit(address, 2))
|
||||
WatchRead(address, 2);
|
||||
// Aligned reads stay inside one SRAM/boot0 mapping segment. Resolve the live mapping once,
|
||||
// including its holes, before considering unrelated memory and peripheral ranges.
|
||||
if ((address & 1) == 0 && IsSRAMWindowAddress(address))
|
||||
@@ -3766,6 +3869,8 @@ u16 StarletMemory::Read16(u32 address)
|
||||
|
||||
u32 StarletMemory::Read32(u32 address)
|
||||
{
|
||||
if (!m_watch_ranges.empty() && WatchHit(address, 4))
|
||||
WatchRead(address, 4);
|
||||
// ARM IOS accesses the Hollywood timer and interrupt controller almost exclusively as aligned
|
||||
// words. Falling back to ARMBus::Read32 decomposes each access into four virtual Read8 calls;
|
||||
// every byte then repeats the complete device-range decoder and IPC register switch. Preserve
|
||||
@@ -4124,6 +4229,9 @@ void StarletMemory::Write32(u32 address, u32 value)
|
||||
{
|
||||
WriteRegister(address, value);
|
||||
m_system.GetWiiIPC().WriteStarletRegister(address - HW_BASE, value);
|
||||
// Phase 11 H1: an IPC reply (ARMCTRL Y1) audits the watched ranges.
|
||||
if (!m_watch_ranges.empty() && address == HW_BASE + 0x0c && (value & 1) != 0)
|
||||
WatchAudit();
|
||||
if (address == HW_BASE + 0x0c && (value & 0x09) != 0)
|
||||
{
|
||||
if (Starlet* const starlet = m_system.GetStarlet())
|
||||
|
||||
@@ -352,9 +352,16 @@ private:
|
||||
std::vector<std::pair<u32, u32>> m_watch_ranges;
|
||||
const ARMCore* m_watch_core = nullptr;
|
||||
bool m_watch_in_dma_buffer = false;
|
||||
// Phase 11 H1: each watched range as the Starlet and the devices wrote it, and which of its
|
||||
// bytes they wrote. Starlet reads and IPC replies compare guest memory with it.
|
||||
std::vector<std::vector<u8>> m_watch_shadow;
|
||||
std::vector<std::vector<u8>> m_watch_known;
|
||||
bool WatchHit(u32 address, u32 size) const;
|
||||
void WatchCPU(u32 address, u32 size, u32 value) const;
|
||||
void WatchDMA(u32 address, u32 size) const;
|
||||
void WatchCPU(u32 address, u32 size, u32 value);
|
||||
void WatchDMA(u32 address, const u8* data, u32 size);
|
||||
void WatchRecord(u32 address, const u8* data, u32 size);
|
||||
void WatchRead(u32 address, u32 size);
|
||||
void WatchAudit();
|
||||
bool m_nand_read_only = false;
|
||||
bool m_boot0_mapped = true;
|
||||
bool m_sram_split_mode = false;
|
||||
|
||||
@@ -53,18 +53,18 @@
|
||||
},
|
||||
{
|
||||
"path": "Source/Core/Core/IOS/Starlet/ARMCore.cpp",
|
||||
"sha256": "40d45aed7c5edb4540105dcdee4dd8dcde88c21ba7f3279eff0a9564f2d02a29",
|
||||
"bytes": 75597
|
||||
"sha256": "262946f1fb95250d1aa1ca73c9bc27ef5f83511731bf84c765a41b04fe7ff70a",
|
||||
"bytes": 76503
|
||||
},
|
||||
{
|
||||
"path": "Source/Core/Core/IOS/Starlet/ARMCore.h",
|
||||
"sha256": "765bc1ebe9554f9821fd649a94a74f27d2b811383b73286062bf382d6a15aee6",
|
||||
"bytes": 14356
|
||||
"sha256": "fa80b0d697e19f0aee178be4b13097ad5466f0bbd0cb9b3e7a74876c045b731f",
|
||||
"bytes": 14647
|
||||
},
|
||||
{
|
||||
"path": "Source/Core/Core/IOS/Starlet/ARMJitX64.cpp",
|
||||
"sha256": "2aa934e97097b25fdf760a9a97849ee066439f1ea7ad231b9329611feeb24e23",
|
||||
"bytes": 109011
|
||||
"sha256": "30ac3e6f74edd40a76406394ed40d66a1ca5b6b9a17b9699a1534d255320108f",
|
||||
"bytes": 109425
|
||||
},
|
||||
{
|
||||
"path": "Source/Core/Core/IOS/Starlet/ARMJitX64.h",
|
||||
@@ -93,13 +93,13 @@
|
||||
},
|
||||
{
|
||||
"path": "Source/Core/Core/IOS/Starlet/StarletMemory.cpp",
|
||||
"sha256": "b6896d66410e08629b1dee568f94326a1caab1fa2740ad8c886f50b4a7d494de",
|
||||
"bytes": 192641
|
||||
"sha256": "f46cb37943c0feb9695e5d0541864e06fb47c2cbed7e6d48b03df64006181afd",
|
||||
"bytes": 196308
|
||||
},
|
||||
{
|
||||
"path": "Source/Core/Core/IOS/Starlet/StarletMemory.h",
|
||||
"sha256": "3005d026366a27d28e2bab0ee6cdbb9fffc347e10a384b77f19e23db1ec09ca0",
|
||||
"bytes": 16146
|
||||
"sha256": "6d58d81d94b32a647dbc3babea3e943aee307c02d9f1fb8904d8ebaccd22cf05",
|
||||
"bytes": 16542
|
||||
},
|
||||
{
|
||||
"path": "Source/Core/DolphinNoGUI/PlatformHeadless.cpp",
|
||||
@@ -178,6 +178,17 @@
|
||||
"Source/Core/Core/IOS/Starlet/ARMJitX64.cpp",
|
||||
"Source/Core/Core/IOS/Starlet/ARMJitX64.h"
|
||||
]
|
||||
},
|
||||
{
|
||||
"recordedAt": "2026-10-06",
|
||||
"purpose": "StarletOS Phase 11 proposal H1: with REV_WATCH, a shadow of each watched range (Starlet stores and device DMA); Starlet reads that differ from it (REVWATCH DIFF) and IPC-reply audits (REVWATCH AUDIT) reveal other writers; register-list transfers whose virtual range overlaps a watched range run through the architectural helper and log their translation (REVWATCH LDM/STM)",
|
||||
"files": [
|
||||
"Source/Core/Core/IOS/Starlet/ARMCore.cpp",
|
||||
"Source/Core/Core/IOS/Starlet/ARMCore.h",
|
||||
"Source/Core/Core/IOS/Starlet/ARMJitX64.cpp",
|
||||
"Source/Core/Core/IOS/Starlet/StarletMemory.cpp",
|
||||
"Source/Core/Core/IOS/Starlet/StarletMemory.h"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -193,3 +193,22 @@ table between two filesystem requests (the companion's finding 11).
|
||||
- The 95 selected Starlet tests passed (`.starlet_linux`); `.starlet_linux_x11`
|
||||
rebuilt. Smoke run with the companion's Revolution P11-H06: 1519 watched
|
||||
writes on its layer table during start-up, attributed by instruction.
|
||||
|
||||
## Shadow comparison for the write watch — 2026-10-06
|
||||
|
||||
Companion proposal H1 (approved). The companion's diagnostic image showed
|
||||
its kernel reading back, from its RAM layer table, an entry different from
|
||||
the one it had just written, with no other Starlet store or device DMA seen.
|
||||
|
||||
- With `REV_WATCH`, `StarletMemory` keeps a shadow of each range, updated by
|
||||
the Starlet stores and device DMA it logs. A Starlet read whose bytes differ
|
||||
from the shadow logs `REVWATCH DIFF addr/size read … expected … PC … LR …`
|
||||
(another writer changed them: Broadway, its GPU, the DSP…); each IPC reply
|
||||
(ARMCTRL Y1) audits the ranges (`REVWATCH AUDIT addr+len now …`, at most 16
|
||||
runs per reply). The shadow then adopts the new bytes.
|
||||
- Register-list transfers whose virtual range overlaps a watched range now
|
||||
take the JIT's architectural helper, and the interpreter logs
|
||||
`REVWATCH LDM|STM virtual->physical/size PC …` (its own translation).
|
||||
- `ARMCore.cpp` keeps its CRLF bytes. 95 Starlet tests passed. Companion
|
||||
smoke run (P11-H06D, 150 s): no DIFF, no AUDIT, 9 LDM with identical
|
||||
virtual and physical addresses.
|
||||
Reference in new issue
Block a user