Compare commits

..
Author SHA1 Message Date
Yaya48andClaude Opus 5.5 6ede30f99e StarletBootstrapProbe: wait for StarOS's whole panic path
--expect-panic passed as soon as __panic_state was nonzero at the end of
a 4096-cycle slice. StarOS's handler stores the guard first, then the
code, then publishes the SBR1 record, so the run could stop with the path
half done. The probe now keeps running until the path is finished and
still is two slices later, and fails if the run budget ends first:
- __panic_state 1 and __panic_code 1;
- with --sbr1-record: STARLET_STATE 5 (Panic), FATAL_REASON 0 and
  EXI_OWNER 1 (handed back to the PPC);
- with --sbr1-absent: every word of the record area still zero.
The failure names the first unmet condition. The check is
StarOSPanicCheck.h, pure, with the SBR1 constants moved there, and
StarletProbePanicCheckTest covers each condition; each of 7 mutants of
the check fails a test. Runs that do not expect a panic are unchanged.

With StarOS's panic test image (eb930ee), interpreter and JIT: the
scenarios pass, two slices later than before. Three images derived from
it pass the previous probe and now fail: EXI kept by the Starlet
(--sbr1-record), __panic_code never stored, the record written although
absent (--sbr1-absent). (StarOS MMU-001 review of F1.)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-14 00:20:23 +02:00
Yaya48andClaude Opus 5.5 a62c65615a IOS LLE: StarOS's panic words must not alias
LoadBootstrapELF accepted __panic_state and __panic_code at the same
address, or as two words sharing bytes: the code's store would then also
set the guard, and nothing could tell the two apart. They must now be two
non-overlapping words of the data segment, like the other checks, before
any byte is written. BootstrapELFTest.PanicWordsMustNotAlias: the same
word and three partial overlaps, all inside the data segment, are
rejected; adjacent words are accepted. With the rule disabled, the test
fails. (StarOS MMU-001 review of F1.)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-14 00:20:22 +02:00
Yaya48andClaude Opus 5.5 15da28ad7b StarletBootstrapProbe: --sbr1-record and --sbr1-absent
StarOS's SBR1 boot record (MEM1 0x00200000) is created by its Broadway
loader before the image runs; the probe never had one, so the paths
that publish into it ran only on the console. --sbr1-record writes the
record as the loader and the ARM relay leave it (header, relay at its
last step, EXI channel 1 given to the Starlet, magic last); after the run
both options print the record's magic, state, reason, EXI owner, fault
code, ring length and dropped count, and how many of its 1024 words are
nonzero (0 is expected with --sbr1-absent). Without either option the
probe behaves as before.

With the StarOS M1 images (interpreter and JIT): idle publishes state 3
and hands EXI back (owner 1, ring 258 bytes, no Gecko emulated);
fatal-data state 4 reason 3; undefined-entry state 6, fault 1, ring
empty, as on the console (gates HW2-HW4).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 22:40:28 +02:00
Yaya48andClaude Opus 5.5 2bae49bdad IOS LLE: the ARM926 TLB is unified (StarOS MMU-001 F1 review)
The ARM926EJ-S has one TLB for instructions and data, with lockdown
entries that whole-TLB invalidation preserves. Two comments said the
I and D TLBs were separate; they now say what the software TLB really
differs in: capacity and replacement (a direct-mapped cache of 4096
1 KiB granules), and lockdown, which is not modelled (c10 writes are
ignored). No code change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 22:03:22 +02:00
Yaya48andClaude Opus 5.5 612cab7b30 StarletBootstrapProbe: report StarOS's panic words
StarOS's panic path (ADR-0014 R6) writes no trace line: it sets the guard
word __panic_state, stores __panic_code and halts. LoadBootstrapELF now
returns both symbols when an image has them (optional, but both or
neither, as words of the data segment), and the probe stops a run whose
guard is set: it prints the trace published so far and
"Starlet panic state <n> code <n>", and passes --expect-panic when the
state is 1. Images without these symbols (StarletOS) load and run as
before; a REV:PANIC trace line still satisfies --expect-panic.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 21:21:58 +02:00
Yaya48andClaude Opus 5.5 cdec826bb8 StarletBootstrapProbe: --mmu-faults
Enables the ARM926 MMU aborts of ARMCore (SetMMUFaultsEnabled) for the
run. Without the option the probe behaves as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 21:09:18 +02:00
Yaya48andClaude Opus 5.5 9b493a1e16 IOS LLE: optional ARM926 MMU aborts and CP15 privilege (StarOS MMU-001 F1)
ARMCore::SetMMUFaultsEnabled(true) makes the Starlet core deliver the
faults of the ARM926EJ-S MMU instead of the identity fallback:
- translation (section 0x5, page 0x7), domain (0x9, 0xb) and permission
  (0xd, 0xf) faults from the descriptors, the DACR (no access and the
  reserved value fault; manager skips AP) and AP with the S and R bits;
  ARMv5 has no execute permission, so a fetch needs read permission;
- data aborts (vector 0x10, LR = instruction + 8 in both states) set the
  FSR and the FAR (the MVA); prefetch aborts (vector 0x0c, LR + 4) set the
  IFSR (c5 opcode2 1) and leave the FAR unchanged;
- alignment faults (0x1) when control bit A is set;
- base-restored data aborts: the first faulting access ends the
  instruction's bus traffic and its register results are undone; words
  stored before the fault stay written;
- LDRT/STRT/LDRBT/STRBT check User permissions in any mode;
- the TLB keeps each granule's domain and AP, never caches a faulting
  walk, and c8 single-entry operations drop only the page that maps the
  MVA, so an entry stays stale until the guest invalidates it;
- MRC/MCR to CP15 from User mode is an undefined instruction.

The JIT stays usable: with the option on, every block entry goes through
C++, which checks the fetch under the current mode and DACR, and only
instructions that touch neither memory, CP15, the PSRs nor the exception
model run natively; the rest use the interpreter.

Off by default. The IOS LLE path keeps its identity fallback, its
savestate format (the option and the IFSR are not serialized) and its
idle-loop fast-forwarding; the only change there is that the data
accessors read one abort flag that stays clear.

20 StarletARMCoreMMUFaults tests, each run in the interpreter and the
JIT; the existing Starlet tests pass unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 21:09:18 +02:00
12 changed files with 1453 additions and 41 deletions

No files matched your search

+413 -30
View File
@@ -26,8 +26,29 @@ namespace
{
constexpr u32 CP15_CONTROL_BIG_ENDIAN = 1U << 7;
constexpr u32 CP15_CONTROL_MMU = 1U << 0;
constexpr u32 CP15_CONTROL_ALIGNMENT = 1U << 1;
constexpr u32 CP15_CONTROL_SYSTEM_PROTECTION = 1U << 8;
constexpr u32 CP15_CONTROL_ROM_PROTECTION = 1U << 9;
constexpr u32 STARLET_EXCEPTION_VECTOR_BASE = 0xffff0000;
// Fault Status Register status values (ARM926EJ-S TRM, c5). The domain goes in bits 7:4 except
// for alignment and section translation faults, whose domain is not valid (written as 0 here).
constexpr u32 FSR_ALIGNMENT = 0x1;
constexpr u32 FSR_TRANSLATION_SECTION = 0x5;
constexpr u32 FSR_TRANSLATION_PAGE = 0x7;
constexpr u32 FSR_DOMAIN_SECTION = 0x9;
constexpr u32 FSR_DOMAIN_PAGE = 0xb;
constexpr u32 FSR_PERMISSION_SECTION = 0xd;
constexpr u32 FSR_PERMISSION_PAGE = 0xf;
// TLBEntry::protection, filled with MMU faults enabled.
constexpr u32 PROTECTION_PAGE = 1U << 6;
constexpr u32 MakeProtection(u32 domain, u32 access_permission, bool page, u32 size_log2)
{
return domain | (access_permission << 4) | (page ? PROTECTION_PAGE : 0) | (size_log2 << 8);
}
constexpr u32 ModeValue(ARMCore::Mode mode)
{
return static_cast<u32>(mode);
@@ -205,6 +226,16 @@ u32 ARMCore::TranslateVirtualAddress(u32 address) const
if ((m_cp15.control & CP15_CONTROL_MMU) == 0)
return address;
if (m_mmu_faults)
{
// Callers that cannot fault here (a fetch after its permission check, the JIT's decoder)
// share the checked TLB. A faulting translation is neither cached nor followed.
const u32 modified_address = ModifiedVirtualAddress(address);
u32 fault_status = 0;
const TLBEntry* const entry = LookupTLBChecked(modified_address, &fault_status);
return entry ? entry->physical_page | (modified_address & 0x3ff) : modified_address;
}
// ARM926 FCSE remaps the low 32 MiB through the process identifier before
// translation.
const u32 modified_address =
@@ -235,6 +266,11 @@ u32 ARMCore::TranslateVirtualAddressForJit(u32 address, const u8** first_descrip
*second_descriptor_value = 0;
if ((m_cp15.control & CP15_CONTROL_MMU) == 0)
return address;
// With MMU faults enabled the JIT dispatches every block through ARMJitX64::Dispatch, which has
// just checked this fetch. Refilling the entry from a fresh walk here would hide a stale TLB
// entry that the guest failed to invalidate, so reuse the checked TLB instead.
if (m_mmu_faults)
return TranslateVirtualAddress(address);
const u32 modified_address =
address < 0x02000000 ? address | (m_cp15.process_id & 0xfe000000) : address;
@@ -339,6 +375,272 @@ void ARMCore::InvalidateTLB()
#endif
}
void ARMCore::SetMMUFaultsEnabled(bool enabled)
{
m_mmu_faults = enabled;
m_abort_pending = false;
m_user_access = false;
m_waiting_for_memory_poll = false;
// Translations and translated code cached under the other model must not survive the switch.
InvalidateTLB();
InvalidateInstructionCache();
}
u32 ARMCore::ModifiedVirtualAddress(u32 address) const
{
return address < 0x02000000 ? address | (m_cp15.process_id & 0xfe000000) : address;
}
ARMCore::CheckedWalk ARMCore::WalkPageTablesChecked(u32 modified_address) const
{
// ARM926EJ-S first- and second-level descriptors. Domain and AP come from the descriptor that
// maps the address: the first level's domain field, then the AP subpage of a large (16 KiB)
// or small (1 KiB) page, or the single AP of a section or tiny page.
CheckedWalk walk;
const u32 first_level = ReadPhysical32((m_cp15.translation_table_base & 0xffffc000) |
((modified_address >> 18) & 0x3ffc));
const u32 domain = (first_level >> 5) & 0xf;
const auto map_page = [&](u32 second_level_address, bool fine) {
const u32 second_level = ReadPhysical32(second_level_address);
switch (second_level & 3)
{
case 1: // 64 KiB large page.
walk.physical_address = (second_level & 0xffff0000) | (modified_address & 0xffff);
walk.protection = MakeProtection(
domain, (second_level >> (4 + 2 * ((modified_address >> 14) & 3))) & 3, true, 16);
return;
case 2: // 4 KiB small page.
walk.physical_address = (second_level & 0xfffff000) | (modified_address & 0xfff);
walk.protection = MakeProtection(
domain, (second_level >> (4 + 2 * ((modified_address >> 10) & 3))) & 3, true, 12);
return;
case 3:
if (fine) // 1 KiB tiny page.
{
walk.physical_address = (second_level & 0xfffffc00) | (modified_address & 0x3ff);
walk.protection = MakeProtection(domain, (second_level >> 4) & 3, true, 10);
return;
}
// A coarse table has no tiny pages: fail closed.
[[fallthrough]];
default:
walk.fault_status = FSR_TRANSLATION_PAGE | (domain << 4);
return;
}
};
switch (first_level & 3)
{
case 1: // Coarse second-level table.
map_page((first_level & 0xfffffc00) | ((modified_address >> 10) & 0x3fc), false);
break;
case 2: // 1 MiB section.
walk.physical_address = (first_level & 0xfff00000) | (modified_address & 0x000fffff);
walk.protection = MakeProtection(domain, (first_level >> 10) & 3, false, 20);
break;
case 3: // Fine second-level table.
map_page((first_level & 0xfffff000) | ((modified_address >> 8) & 0xffc), true);
break;
default:
walk.fault_status = FSR_TRANSLATION_SECTION;
break;
}
return walk;
}
const ARMCore::TLBEntry* ARMCore::LookupTLBChecked(u32 modified_address, u32* fault_status) const
{
const u32 virtual_page = modified_address >> 10;
TLBEntry& entry = m_tlb[GetTLBIndex(virtual_page)];
if (entry.generation == m_tlb_generation && entry.virtual_page == virtual_page)
return &entry;
const CheckedWalk walk = WalkPageTablesChecked(modified_address);
if (walk.fault_status != 0)
{
// The TLB never holds a translation that faulted: the next access walks again.
*fault_status = walk.fault_status;
return nullptr;
}
entry.virtual_page = virtual_page;
entry.physical_page = walk.physical_address & ~0x3ffU;
entry.generation = m_tlb_generation;
entry.protection = walk.protection;
return &entry;
}
u32 ARMCore::PermissionFault(const TLBEntry& entry, AccessKind kind, bool user) const
{
// The DACR is read at every access, as on ARM926; AP comes from the TLB entry, so changing a
// descriptor's AP takes effect only after the guest invalidates that entry.
const u32 domain = entry.protection & 0xf;
const bool page = (entry.protection & PROTECTION_PAGE) != 0;
switch ((m_cp15.domain_access_control >> (2 * domain)) & 3)
{
case 3: // Manager: accesses are not checked against AP.
return 0;
case 1: // Client: AP below.
break;
default: // No access, and the reserved value 0b10 (fail closed).
return (page ? FSR_DOMAIN_PAGE : FSR_DOMAIN_SECTION) | (domain << 4);
}
// ARMv5 has no execute permission: a fetch needs read permission (no XN, no W^X).
const bool write = kind == AccessKind::Write;
bool allowed;
switch ((entry.protection >> 4) & 3)
{
case 3: // Read/write for both.
allowed = true;
break;
case 2: // Privileged read/write, User read-only.
allowed = !user || !write;
break;
case 1: // Privileged read/write, User none.
allowed = !user;
break;
default:
{
// AP 00 depends on the control register: S alone grants privileged reads, R alone grants
// reads to both. Neither grants nothing; both together are reserved (fail closed).
const bool system = (m_cp15.control & CP15_CONTROL_SYSTEM_PROTECTION) != 0;
const bool rom = (m_cp15.control & CP15_CONTROL_ROM_PROTECTION) != 0;
allowed = !write && ((system && !rom && !user) || (!system && rom));
break;
}
}
return allowed ? 0 : (page ? FSR_PERMISSION_PAGE : FSR_PERMISSION_SECTION) | (domain << 4);
}
bool ARMCore::IsUserAccess() const
{
return GetMode() == Mode::User || m_user_access;
}
u32 ARMCore::DataAddress(u32 address, bool write) const
{
if (!m_mmu_faults)
return TranslateVirtualAddress(address);
if (m_abort_pending || (m_cp15.control & CP15_CONTROL_MMU) == 0)
return address;
const u32 modified_address = ModifiedVirtualAddress(address);
u32 fault_status = 0;
const TLBEntry* const entry = LookupTLBChecked(modified_address, &fault_status);
if (entry != nullptr)
{
fault_status =
PermissionFault(*entry, write ? AccessKind::Write : AccessKind::Read, IsUserAccess());
}
if (fault_status != 0)
{
RaiseDataAbort(fault_status, modified_address);
return address;
}
return entry->physical_page | (modified_address & 0x3ff);
}
void ARMCore::RaiseDataAbort(u32 fault_status, u32 modified_address) const
{
// Only the first fault of an instruction is reported.
if (m_abort_pending)
return;
m_abort_pending = true;
m_abort_status = fault_status;
m_abort_address = modified_address;
}
void ARMCore::CheckAlignment(u32 address, u32 mask) const
{
// ARM926 checks alignment before translation, when control bit A is set.
if (!m_mmu_faults || (m_cp15.control & CP15_CONTROL_ALIGNMENT) == 0 || (address & mask) == 0)
return;
const bool mmu = (m_cp15.control & CP15_CONTROL_MMU) != 0;
RaiseDataAbort(FSR_ALIGNMENT, mmu ? ModifiedVirtualAddress(address) : address);
}
bool ARMCore::TryTranslateFetch(u32 address, u32* physical_address, u32* fault_status) const
{
*fault_status = 0;
if ((m_cp15.control & CP15_CONTROL_MMU) == 0)
{
*physical_address = address;
return true;
}
const u32 modified_address = ModifiedVirtualAddress(address);
const TLBEntry* const entry = LookupTLBChecked(modified_address, fault_status);
if (entry != nullptr)
*fault_status = PermissionFault(*entry, AccessKind::Fetch, GetMode() == Mode::User);
if (*fault_status != 0)
return false;
*physical_address = entry->physical_page | (modified_address & 0x3ff);
return true;
}
bool ARMCore::EnterPrefetchAbortIfFaulted(u32 address)
{
u32 physical_address = 0;
u32 fault_status = 0;
if (TryTranslateFetch(address, &physical_address, &fault_status))
return false;
// A prefetch abort sets the IFSR only. The FAR keeps the address of the last data abort; the
// handler finds the faulting instruction from LR, which is its address + 4 in both states.
m_instruction_fault_status = fault_status;
EnterException(Mode::Abort, STARLET_EXCEPTION_VECTOR_BASE + 0x0c, address + 4);
return true;
}
void ARMCore::ExecuteARMWithAborts(u32 instruction)
{
const std::array<u32, 16> registers = m_registers;
const std::array<u32, 7> user_registers = m_user_r8_r14;
const u32 cpsr = m_cpsr;
ExecuteARM(instruction);
if (!m_abort_pending)
return;
// Undo the instruction's register results: an LDM^ with PC may also have restored the CPSR
// after its loads. Memory it wrote before the fault stays written, as on ARM926.
m_abort_pending = false;
if (m_cpsr != cpsr)
SetCPSR(cpsr);
m_registers = registers;
m_user_r8_r14 = user_registers;
m_cp15.fault_status = m_abort_status;
m_cp15.fault_address = m_abort_address;
// A data abort returns to the aborted instruction + 8 in both states.
EnterException(Mode::Abort, STARLET_EXCEPTION_VECTOR_BASE + 0x10, m_instruction_address + 8);
}
void ARMCore::ExecuteThumbWithAborts(u16 instruction)
{
const std::array<u32, 16> registers = m_registers;
const u32 cpsr = m_cpsr;
ExecuteThumb(instruction);
if (!m_abort_pending)
return;
m_abort_pending = false;
if (m_cpsr != cpsr)
SetCPSR(cpsr);
m_registers = registers;
m_cp15.fault_status = m_abort_status;
m_cp15.fault_address = m_abort_address;
EnterException(Mode::Abort, STARLET_EXCEPTION_VECTOR_BASE + 0x10, m_instruction_address + 8);
}
void ARMCore::InvalidateTLBEntry(u32 modified_address)
{
// ARM926 drops the one entry that maps this MVA, whatever the size of its page. The software TLB
// holds that page as 1 KiB granules: drop each of them, and nothing else.
for (TLBEntry& entry : m_tlb)
{
if (entry.generation != m_tlb_generation)
continue;
const u32 size_log2 = (entry.protection >> 8) & 0xff;
const u32 mask = ~((1U << size_log2) - 1);
if (((entry.virtual_page << 10) & mask) == (modified_address & mask))
entry.generation = 0;
}
}
u32 ARMCore::FetchARMInstruction(u32 address)
{
const u32 physical_address = TranslateVirtualAddress(address);
@@ -391,35 +693,61 @@ void ARMCore::InvalidateInstructionCache()
#endif
}
// The data accessors translate through DataAddress. Without MMU faults that is the plain
// translation and m_abort_pending is never set. With them, a fault records the instruction's
// data abort and the bus is not touched by this or any later access of that instruction.
u8 ARMCore::ReadByte(u32 address) const
{
return m_bus.Read8(TranslateVirtualAddress(address));
const u32 physical_address = DataAddress(address, false);
return m_abort_pending ? 0 : m_bus.Read8(physical_address);
}
void ARMCore::WriteByte(u32 address, u8 value)
{
m_bus.Write8(TranslateVirtualAddress(address), value);
const u32 physical_address = DataAddress(address, true);
if (!m_abort_pending)
m_bus.Write8(physical_address, value);
}
u16 ARMCore::Read16(u32 address) const
{
const u16 value =
(address & 1) == 0 ?
m_bus.Read16(TranslateVirtualAddress(address)) :
static_cast<u16>((static_cast<u16>(m_bus.Read8(TranslateVirtualAddress(address))) << 8) |
m_bus.Read8(TranslateVirtualAddress(address + 1)));
const auto read8 = [this](u32 byte_address) -> u16 {
const u32 physical_address = DataAddress(byte_address, false);
return m_abort_pending ? 0 : m_bus.Read8(physical_address);
};
u16 value;
if ((address & 1) == 0)
{
const u32 physical_address = DataAddress(address, false);
value = m_abort_pending ? 0 : m_bus.Read16(physical_address);
}
else
{
const u16 high = read8(address);
value = static_cast<u16>((high << 8) | read8(address + 1));
}
return m_big_endian ? value : static_cast<u16>((value >> 8) | (value << 8));
}
u32 ARMCore::Read32(u32 address) const
{
const u32 value =
(address & 3) == 0 ?
m_bus.Read32(TranslateVirtualAddress(address)) :
(static_cast<u32>(m_bus.Read8(TranslateVirtualAddress(address))) << 24) |
(static_cast<u32>(m_bus.Read8(TranslateVirtualAddress(address + 1))) << 16) |
(static_cast<u32>(m_bus.Read8(TranslateVirtualAddress(address + 2))) << 8) |
m_bus.Read8(TranslateVirtualAddress(address + 3));
const auto read8 = [this](u32 byte_address) -> u32 {
const u32 physical_address = DataAddress(byte_address, false);
return m_abort_pending ? 0 : m_bus.Read8(physical_address);
};
u32 value;
if ((address & 3) == 0)
{
const u32 physical_address = DataAddress(address, false);
value = m_abort_pending ? 0 : m_bus.Read32(physical_address);
}
else
{
const u32 byte0 = read8(address);
const u32 byte1 = read8(address + 1);
const u32 byte2 = read8(address + 2);
value = (byte0 << 24) | (byte1 << 16) | (byte2 << 8) | read8(address + 3);
}
if (m_big_endian)
return value;
return (value >> 24) | ((value >> 8) & 0x0000ff00) | ((value << 8) & 0x00ff0000) | (value << 24);
@@ -431,11 +759,13 @@ void ARMCore::Write16(u32 address, u16 value)
value = static_cast<u16>((value >> 8) | (value << 8));
if ((address & 1) == 0)
{
m_bus.Write16(TranslateVirtualAddress(address), value);
const u32 physical_address = DataAddress(address, true);
if (!m_abort_pending)
m_bus.Write16(physical_address, value);
return;
}
m_bus.Write8(TranslateVirtualAddress(address), static_cast<u8>(value >> 8));
m_bus.Write8(TranslateVirtualAddress(address + 1), static_cast<u8>(value));
WriteByte(address, static_cast<u8>(value >> 8));
WriteByte(address + 1, static_cast<u8>(value));
}
void ARMCore::Write32(u32 address, u32 value)
@@ -447,13 +777,15 @@ void ARMCore::Write32(u32 address, u32 value)
}
if ((address & 3) == 0)
{
m_bus.Write32(TranslateVirtualAddress(address), value);
const u32 physical_address = DataAddress(address, true);
if (!m_abort_pending)
m_bus.Write32(physical_address, value);
return;
}
m_bus.Write8(TranslateVirtualAddress(address), static_cast<u8>(value >> 24));
m_bus.Write8(TranslateVirtualAddress(address + 1), static_cast<u8>(value >> 16));
m_bus.Write8(TranslateVirtualAddress(address + 2), static_cast<u8>(value >> 8));
m_bus.Write8(TranslateVirtualAddress(address + 3), static_cast<u8>(value));
WriteByte(address, static_cast<u8>(value >> 24));
WriteByte(address + 1, static_cast<u8>(value >> 16));
WriteByte(address + 2, static_cast<u8>(value >> 8));
WriteByte(address + 3, static_cast<u8>(value));
}
u32 ARMCore::ReadRegisterOperand(u32 index) const
@@ -769,11 +1101,23 @@ int ARMCore::StepInternal(bool advance_bus)
m_instruction_address = m_registers[15];
m_pc_written = false;
if (m_cpsr & CPSR_T)
if (m_mmu_faults && EnterPrefetchAbortIfFaulted(m_instruction_address))
{
// The instruction is not executed. Idle-loop fast-forwarding is off with MMU faults: its
// polls translate without permission checks.
}
else if (m_cpsr & CPSR_T)
{
const u16 instruction = FetchThumbInstruction(m_instruction_address);
ExecuteThumb(instruction);
TryEnterThumbMemoryPoll(instruction);
if (m_mmu_faults)
{
ExecuteThumbWithAborts(instruction);
}
else
{
ExecuteThumb(instruction);
TryEnterThumbMemoryPoll(instruction);
}
}
else
{
@@ -790,9 +1134,13 @@ int ARMCore::StepInternal(bool advance_bus)
}
else if (ConditionPassed(instruction >> 28))
{
ExecuteARM(instruction);
if (m_mmu_faults)
ExecuteARMWithAborts(instruction);
else
ExecuteARM(instruction);
}
TryEnterARMSliceStablePoll(instruction);
if (!m_mmu_faults)
TryEnterARMSliceStablePoll(instruction);
}
if (!m_pc_written)
@@ -1460,6 +1808,7 @@ void ARMCore::ExecuteARM(u32 instruction)
}
else
{
CheckAlignment(address, 3);
const u32 old = Read32(address & ~3U);
Write32(address & ~3U, ReadRegisterOperand(rm));
m_registers[rd] = old;
@@ -1734,6 +2083,10 @@ void ARMCore::ExecuteThumb(u16 instruction)
const u32 rb = (instruction >> 3) & 7;
const u32 rd = instruction & 7;
const u32 address = m_registers[rb] + m_registers[ro];
if (operation == 0 || operation == 4)
CheckAlignment(address, 3);
else if (operation == 1 || operation == 5 || operation == 7)
CheckAlignment(address, 1);
switch (operation)
{
case 0: // STR
@@ -1773,6 +2126,8 @@ void ARMCore::ExecuteThumb(u16 instruction)
const u32 rb = (instruction >> 3) & 7;
const u32 rd = instruction & 7;
const u32 address = m_registers[rb] + offset;
if (!byte)
CheckAlignment(address, 3);
if (load)
{
m_registers[rd] = byte ? ReadByte(address) : Read32(address);
@@ -1794,6 +2149,7 @@ void ARMCore::ExecuteThumb(u16 instruction)
const bool load = (instruction & 0x0800) != 0;
const u32 address = m_registers[(instruction >> 3) & 7] + (((instruction >> 6) & 0x1f) << 1);
const u32 rd = instruction & 7;
CheckAlignment(address, 1);
if (load)
m_registers[rd] = Read16(address);
else
@@ -1807,6 +2163,7 @@ void ARMCore::ExecuteThumb(u16 instruction)
const bool load = (instruction & 0x0800) != 0;
const u32 rd = (instruction >> 8) & 7;
const u32 address = m_registers[13] + ((instruction & 0xff) << 2);
CheckAlignment(address, 3);
if (load)
m_registers[rd] = Read32(address);
else
@@ -1851,6 +2208,7 @@ void ARMCore::ExecuteThumb(u16 instruction)
if (!pop)
{
u32 address = m_registers[13] - count * 4;
CheckAlignment(address, 3);
m_registers[13] = address;
for (u32 reg = 0; reg < 8; ++reg)
{
@@ -1866,6 +2224,7 @@ void ARMCore::ExecuteThumb(u16 instruction)
else
{
u32 address = m_registers[13];
CheckAlignment(address, 3);
for (u32 reg = 0; reg < 8; ++reg)
{
if (register_list & (1U << reg))
@@ -1903,6 +2262,7 @@ void ARMCore::ExecuteThumb(u16 instruction)
return;
}
u32 address = m_registers[rb];
CheckAlignment(address, 3);
for (u32 reg = 0; reg < 8; ++reg)
{
if (!(register_list & (1U << reg)))
@@ -2121,6 +2481,10 @@ void ARMCore::ExecuteSingleDataTransfer(u32 instruction)
const u32 base = ReadRegisterOperand(rn);
const u32 indexed = add_offset ? base + offset : base - offset;
const u32 address = preindex ? indexed : base;
if (!byte)
CheckAlignment(address, 3);
// LDRT/STRT/LDRBT/STRBT (post-indexed with W set) check User permissions in any mode.
m_user_access = !preindex && writeback;
if (load)
{
u32 value;
@@ -2147,6 +2511,7 @@ void ARMCore::ExecuteSingleDataTransfer(u32 instruction)
else
Write32(address & ~3U, value);
}
m_user_access = false;
if ((!preindex || writeback) && !(load && rn == rd))
m_registers[rn] = indexed;
@@ -2203,6 +2568,8 @@ void ARMCore::ExecuteHalfwordDataTransfer(u32 instruction)
return;
}
if (type == 1 || type == 3)
CheckAlignment(address, 1);
if (load)
{
u32 value;
@@ -2280,6 +2647,7 @@ void ARMCore::ExecuteBlockDataTransfer(u32 instruction)
address = preindex ? base - count * 4 : base - (count - 1) * 4;
if (!m_watch_ranges.empty())
TraceWatchedBlockTransfer(address, count * 4, load);
CheckAlignment(address, 3);
u32 loaded_pc = 0;
bool pc_loaded = false;
@@ -2475,6 +2843,12 @@ void ARMCore::ExecuteCoprocessor(u32 instruction)
UndefinedInstruction(instruction);
return;
}
// CP15 is privileged: from User mode an MRC or MCR is an undefined instruction.
if (m_mmu_faults && GetMode() == Mode::User)
{
UndefinedInstruction(instruction);
return;
}
if (load)
{
@@ -2699,7 +3073,8 @@ u32 ARMCore::ReadCP15(u32 opcode1, u32 crn, u32 crm, u32 opcode2) const
case 3:
return m_cp15.domain_access_control;
case 5:
return m_cp15.fault_status;
// Opcode_2 1 is the IFSR, modelled with MMU faults only.
return m_mmu_faults && opcode2 == 1 ? m_instruction_fault_status : m_cp15.fault_status;
case 6:
return m_cp15.fault_address;
case 13:
@@ -2738,7 +3113,10 @@ void ARMCore::WriteCP15(u32 opcode1, u32 crn, u32 crm, u32 opcode2, u32 value)
m_cp15.domain_access_control = value;
break;
case 5:
m_cp15.fault_status = value;
if (m_mmu_faults && opcode2 == 1)
m_instruction_fault_status = value;
else
m_cp15.fault_status = value;
break;
case 6:
m_cp15.fault_address = value;
@@ -2752,7 +3130,12 @@ void ARMCore::WriteCP15(u32 opcode1, u32 crn, u32 crm, u32 opcode2, u32 value)
InvalidateInstructionCache();
break;
case 8: // TLB maintenance.
InvalidateTLB();
// With MMU faults, c8 c5/c6/c7 opcode2 1 invalidate the single entry that maps the MVA in Rd.
// The ARM926EJ-S TLB is unified, so the I, D and unified forms act on the same entries.
if (m_mmu_faults && opcode2 == 1 && (crm == 5 || crm == 6 || crm == 7))
InvalidateTLBEntry(value);
else
InvalidateTLB();
break;
case 13:
// ARM926 FCSE is specifically designed to switch low-address process spaces without flushing
+62 -3
View File
@@ -172,6 +172,15 @@ public:
u64 GetExecutedInstructions() const { return m_executed_instructions; }
void SetJitEnabled(bool enabled);
bool IsJitEnabled() const { return m_jit_enabled; }
// StarOS MMU-001 (F1): the ARM926 MMU's aborts and the CP15 privilege rule. When enabled, a
// translation, domain or permission fault (and, with control bit A, an alignment fault) raises
// a data abort (FSR c5 opcode2 0, FAR c6) or a prefetch abort (IFSR c5 opcode2 1; FAR left
// unchanged), access permissions follow AP, the S and R control bits and the DACR, TLB entries
// can be invalidated one MVA at a time, and CP15 from User mode is undefined. Off by default:
// the IOS LLE path keeps its identity fallback for fault descriptors and its savestate format.
void SetMMUFaultsEnabled(bool enabled);
bool AreMMUFaultsEnabled() const { return m_mmu_faults; }
u32 GetInstructionFaultStatus() const { return m_instruction_fault_status; }
bool IsJitMemoryProfilingEnabled() const { return m_profile_jit_memory; }
u64 GetTraceGeneration() const { return m_trace_generation; }
u64 GetJitExecutedInstructions() const;
@@ -246,6 +255,26 @@ private:
u32 virtual_page = 0;
u32 physical_page = 0;
u32 generation = 0;
// Filled only with MMU faults enabled: the descriptor's domain (bits 3:0), the AP of this
// 1 KiB granule (5:4), whether it came from a second-level page (bit 6), and log2 of the
// size the descriptor maps (15:8), so that one MVA invalidation drops its whole page.
u32 protection = 0;
};
enum class AccessKind
{
Read,
Write,
Fetch,
};
// The result of a page-table walk with MMU faults enabled.
struct CheckedWalk
{
u32 physical_address = 0;
// 0, or the FSR value of a translation fault: status in bits 3:0, domain in bits 7:4.
u32 fault_status = 0;
u32 protection = 0;
};
struct InstructionCacheEntry
@@ -291,6 +320,20 @@ private:
u32* second_descriptor_value) const;
u32 ReadPhysical32(u32 address) const;
void InvalidateTLB();
// MMU faults enabled only.
u32 ModifiedVirtualAddress(u32 address) const;
CheckedWalk WalkPageTablesChecked(u32 modified_address) const;
const TLBEntry* LookupTLBChecked(u32 modified_address, u32* fault_status) const;
u32 PermissionFault(const TLBEntry& entry, AccessKind kind, bool user) const;
bool IsUserAccess() const;
u32 DataAddress(u32 address, bool write) const;
void RaiseDataAbort(u32 fault_status, u32 modified_address) const;
void CheckAlignment(u32 address, u32 mask) const;
bool TryTranslateFetch(u32 address, u32* physical_address, u32* fault_status) const;
bool EnterPrefetchAbortIfFaulted(u32 address);
void ExecuteARMWithAborts(u32 instruction);
void ExecuteThumbWithAborts(u16 instruction);
void InvalidateTLBEntry(u32 modified_address);
u16 FetchThumbInstruction(u32 address);
u32 FetchARMInstruction(u32 address);
void InvalidateInstructionCache();
@@ -356,9 +399,13 @@ private:
u32 m_spsr_und = 0;
CP15State m_cp15{};
// ARM926 has separate instruction/data TLBs. A unified 1 KiB-granularity
// software cache keeps the observable translations while avoiding a
// page-table walk on every interpreted access.
// The ARM926EJ-S has one TLB for instructions and data: set-associative main entries plus
// lockdown entries (c10) that whole-TLB invalidation preserves (ARM926EJ-S TRM). This software
// TLB is unified too, but it is a 4096-entry direct-mapped cache of 1 KiB granules, so its
// capacity and replacement differ: an entry can stay here after the hardware would have
// replaced it, or be replaced here while the hardware keeps it. Lockdown is not modelled (c10
// writes are ignored). It keeps the observable translations while avoiding a page-table walk on
// every interpreted access.
mutable std::array<TLBEntry, TLB_ENTRY_COUNT> m_tlb{};
std::array<InstructionCacheEntry, INSTRUCTION_CACHE_ENTRY_COUNT> m_instruction_cache{};
mutable u32 m_tlb_generation = 1;
@@ -386,6 +433,18 @@ private:
u64 m_next_profile_instruction = PROFILE_SAMPLE_INTERVAL;
std::unordered_map<u32, u64> m_pc_samples;
bool m_jit_enabled = false;
// StarOS MMU-001 (F1). Not serialized: the option is a host choice made before the run, and
// the IOS LLE savestate format stays unchanged.
bool m_mmu_faults = false;
u32 m_instruction_fault_status = 0;
// Set by LDRT/STRT/LDRBT/STRBT around their access: User permissions in a privileged mode.
bool m_user_access = false;
// The first faulting access of an instruction records its abort here. Later accesses of the
// same instruction neither read nor write the bus, and the instruction's register results are
// undone before the data abort is taken (ARM926 base-restored abort model).
mutable bool m_abort_pending = false;
mutable u32 m_abort_status = 0;
mutable u32 m_abort_address = 0;
#if defined(_M_X86_64)
std::unique_ptr<ARMJitX64> m_jit;
#endif
+85 -4
View File
@@ -194,6 +194,11 @@ void ARMJitX64::GenerateDispatcher()
MOV(64, R(RAX), ImmPtr(&m_clear_pending));
CMP(8, MatR(RAX), Imm8(0));
FixupBranch invalidated = J_CC(CC_NE, Jump::Near);
// With MMU faults enabled, every block entry goes through C++, which checks the fetch under the
// current mode and DACR: the native caches below validate descriptors, not permissions.
MOV(64, R(RAX), ImmPtr(&m_core.m_mmu_faults));
CMP(8, MatR(RAX), Imm8(0));
FixupBranch checked_dispatch = J_CC(CC_NE, Jump::Near);
// Four-way native block cache. The key includes CPSR.T in bit zero and uses the ARM926 modified
// virtual address (MVA) for low FCSE addresses. Different IOS process identifiers can therefore
@@ -379,6 +384,11 @@ void ARMJitX64::GenerateDispatcher()
MOV(32, R(ABI_PARAM3), R(EAX));
MOV(32, R(ABI_PARAM2), Imm32(static_cast<u32>(DispatchReason::PageMiss)));
FixupBranch dispatch_reason_page = J(Jump::Near);
SetJumpTarget(checked_dispatch);
XOR(32, R(ABI_PARAM4), R(ABI_PARAM4));
XOR(32, R(ABI_PARAM3), R(ABI_PARAM3));
MOV(32, R(ABI_PARAM2), Imm32(static_cast<u32>(DispatchReason::Checked)));
FixupBranch dispatch_reason_checked = J(Jump::Near);
SetJumpTarget(empty_entry);
SetJumpTarget(empty_entry_no_mmu);
MOV(32, R(ABI_PARAM4), R(EDX));
@@ -387,6 +397,7 @@ void ARMJitX64::GenerateDispatcher()
SetJumpTarget(dispatch_reason_translation);
SetJumpTarget(dispatch_reason_key);
SetJumpTarget(dispatch_reason_page);
SetJumpTarget(dispatch_reason_checked);
MOV(64, R(ABI_PARAM1), ImmPtr(this));
ABI_CallFunction(Dispatch);
TEST(64, R(RAX), R(RAX));
@@ -427,9 +438,62 @@ size_t ARMJitX64::GetFastEntrySetIndex(u32 key)
return static_cast<u32>(key * 0x9e3779b1U) >> 16;
}
const u8* ARMJitX64::DispatchChecked(ARMJitX64* jit)
{
ARMCore& core = jit->m_core;
++jit->m_dispatch_slow_count;
u32 physical_address = 0;
u32 fault_status = 0;
// A fetch that faults is left to the interpreter, whose next step takes the prefetch abort.
if (!core.TryTranslateFetch(core.m_registers[15], &physical_address, &fault_status))
return nullptr;
const bool thumb = (core.m_cpsr & ARMCore::CPSR_T) != 0;
const u8* first_descriptor = nullptr;
const u8* second_descriptor = nullptr;
u32 first_descriptor_value = 0;
u32 second_descriptor_value = 0;
// No fast entry is recorded: the generated dispatcher sends the next entry here again.
Block* const block = jit->GetOrCompileBlock(
core.m_registers[15] | (thumb ? 1U : 0U), &physical_address, &first_descriptor,
&first_descriptor_value, &second_descriptor, &second_descriptor_value);
return block && block->runnable ? block->entry : nullptr;
}
bool ARMJitX64::IsCheckedNativeARM(u32 instruction)
{
if ((instruction >> 28) == 0xf)
return false;
// B and BL.
if ((instruction & 0x0e000000) == 0x0a000000)
return true;
// Data processing only: not the multiply and extra load/store space, not the miscellaneous
// space (MRS, MSR, BX, BLX, CLZ, DSP: opcodes 8 to 11 with S clear), and not a write of PC,
// which may be an exception return.
if ((instruction & 0x0c000000) != 0 || (instruction & 0x02000090) == 0x00000090 ||
(instruction & 0x01900000) == 0x01000000)
{
return false;
}
return ((instruction >> 12) & 0xf) != 15;
}
bool ARMJitX64::IsCheckedNativeThumb(u16 instruction)
{
// Shifts, add/subtract, move/compare/add/subtract immediate, ALU operations, conditional
// branches (not SWI or the undefined condition) and unconditional branches.
if ((instruction & 0xc000) == 0x0000 || (instruction & 0xfc00) == 0x4000 ||
(instruction & 0xf800) == 0xe000)
{
return true;
}
return (instruction & 0xf000) == 0xd000 && ((instruction >> 8) & 0xf) < 0xe;
}
const u8* ARMJitX64::Dispatch(ARMJitX64* jit, DispatchReason reason, u32 generated_key,
u32 generated_set_offset)
{
if (reason == DispatchReason::Checked)
return DispatchChecked(jit);
const bool thumb = (jit->m_core.m_cpsr & ARMCore::CPSR_T) != 0;
const u32 key = jit->m_core.m_registers[15] | (thumb ? 1U : 0U);
const u32 fast_key =
@@ -767,6 +831,10 @@ ARMJitX64::Block ARMJitX64::CompileBlock(u32 address, bool thumb)
bool ARMJitX64::EmitDirectARM(u32 instruction, u32 address, bool* terminal, bool* dispatcher_exit)
{
// With MMU faults, the interpreter (through FallbackARM, which ends the block) runs every
// instruction that could fault or change the next fetch's permission.
if (m_core.m_mmu_faults && !IsCheckedNativeARM(instruction))
return false;
// The IOS scheduler executes a small set of ARM926 system-control writes in virtually every
// syscall and interrupt path. Decode those architectural operations once while compiling the
// block instead of entering the complete interpreter decoder every time the block runs.
@@ -1862,6 +1930,8 @@ void ARMJitX64::EmitExchangeBranch(OpArg target, bool link, u32 return_address)
bool ARMJitX64::EmitDirectThumb(u16 instruction, u32 address, bool* terminal)
{
if (m_core.m_mmu_faults && !IsCheckedNativeThumb(instruction))
return false;
if ((instruction & 0xff00) == 0xdf00)
{
FlushRegisterCache();
@@ -2911,8 +2981,15 @@ void ARMJitX64::FallbackThumb(ARMJitX64* jit, u16 instruction, u32 address)
core->RecordJitFallback(address, true);
core->m_instruction_address = address;
core->m_pc_written = false;
core->ExecuteThumb(instruction);
core->TryEnterThumbMemoryPoll(instruction);
if (core->m_mmu_faults)
{
core->ExecuteThumbWithAborts(instruction);
}
else
{
core->ExecuteThumb(instruction);
core->TryEnterThumbMemoryPoll(instruction);
}
if (!core->m_pc_written)
core->m_registers[15] = address + ((core->m_cpsr & ARMCore::CPSR_T) ? 2 : 4);
}
@@ -2934,9 +3011,13 @@ void ARMJitX64::FallbackARM(ARMJitX64* jit, u32 instruction, u32 address)
}
else if (core->ConditionPassed(instruction >> 28))
{
core->ExecuteARM(instruction);
if (core->m_mmu_faults)
core->ExecuteARMWithAborts(instruction);
else
core->ExecuteARM(instruction);
}
core->TryEnterARMSliceStablePoll(instruction);
if (!core->m_mmu_faults)
core->TryEnterARMSliceStablePoll(instruction);
if (!core->m_pc_written)
core->m_registers[15] = address + ((core->m_cpsr & ARMCore::CPSR_T) ? 2 : 4);
}
+7
View File
@@ -157,6 +157,8 @@ private:
KeyMiss,
PageMiss,
EmptyEntry,
// MMU faults enabled: every block entry checks its fetch (ARMCore::SetMMUFaultsEnabled).
Checked,
};
void PoisonMemory() override;
@@ -172,6 +174,11 @@ private:
u32* first_descriptor_value_out, const u8** second_descriptor_out,
u32* second_descriptor_value_out);
Block CompileBlock(u32 address, bool thumb);
static const u8* DispatchChecked(ARMJitX64* jit);
// With MMU faults enabled, the instructions that run natively: they touch neither memory, CP15,
// the PSRs nor the exception model, so they can neither fault nor change a fetch's permission.
static bool IsCheckedNativeARM(u32 instruction);
static bool IsCheckedNativeThumb(u16 instruction);
bool EmitDirectARM(u32 instruction, u32 address, bool* terminal, bool* dispatcher_exit);
bool EmitARMMultiplyLong(u32 instruction);
bool CanEmitARMDataProcessing(u32 instruction) const;
@@ -192,6 +192,14 @@ bool LoadBootstrapELF(std::span<const u8> image, ARMBus& bus, BootstrapELFInfo*
parsed.fault_spsr = address;
found_fault_spsr = true;
}
else if (*name == "__panic_state")
{
parsed.panic_state = address;
}
else if (*name == "__panic_code")
{
parsed.panic_code = address;
}
}
}
const auto data_segment =
@@ -208,6 +216,17 @@ bool LoadBootstrapELF(std::span<const u8> image, ARMBus& bus, BootstrapELFInfo*
{
return Fail(error, "bootstrap ELF is missing the trace or fault symbols");
}
// The panic words are optional, but both or neither, as two distinct words of the data segment:
// aliased, the code's store would also set the guard, and a probe could not tell them apart.
const bool panic_words_overlap =
static_cast<u64>(parsed.panic_state) < static_cast<u64>(parsed.panic_code) + 4 &&
static_cast<u64>(parsed.panic_code) < static_cast<u64>(parsed.panic_state) + 4;
if ((parsed.panic_state != 0) != (parsed.panic_code != 0) ||
(parsed.panic_state != 0 && (!in_data(parsed.panic_state, 4) ||
!in_data(parsed.panic_code, 4) || panic_words_overlap)))
{
return Fail(error, "bootstrap ELF has an invalid panic symbol");
}
for (const Segment& segment : segments)
{
@@ -19,6 +19,10 @@ struct BootstrapELFInfo
u32 fault_code = 0;
u32 fault_lr = 0;
u32 fault_spsr = 0;
// Optional (StarOS ADR-0014 R6): the panic path's guard and code words, 0 when the image has
// none. A nonzero guard means the panic handler ran.
u32 panic_state = 0;
u32 panic_code = 0;
};
// Load the Phase 01 ELF into a bus already configured with the high SRAM mapping.
+3
View File
@@ -16,6 +16,9 @@ add_dolphin_test(ESFormatsTest IOS/ES/FormatsTest.cpp)
add_dolphin_test(StarletARMCoreTest IOS/Starlet/ARMCoreTest.cpp)
add_dolphin_test(StarletBootstrapELFTest IOS/Starlet/BootstrapELFTest.cpp)
# StarletBootstrapProbe's StarOS panic check (Tools/StarletBootstrapProbe/StarOSPanicCheck.h).
add_dolphin_test(StarletProbePanicCheckTest IOS/Starlet/ProbePanicCheckTest.cpp)
target_include_directories(StarletProbePanicCheckTest PRIVATE ${CMAKE_SOURCE_DIR}/Tools/StarletBootstrapProbe)
add_dolphin_test(StarletDMATest IOS/Starlet/DMATest.cpp)
add_dolphin_test(StarletSRAMReadTest IOS/Starlet/SRAMReadTest.cpp)
add_dolphin_test(StarletSRAMWriteTest IOS/Starlet/SRAMWriteTest.cpp)
@@ -6,7 +6,9 @@
#include <array>
#include <bit>
#include <chrono>
#include <initializer_list>
#include <iostream>
#include <utility>
#include <vector>
#include <gtest/gtest.h>
@@ -3536,5 +3538,495 @@ TEST(StarletARMCore, JitCachesProvisionalIdentityTranslationForFaultPages)
EXPECT_GT(bus.GetDirectPointerCount(), 0u); // Native descriptor validation is still present.
#endif
}
// StarOS MMU-001 (F1): ARM926 MMU aborts with ARMCore::SetMMUFaultsEnabled. Every case runs in the
// interpreter and, on x86-64, in the JIT; both must reach the same state.
class MMUFaultMachine
{
public:
static constexpr u32 TABLE = 0x4000; // first-level table, 16 KiB aligned
static constexpr u32 COARSE = 0x8000; // one coarse second-level table
// Section 0x800 maps physical 0 privileged-only (AP 01), section 0x880 the same bytes with full
// access (AP 11), both in domain 0. Code is written once and run from either view.
static constexpr u32 KERNEL = 0x80000000;
static constexpr u32 CODE = KERNEL + 0x1000;
static constexpr u32 USER_CODE = 0x88001000;
static constexpr u32 FAR_SENTINEL = 0xdeadbeef;
static constexpr u32 SVC_CPSR = 0xd3;
static constexpr u32 USER_CPSR = 0xd0;
static constexpr u32 ABORT_MODE = 0x17;
static constexpr u32 UNDEFINED_MODE = 0x1b;
explicit MMUFaultMachine(bool jit) : m_bus(0x100000), m_core(m_bus)
{
// High vectors: undefined and SWI stop on themselves; both aborts branch to a handler that
// records SPSR (r8), DFSR (r9), FAR (r10) and IFSR (r11) through MRS/MRC, then marks its kind
// in r12 (0xd data, 0xc prefetch) and stops.
for (u32 vector = 0; vector < 0x20; vector += 4)
m_bus.WriteARM(0xffff0000 + vector, 0xeafffffe); // b .
m_bus.WriteARM(0xffff000c, 0xea00004b); // b 0xffff0140
m_bus.WriteARM(0xffff0010, 0xea00003a); // b 0xffff0100
for (const auto [handler, kind] : {std::pair{0xffff0100u, 0xe3a0c00du},
std::pair{0xffff0140u, 0xe3a0c00cu}})
{
m_bus.WriteARM(handler, 0xe14f8000); // mrs r8, spsr
m_bus.WriteARM(handler + 4, 0xee159f10); // mrc p15, 0, r9, c5, c0, 0 (DFSR)
m_bus.WriteARM(handler + 8, 0xee16af10); // mrc p15, 0, r10, c6, c0, 0 (FAR)
m_bus.WriteARM(handler + 12, 0xee15bf30); // mrc p15, 0, r11, c5, c0, 1 (IFSR)
m_bus.WriteARM(handler + 16, kind); // mov r12, #kind
m_bus.WriteARM(handler + 20, 0xeafffffe); // b .
}
MapSection(0x800, 0, 1, 0);
MapSection(0x880, 0, 3, 0);
MapSection(0xfff, 0xfff00000, 1, 0);
auto& cp15 = m_core.GetCP15State();
cp15.translation_table_base = TABLE;
cp15.domain_access_control = 1; // domain 0 client, every other domain no access
cp15.control |= 1;
cp15.fault_address = FAR_SENTINEL;
m_core.SetMMUFaultsEnabled(true);
m_core.SetJitEnabled(jit);
m_core.SetRegister(15, CODE);
}
void MapSection(u32 index, u32 physical, u32 ap, u32 domain)
{
m_bus.WriteARM(TABLE + index * 4,
(physical & 0xfff00000) | (ap << 10) | (domain << 5) | 0x12);
}
// The section of `virtual_address` points at the coarse table, whose entry for it maps a small
// page of `physical` with the same AP in its four subpages.
void MapSmallPage(u32 virtual_address, u32 physical, u32 ap, u32 domain)
{
m_bus.WriteARM(TABLE + (virtual_address >> 20) * 4, COARSE | (domain << 5) | 0x11);
m_bus.WriteARM(COARSE + ((virtual_address >> 12) & 0xff) * 4,
(physical & 0xfffff000) | ((ap * 0x55) << 4) | 2);
}
// Code at a physical address (its view in section 0x800 or 0x880 is `physical` + base).
void Code(u32 physical, std::initializer_list<u32> instructions)
{
for (const u32 instruction : instructions)
{
m_bus.WriteARM(physical, instruction);
physical += 4;
}
}
void Start(u32 cpsr, u32 pc)
{
m_core.SetCPSR(cpsr);
m_core.SetRegister(15, pc);
}
void Run(u64 cycles = 48) { m_core.RunCycles(cycles); }
void ExpectDataAbort(u32 instruction_address, u32 dfsr, u32 far, u32 spsr)
{
EXPECT_EQ(m_core.GetCPSR() & 0x1f, ABORT_MODE);
EXPECT_EQ(m_core.GetRegister(12), 0xdu) << "not the data abort vector";
EXPECT_EQ(m_core.GetRegister(14), instruction_address + 8);
EXPECT_EQ(m_core.GetRegister(8), spsr);
EXPECT_EQ(m_core.GetRegister(9), dfsr);
EXPECT_EQ(m_core.GetRegister(10), far);
}
void ExpectPrefetchAbort(u32 instruction_address, u32 ifsr, u32 spsr)
{
EXPECT_EQ(m_core.GetCPSR() & 0x1f, ABORT_MODE);
EXPECT_EQ(m_core.GetRegister(12), 0xcu) << "not the prefetch abort vector";
EXPECT_EQ(m_core.GetRegister(14), instruction_address + 4);
EXPECT_EQ(m_core.GetRegister(8), spsr);
EXPECT_EQ(m_core.GetRegister(11), ifsr);
// A prefetch abort leaves the FAR as it was.
EXPECT_EQ(m_core.GetRegister(10), FAR_SENTINEL);
}
TestBus& Bus() { return m_bus; }
ARMCore& Core() { return m_core; }
private:
TestBus m_bus;
ARMCore m_core;
};
template <typename Case>
void ForEachEngine(Case run_case)
{
for (const bool jit : {false, true})
{
#if !defined(_M_X86_64)
if (jit)
continue;
#endif
SCOPED_TRACE(jit ? "JIT" : "interpreter");
MMUFaultMachine machine(jit);
run_case(machine);
}
}
using M = MMUFaultMachine;
TEST(StarletARMCoreMMUFaults, SectionTranslationFaultIsADataAbort)
{
ForEachEngine([](M& m) {
m.Code(0x1000, {0xe5921000}); // ldr r1, [r2]
m.Core().SetRegister(1, 0x11111111);
m.Core().SetRegister(2, 0x90000004); // section 0x900 has no descriptor
m.Run();
m.ExpectDataAbort(M::CODE, 0x05, 0x90000004, M::SVC_CPSR);
EXPECT_EQ(m.Core().GetRegister(1), 0x11111111u);
});
}
TEST(StarletARMCoreMMUFaults, PageTranslationFaultReportsItsDomain)
{
ForEachEngine([](M& m) {
m.MapSmallPage(0x90000000, 0x2000, 3, 2);
m.Code(0x1000, {0xe5921000}); // ldr r1, [r2]
m.Core().SetRegister(2, 0x90001000); // the next small page has no descriptor
m.Run();
m.ExpectDataAbort(M::CODE, 0x27, 0x90001000, M::SVC_CPSR);
});
}
TEST(StarletARMCoreMMUFaults, DomainWithoutAccessFaultsForSectionsAndPages)
{
ForEachEngine([](M& m) {
m.MapSection(0x900, 0, 3, 2);
m.Code(0x1000, {0xe5921000}); // ldr r1, [r2]
m.Core().SetRegister(2, 0x90002000);
m.Run();
m.ExpectDataAbort(M::CODE, 0x29, 0x90002000, M::SVC_CPSR);
});
ForEachEngine([](M& m) {
m.MapSmallPage(0x91000000, 0x2000, 3, 4);
m.Code(0x1000, {0xe5921000}); // ldr r1, [r2]
m.Core().SetRegister(2, 0x91000010);
m.Run();
m.ExpectDataAbort(M::CODE, 0x4b, 0x91000010, M::SVC_CPSR);
});
}
TEST(StarletARMCoreMMUFaults, UserAccessFollowsAP)
{
// AP 01: privileged only. AP 10: User read-only. AP 11: full access.
ForEachEngine([](M& m) {
m.Code(0x1000, {0xe5921000}); // ldr r1, [r2]
m.Core().SetRegister(2, M::KERNEL + 0x2000);
m.Start(M::USER_CPSR, M::USER_CODE);
m.Run();
m.ExpectDataAbort(M::USER_CODE, 0x0d, M::KERNEL + 0x2000, M::USER_CPSR);
});
ForEachEngine([](M& m) {
m.MapSection(0x920, 0, 2, 0);
m.Bus().WriteARM(0x2000, 0x12345678);
m.Code(0x1000, {0xe5921000, 0xe5823000}); // ldr r1, [r2]; str r3, [r2]
m.Core().SetRegister(2, 0x92002000);
m.Core().SetRegister(3, 0xabcdef01);
m.Start(M::USER_CPSR, M::USER_CODE);
m.Run();
EXPECT_EQ(m.Core().GetRegister(1), 0x12345678u);
m.ExpectDataAbort(M::USER_CODE + 4, 0x0d, 0x92002000, M::USER_CPSR);
EXPECT_EQ(m.Bus()[0x2000], 0x12u) << "the faulting store wrote memory";
});
}
TEST(StarletARMCoreMMUFaults, APZeroDependsOnTheSAndRBits)
{
struct Case
{
u32 control_bits;
bool user;
bool write;
bool allowed;
};
constexpr u32 S = 1U << 8;
constexpr u32 R = 1U << 9;
for (const Case c : {Case{0, false, false, false}, Case{S, false, false, true},
Case{S, false, true, false}, Case{S, true, false, false},
Case{R, true, false, true}, Case{R, false, true, false},
Case{S | R, false, false, false}})
{
SCOPED_TRACE(testing::Message() << "control " << std::hex << c.control_bits << " user "
<< c.user << " write " << c.write);
ForEachEngine([&](M& m) {
m.MapSection(0x900, 0, 0, 0);
m.Bus().WriteARM(0x2000, 0x0badf00d);
m.Core().GetCP15State().control |= c.control_bits;
m.Code(0x1000, {c.write ? 0xe5821000u : 0xe5921000u}); // str/ldr r1, [r2]
m.Core().SetRegister(1, 0x11111111);
m.Core().SetRegister(2, 0x90002000);
const u32 code = c.user ? M::USER_CODE : M::CODE;
m.Start(c.user ? M::USER_CPSR : M::SVC_CPSR, code);
m.Run();
if (c.allowed)
{
EXPECT_NE(m.Core().GetCPSR() & 0x1f, M::ABORT_MODE);
EXPECT_EQ(m.Core().GetRegister(1), c.write ? 0x11111111u : 0x0badf00du);
}
else
{
m.ExpectDataAbort(code, 0x0d, 0x90002000, c.user ? M::USER_CPSR : M::SVC_CPSR);
}
});
}
}
TEST(StarletARMCoreMMUFaults, ManagerDomainSkipsAP)
{
ForEachEngine([](M& m) {
m.MapSection(0x900, 0, 0, 5);
m.Core().GetCP15State().domain_access_control = 1 | (3 << 10);
m.Bus().WriteARM(0x2000, 0x600dcafe);
m.Code(0x1000, {0xe5921000, 0xe5823004}); // ldr r1, [r2]; str r3, [r2, #4]
m.Core().SetRegister(2, 0x90002000);
m.Core().SetRegister(3, 0x01020304);
m.Start(M::USER_CPSR, M::USER_CODE);
m.Run(8);
EXPECT_EQ(m.Core().GetCPSR() & 0x1f, 0x10u);
EXPECT_EQ(m.Core().GetRegister(1), 0x600dcafeu);
EXPECT_EQ(m.Bus()[0x2004], 0x01u);
});
}
TEST(StarletARMCoreMMUFaults, FetchFromAnUnmappedSectionIsAPrefetchAbort)
{
ForEachEngine([](M& m) {
m.Code(0x1000, {0xe12fff13}); // bx r3
m.Core().SetRegister(3, 0x90000000);
m.Run();
m.ExpectPrefetchAbort(0x90000000, 0x05, M::SVC_CPSR);
});
}
TEST(StarletARMCoreMMUFaults, UserFetchFromPrivilegedCodeIsAPrefetchAbort)
{
// ARMv5 has no execute permission: a fetch needs read permission, so AP 01 stops User code.
ForEachEngine([](M& m) {
m.Code(0x1000, {0xe12fff13}); // bx r3
m.Core().SetRegister(3, M::CODE + 0x40);
m.Start(M::USER_CPSR, M::USER_CODE);
m.Run();
m.ExpectPrefetchAbort(M::CODE + 0x40, 0x0d, M::USER_CPSR);
});
}
TEST(StarletARMCoreMMUFaults, CachedCodeIsCheckedAgainAtEveryEntry)
{
// The JIT compiles the block in Supervisor mode; entering it again in User mode must fault.
ForEachEngine([](M& m) {
m.Code(0x1040, {0xe3a07001, 0xeafffffe}); // mov r7, #1; b .
m.Start(M::SVC_CPSR, M::CODE + 0x40);
m.Run(8);
EXPECT_EQ(m.Core().GetRegister(7), 1u);
m.Core().SetRegister(7, 0);
m.Start(M::USER_CPSR, M::CODE + 0x40);
m.Run();
m.ExpectPrefetchAbort(M::CODE + 0x40, 0x0d, M::USER_CPSR);
EXPECT_EQ(m.Core().GetRegister(7), 0u);
});
}
TEST(StarletARMCoreMMUFaults, RevokingTheCodeDomainStopsTheNextFetch)
{
ForEachEngine([](M& m) {
m.MapSection(0x930, 0, 1, 3);
m.Core().GetCP15State().domain_access_control = 1 | (1 << 6);
m.Code(0x1000, {0xee034f10, 0xe3a07001}); // mcr p15, 0, r4, c3, c0, 0; mov r7, #1
m.Core().SetRegister(4, 1); // domain 3: no access
m.Start(M::SVC_CPSR, 0x93001000);
m.Run();
m.ExpectPrefetchAbort(0x93001004, 0x39, M::SVC_CPSR);
EXPECT_EQ(m.Core().GetRegister(7), 0u);
});
}
TEST(StarletARMCoreMMUFaults, BlockTransferAbortRestoresTheBase)
{
ForEachEngine([](M& m) {
m.Code(0x1000, {0xe8b0000e}); // ldmia r0!, {r1, r2, r3}
for (u32 reg = 1; reg <= 3; ++reg)
m.Core().SetRegister(reg, 0x11111111 * reg);
m.Core().SetRegister(0, 0x800ffffc); // the second word is in unmapped section 0x801
m.Run();
m.ExpectDataAbort(M::CODE, 0x05, 0x80100000, M::SVC_CPSR);
EXPECT_EQ(m.Core().GetRegister(0), 0x800ffffcu);
for (u32 reg = 1; reg <= 3; ++reg)
EXPECT_EQ(m.Core().GetRegister(reg), 0x11111111 * reg);
});
ForEachEngine([](M& m) {
m.Code(0x1000, {0xe8a00006}); // stmia r0!, {r1, r2}
m.Core().SetRegister(0, 0x800ffffc);
m.Core().SetRegister(1, 0xa1a2a3a4);
m.Core().SetRegister(2, 0xb1b2b3b4);
m.Run();
m.ExpectDataAbort(M::CODE, 0x05, 0x80100000, M::SVC_CPSR);
EXPECT_EQ(m.Core().GetRegister(0), 0x800ffffcu);
// The word stored before the fault stays written, as on ARM926.
EXPECT_EQ(m.Bus()[0xffffc], 0xa1u);
});
}
TEST(StarletARMCoreMMUFaults, LDRTChecksUserPermissionsInAPrivilegedMode)
{
ForEachEngine([](M& m) {
m.Bus().WriteARM(0x2000, 0x13572468);
m.Code(0x1000, {0xe5921000, 0xe4b23000}); // ldr r1, [r2]; ldrt r3, [r2], #0
m.Core().SetRegister(2, M::KERNEL + 0x2000);
m.Run();
EXPECT_EQ(m.Core().GetRegister(1), 0x13572468u);
m.ExpectDataAbort(M::CODE + 4, 0x0d, M::KERNEL + 0x2000, M::SVC_CPSR);
});
}
TEST(StarletARMCoreMMUFaults, AlignmentFaultsNeedTheABit)
{
ForEachEngine([](M& m) {
m.Core().GetCP15State().control |= 2;
m.Code(0x1000, {0xe5921000}); // ldr r1, [r2]
m.Core().SetRegister(2, M::KERNEL + 0x2002);
m.Run();
m.ExpectDataAbort(M::CODE, 0x01, M::KERNEL + 0x2002, M::SVC_CPSR);
});
ForEachEngine([](M& m) {
m.Bus().WriteARM(0x2000, 0x11223344);
m.Code(0x1000, {0xe5921000}); // ldr r1, [r2]: rotated, no fault
m.Core().SetRegister(2, M::KERNEL + 0x2002);
m.Run(4);
EXPECT_EQ(m.Core().GetRegister(1), 0x33441122u);
});
}
TEST(StarletARMCoreMMUFaults, CP15FromUserModeIsUndefined)
{
ForEachEngine([](M& m) {
m.Code(0x1000, {0xee034f10}); // mcr p15, 0, r4, c3, c0, 0
m.Core().SetRegister(4, 0xffffffff);
m.Start(M::USER_CPSR, M::USER_CODE);
m.Run(8);
EXPECT_EQ(m.Core().GetCPSR() & 0x1f, M::UNDEFINED_MODE);
EXPECT_EQ(m.Core().GetRegister(15), 0xffff0004u);
EXPECT_EQ(m.Core().GetCP15State().domain_access_control, 1u);
});
}
TEST(StarletARMCoreMMUFaults, TLBEntriesStaleUntilTheirOwnInvalidation)
{
ForEachEngine([](M& m) {
m.MapSmallPage(0x94000000, 0x3000, 3, 0);
m.Bus().WriteARM(0x3000, 0xcafef00d);
m.Code(0x1000, {
0xe5921000, // ldr r1, [r2]: fills the TLB
0xe5854000, // str r4, [r5]: clears the page's descriptor
0xe5923000, // ldr r3, [r2]: the stale entry still translates
0xee086f37, // mcr p15, 0, r6, c8, c7, 1: another page's MVA
0xe5927000, // ldr r7, [r2]: still stale
0xee082f37, // mcr p15, 0, r2, c8, c7, 1: this page's MVA
0xe5920000, // ldr r0, [r2]: translation fault
});
m.Core().SetRegister(0, 0x5a5a5a5a);
m.Core().SetRegister(2, 0x94000000);
m.Core().SetRegister(4, 0);
m.Core().SetRegister(5, M::KERNEL + M::COARSE);
m.Core().SetRegister(6, 0x95000000);
m.Run(64);
EXPECT_EQ(m.Core().GetRegister(1), 0xcafef00du);
EXPECT_EQ(m.Core().GetRegister(3), 0xcafef00du);
EXPECT_EQ(m.Core().GetRegister(7), 0xcafef00du);
m.ExpectDataAbort(M::CODE + 24, 0x07, 0x94000000, M::SVC_CPSR);
EXPECT_EQ(m.Core().GetRegister(0), 0x5a5a5a5au);
});
}
TEST(StarletARMCoreMMUFaults, DACRChangesNeedNoTLBMaintenance)
{
ForEachEngine([](M& m) {
m.MapSection(0x960, 0, 3, 6);
m.Core().GetCP15State().domain_access_control = 1 | (1 << 12);
m.Bus().WriteARM(0x2000, 0x76543210);
m.Code(0x1000, {0xe5921000, 0xee034f10, 0xe5923000}); // ldr; mcr c3 (r4); ldr r3
m.Core().SetRegister(2, 0x96002000);
m.Core().SetRegister(3, 0x33333333);
m.Core().SetRegister(4, 1);
m.Run();
EXPECT_EQ(m.Core().GetRegister(1), 0x76543210u);
m.ExpectDataAbort(M::CODE + 8, 0x69, 0x96002000, M::SVC_CPSR);
EXPECT_EQ(m.Core().GetRegister(3), 0x33333333u);
});
}
TEST(StarletARMCoreMMUFaults, FCSEFaultsReportTheModifiedAddress)
{
ForEachEngine([](M& m) {
m.Core().GetCP15State().process_id = 0x02000000;
m.Code(0x1000, {0xe5921000}); // ldr r1, [r2]
m.Core().SetRegister(2, 0x00001000);
m.Run();
m.ExpectDataAbort(M::CODE, 0x05, 0x02001000, M::SVC_CPSR);
});
ForEachEngine([](M& m) {
m.Core().GetCP15State().process_id = 0x02000000;
m.MapSection(0x020, 0, 3, 0);
m.Code(0x1000, {0xe5921000}); // ldr r1, [r2]: MVA 0x02001000, this instruction itself
m.Core().SetRegister(2, 0x00001000);
m.Run(4);
EXPECT_EQ(m.Core().GetRegister(1), 0xe5921000u);
});
}
TEST(StarletARMCoreMMUFaults, ThumbDataAbortReturnsToTheInstructionPlusEight)
{
ForEachEngine([](M& m) {
m.Bus().WriteThumb(0x1080, 0x6811); // ldr r1, [r2, #0]
m.Core().SetRegister(2, 0x90000000);
m.Start(M::SVC_CPSR | 0x20, M::CODE + 0x80);
m.Run();
m.ExpectDataAbort(M::CODE + 0x80, 0x05, 0x90000000, M::SVC_CPSR | 0x20);
EXPECT_EQ(m.Core().GetCPSR() & 0x20, 0u);
});
}
TEST(StarletARMCoreMMUFaults, TheJITStillRunsNativeCode)
{
#if defined(_M_X86_64)
MMUFaultMachine m(true);
m.Code(0x1000, {
0xe3a00000, // mov r0, #0
0xe2800001, // add r0, r0, #1
0xe3500010, // cmp r0, #16
0x1afffffc, // bne 0x1004
0xeafffffe, // b .
});
m.Run(96);
EXPECT_EQ(m.Core().GetRegister(0), 16u);
EXPECT_GT(m.Core().GetJitNativeExecutedInstructions(), 0u);
#endif
}
TEST(StarletARMCoreMMUFaults, DisabledFaultsKeepTheIdentityFallbackAndUserCP15)
{
// The IOS LLE model is unchanged unless SetMMUFaultsEnabled(true) is called.
for (const bool jit : {false, true})
{
#if !defined(_M_X86_64)
if (jit)
continue;
#endif
SCOPED_TRACE(jit ? "JIT" : "interpreter");
MMUFaultMachine m(jit);
m.Core().SetMMUFaultsEnabled(false);
m.Bus().WriteARM(0x2000, 0x44556677);
m.Code(0x1000, {0xe5921000, 0xee034f10, 0xeafffffe}); // ldr; mcr c3 (r4); b .
m.Core().SetRegister(2, 0x00002000); // section 0x000: no descriptor
m.Core().SetRegister(4, 0x55555555);
m.Start(M::USER_CPSR, M::USER_CODE);
m.Run(8);
EXPECT_EQ(m.Core().GetRegister(1), 0x44556677u);
EXPECT_EQ(m.Core().GetCP15State().domain_access_control, 0x55555555u);
EXPECT_EQ(m.Core().GetCPSR() & 0x1f, 0x10u);
}
}
} // namespace
} // namespace IOS::LLE
@@ -4,6 +4,7 @@
#include <array>
#include <string>
#include <unordered_map>
#include <utility>
#include <gtest/gtest.h>
@@ -232,5 +233,80 @@ TEST(ARMCoreTest, Armv5DoublewordTransfersAndAlignmentAbort)
EXPECT_EQ(core.GetRegister(14), TEXT_BASE + 8);
EXPECT_EQ(core.GetCP15State().fault_address, DATA_BASE + 4);
}
// StarOS ADR-0014 R6: the optional panic words. The string table moves past the section headers
// to make room for two more symbols.
std::array<u8, 0x300> MakePanicImage(u32 state, u32 code)
{
auto image = MakeImage();
constexpr char names[] =
"\0REV_TRACE\0__fault_code\0__fault_lr\0__fault_spsr\0__panic_state\0__panic_code";
for (size_t index = 0; index < sizeof(names); ++index)
image[0x278 + index] = static_cast<u8>(names[index]);
Put32(image, 0x228 + 16, 0x278);
Put32(image, 0x228 + 20, sizeof(names));
Put32(image, 0x250 + 20, 7 * 16);
const auto symbol = [&image](size_t index, u32 name, u32 address) {
const size_t offset = 0x120 + index * 16;
Put32(image, offset, name);
Put32(image, offset + 4, address);
Put32(image, offset + 8, 4);
};
// An omitted word keeps its slot, with the empty name.
symbol(5, state != 0 ? 48 : 0, state);
symbol(6, code != 0 ? 62 : 0, code);
return image;
}
TEST(BootstrapELFTest, PanicWordsAreOptionalButBothInData)
{
RecordingBus bus;
BootstrapELFInfo info;
std::string error;
ASSERT_TRUE(LoadBootstrapELF(MakeImage(), bus, &info, &error)) << error;
EXPECT_EQ(info.panic_state, 0u);
EXPECT_EQ(info.panic_code, 0u);
ASSERT_TRUE(LoadBootstrapELF(MakePanicImage(DATA_BASE + 0x41c, DATA_BASE + 4), bus, &info, &error))
<< error;
EXPECT_EQ(info.panic_state, DATA_BASE + 0x41c);
EXPECT_EQ(info.panic_code, DATA_BASE + 4);
for (const auto [state, code] : {std::pair{DATA_BASE + 0x41c, 0u}, std::pair{0u, DATA_BASE + 4},
std::pair{DATA_BASE + 0x41c, DATA_BASE + 0x420}})
{
RecordingBus rejected;
EXPECT_FALSE(LoadBootstrapELF(MakePanicImage(state, code), rejected, &info, &error));
EXPECT_EQ(error, "bootstrap ELF has an invalid panic symbol");
EXPECT_TRUE(rejected.bytes.empty());
}
}
TEST(BootstrapELFTest, PanicWordsMustNotAlias)
{
BootstrapELFInfo info;
std::string error;
// The same word, or two words sharing bytes, in either order; all inside the 0x420-byte data
// segment, so only the aliasing rule rejects them.
for (const auto [state, code] :
{std::pair{DATA_BASE + 4, DATA_BASE + 4}, std::pair{DATA_BASE + 4, DATA_BASE + 6},
std::pair{DATA_BASE + 7, DATA_BASE + 4}, std::pair{DATA_BASE + 0x41c, DATA_BASE + 0x41a}})
{
RecordingBus rejected;
EXPECT_FALSE(LoadBootstrapELF(MakePanicImage(state, code), rejected, &info, &error))
<< std::hex << state << ' ' << code;
EXPECT_EQ(error, "bootstrap ELF has an invalid panic symbol");
EXPECT_TRUE(rejected.bytes.empty());
}
// Adjacent words do not alias.
for (const auto [state, code] :
{std::pair{DATA_BASE + 4, DATA_BASE + 8}, std::pair{DATA_BASE + 8, DATA_BASE + 4}})
{
RecordingBus bus;
EXPECT_TRUE(LoadBootstrapELF(MakePanicImage(state, code), bus, &info, &error)) << error;
EXPECT_EQ(info.panic_state, state);
EXPECT_EQ(info.panic_code, code);
}
}
} // namespace
} // namespace IOS::LLE
@@ -0,0 +1,99 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <map>
#include <string>
#include <gtest/gtest.h>
#include "StarOSPanicCheck.h"
namespace StarletBootstrapProbe
{
namespace
{
// MEM1 words by address; absent words read as zero.
class FakeMem1
{
public:
std::map<u32, u32> words;
u32 operator()(u32 address) const
{
const auto it = words.find(address);
return it == words.end() ? 0 : it->second;
}
};
// The record as the loader leaves it, before the image runs.
FakeMem1 CreatedRecord()
{
FakeMem1 mem1;
mem1.words[SBR1_BASE] = SBR1_MAGIC;
mem1.words[SBR1_BASE + 0x04] = SBR1_VERSION;
mem1.words[SBR1_BASE + 0x08] = SBR1_SIZE;
mem1.words[SBR1_BASE + SBR1_EXI_OWNER] = SBR1_EXI_STARLET;
mem1.words[SBR1_BASE + SBR1_RELAY_STATUS] = SBR1_RELAY_JUMPING;
return mem1;
}
// The record once StarOS's panic path has published it.
FakeMem1 PanicRecord()
{
FakeMem1 mem1 = CreatedRecord();
mem1.words[SBR1_BASE + SBR1_STARLET_STATE] = SBR1_STATE_PANIC;
mem1.words[SBR1_BASE + SBR1_EXI_OWNER] = SBR1_EXI_PPC;
return mem1;
}
TEST(StarletProbePanicCheck, WaitsForTheCodeAfterTheGuard)
{
const FakeMem1 none;
EXPECT_EQ(UnmetPanicCondition(1, 1, SBR1Mode::None, none), "");
// The guard is stored before the code: a slice can end in between.
EXPECT_EQ(UnmetPanicCondition(1, 0, SBR1Mode::None, none), "__panic_code is 0, not 1");
EXPECT_EQ(UnmetPanicCondition(1, 2, SBR1Mode::None, none), "__panic_code is 2, not 1");
EXPECT_EQ(UnmetPanicCondition(0, 1, SBR1Mode::None, none), "__panic_state is 0, not 1");
EXPECT_EQ(UnmetPanicCondition(2, 1, SBR1Mode::None, none), "__panic_state is 2, not 1");
// The words come first, whatever the record says.
EXPECT_EQ(UnmetPanicCondition(1, 0, SBR1Mode::Record, PanicRecord()),
"__panic_code is 0, not 1");
}
TEST(StarletProbePanicCheck, RecordNeedsPanicStateReasonAndEXIBack)
{
EXPECT_EQ(UnmetPanicCondition(1, 1, SBR1Mode::Record, PanicRecord()), "");
// Panic words set, record not yet published.
EXPECT_EQ(UnmetPanicCondition(1, 1, SBR1Mode::Record, CreatedRecord()),
"SBR1 STARLET_STATE is 0, not 5");
FakeMem1 running = PanicRecord();
running.words[SBR1_BASE + SBR1_STARLET_STATE] = 2;
EXPECT_EQ(UnmetPanicCondition(1, 1, SBR1Mode::Record, running), "SBR1 STARLET_STATE is 2, not 5");
FakeMem1 reason = PanicRecord();
reason.words[SBR1_BASE + SBR1_FATAL_REASON] = 3;
EXPECT_EQ(UnmetPanicCondition(1, 1, SBR1Mode::Record, reason), "SBR1 FATAL_REASON is 3, not 0");
// State published, EXI not yet handed back to the PPC.
FakeMem1 exi = PanicRecord();
exi.words[SBR1_BASE + SBR1_EXI_OWNER] = SBR1_EXI_STARLET;
EXPECT_EQ(UnmetPanicCondition(1, 1, SBR1Mode::Record, exi), "SBR1 EXI_OWNER is 2, not 1");
}
TEST(StarletProbePanicCheck, AbsentRecordAreaStaysZero)
{
EXPECT_EQ(UnmetPanicCondition(1, 1, SBR1Mode::Absent, FakeMem1{}), "");
for (const u32 offset : {0x0u, SBR1_STARLET_STATE, SBR1_EXI_OWNER, SBR1_SIZE - 4})
{
FakeMem1 written;
written.words[SBR1_BASE + offset] = 5;
EXPECT_EQ(UnmetPanicCondition(1, 1, SBR1Mode::Absent, written),
fmt::format("SBR1 area word +{:#x} is 0x5 without a record", offset));
}
// Words outside the area are not the record's.
FakeMem1 outside;
outside.words[SBR1_BASE - 4] = 5;
outside.words[SBR1_BASE + SBR1_SIZE] = 5;
EXPECT_EQ(UnmetPanicCondition(1, 1, SBR1Mode::Absent, outside), "");
// Without an SBR1 option, a written area is not checked.
EXPECT_EQ(UnmetPanicCondition(1, 1, SBR1Mode::None, PanicRecord()), "");
}
} // namespace
} // namespace StarletBootstrapProbe
@@ -0,0 +1,89 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <functional>
#include <string>
#include <fmt/format.h>
#include "Common/CommonTypes.h"
// What StarletBootstrapProbe requires of a finished StarOS panic path (StarOS ADR-0014 R6). The
// path sets __panic_state first, then __panic_code, then publishes the SBR1 boot record when
// there is one, so the first nonzero guard can show it half done: the probe keeps running until
// every condition holds. Words come through a callback, so the check is tested without a CPU.
namespace StarletBootstrapProbe
{
// StarOS's SBR1 boot record in MEM1 (StarOS crates/boot-contract, report.rs).
enum class SBR1Mode
{
None,
// --sbr1-record: the probe creates the record as the loader does.
Record,
// --sbr1-absent: no record; its area must stay zero.
Absent,
};
constexpr u32 SBR1_BASE = 0x00200000;
constexpr u32 SBR1_SIZE = 0x1000;
constexpr u32 SBR1_MAGIC = 0x53425231;
constexpr u32 SBR1_VERSION = 1;
// Word offsets.
constexpr u32 SBR1_EXI_OWNER = 0x0c;
constexpr u32 SBR1_RELAY_STATUS = 0x10;
constexpr u32 SBR1_STARLET_STATE = 0x18;
constexpr u32 SBR1_FATAL_REASON = 0x1c;
constexpr u32 SBR1_FAULT_CODE = 0x20;
constexpr u32 SBR1_RING_LENGTH = 0x2c;
constexpr u32 SBR1_RING_DROPPED = 0x30;
// Values.
constexpr u32 SBR1_EXI_PPC = 1;
constexpr u32 SBR1_EXI_STARLET = 2;
constexpr u32 SBR1_RELAY_JUMPING = 4;
constexpr u32 SBR1_STATE_PANIC = 5;
// __panic_state once the path is entered; __panic_code of a Rust panic.
constexpr u32 PANIC_ENTERED = 1;
constexpr u32 PANIC_CODE_RUST = 1;
// The first condition of a finished panic path that does not hold, or an empty string:
// - __panic_state 1 and __panic_code 1;
// - with --sbr1-record: STARLET_STATE 5 (Panic), FATAL_REASON 0 and EXI_OWNER 1 (PPC);
// - with --sbr1-absent: every word of the record area still zero.
// read_mem1 returns the big-endian MEM1 word at an address.
inline std::string UnmetPanicCondition(u32 panic_state, u32 panic_code, SBR1Mode sbr1,
const std::function<u32(u32)>& read_mem1)
{
if (panic_state != PANIC_ENTERED)
return fmt::format("__panic_state is {}, not {}", panic_state, PANIC_ENTERED);
if (panic_code != PANIC_CODE_RUST)
return fmt::format("__panic_code is {}, not {}", panic_code, PANIC_CODE_RUST);
if (sbr1 == SBR1Mode::Record)
{
const struct
{
const char* name;
u32 offset;
u32 expected;
} fields[] = {{"STARLET_STATE", SBR1_STARLET_STATE, SBR1_STATE_PANIC},
{"FATAL_REASON", SBR1_FATAL_REASON, 0},
{"EXI_OWNER", SBR1_EXI_OWNER, SBR1_EXI_PPC}};
for (const auto& field : fields)
{
const u32 value = read_mem1(SBR1_BASE + field.offset);
if (value != field.expected)
return fmt::format("SBR1 {} is {}, not {}", field.name, value, field.expected);
}
}
if (sbr1 == SBR1Mode::Absent)
{
for (u32 offset = 0; offset < SBR1_SIZE; offset += 4)
{
const u32 value = read_mem1(SBR1_BASE + offset);
if (value != 0)
return fmt::format("SBR1 area word +{:#x} is {:#x} without a record", offset, value);
}
}
return {};
}
} // namespace StarletBootstrapProbe
@@ -1,6 +1,7 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <cstdlib>
#include <fstream>
#include <iostream>
@@ -16,9 +17,12 @@
#include "Core/IOS/Starlet/BootstrapELF.h"
#include "Core/IOS/Starlet/StarletMemory.h"
#include "Core/System.h"
#include "StarOSPanicCheck.h"
namespace
{
using namespace StarletBootstrapProbe;
enum class ExpectedOutcome
{
Idle,
@@ -31,6 +35,9 @@ enum class ExpectedOutcome
// Exit status of a run stopped by a Dolphin alert (1: failed expectation, 2: usage).
constexpr int ALERT_EXIT_STATUS = 3;
// Slices run once a StarOS panic path looks finished, to see that it stays so.
constexpr int PANIC_SETTLE_SLICES = 2;
// Dolphin's default handler shows an alert in a modal MessageBox on Windows, which blocks the
// probe until someone closes it. Report the alert on stderr and fail the run at once instead.
bool FailOnAlert(const char* caption, const char* text, bool, Common::MsgType)
@@ -40,7 +47,8 @@ bool FailOnAlert(const char* caption, const char* text, bool, Common::MsgType)
std::_Exit(ALERT_EXIT_STATUS);
}
bool Run(const std::string& path, bool jit, ExpectedOutcome expected)
bool Run(const std::string& path, bool jit, bool mmu_faults, SBR1Mode sbr1,
ExpectedOutcome expected)
{
std::ifstream file(path, std::ios::binary | std::ios::ate);
if (!file || file.tellg() <= 0 || file.tellg() > 16 * 1024 * 1024)
@@ -74,8 +82,30 @@ bool Run(const std::string& path, bool jit, ExpectedOutcome expected)
core.SetJitEnabled(jit);
core.Reset(info.entry);
core.SetBigEndian(true);
// StarOS MMU-001: translation, domain, permission and alignment aborts (off by default).
core.SetMMUFaultsEnabled(mmu_faults);
constexpr u32 TRACE_MAGIC = 0x52455631;
constexpr u32 TRACE_CAPACITY = 1024;
auto& mem1 = system.GetMemory();
const auto read_mem1 = [&mem1](u32 address) { return mem1.Read_U32(address); };
const auto panic_unmet = [&] {
return UnmetPanicCondition(memory.Read32(info.panic_state), memory.Read32(info.panic_code),
sbr1, read_mem1);
};
// StarOS's panic path writes no trace line. Print the trace published so far, for the
// scenario's ordered checks, and the panic words.
const auto report_panic = [&] {
const u32 length = memory.Read32(info.trace) == TRACE_MAGIC ?
std::min(memory.Read32(info.trace + 4), TRACE_CAPACITY) :
0;
for (u32 index = 0; index < length; ++index)
std::cout << static_cast<char>(memory.Read8(info.trace + 8 + index));
std::cout << "PC=0x" << std::hex << core.GetRegister(15) << std::dec
<< " instructions=" << core.GetExecutedInstructions() << '\n';
std::cerr << "Starlet panic state " << memory.Read32(info.panic_state) << " code "
<< memory.Read32(info.panic_code) << '\n';
};
int panic_settled = 0;
for (int slice = 0; slice < 512; ++slice)
{
core.SetIRQLine(system.GetWiiIPC().IsStarletIRQAsserted());
@@ -90,6 +120,26 @@ bool Run(const std::string& path, bool jit, ExpectedOutcome expected)
return expected == ExpectedOutcome::UndefinedFault && fault_code == 1 &&
memory.Read32(info.fault_lr) != 0 && memory.Read32(info.fault_spsr) != 0;
}
// StarOS's panic path sets its guard word first, then its code, then publishes the SBR1
// record when there is one, and halts. An expected panic passes once all of that holds
// (UnmetPanicCondition) and still holds PANIC_SETTLE_SLICES slices later.
if (info.panic_state != 0 && memory.Read32(info.panic_state) != 0)
{
if (expected != ExpectedOutcome::Panic)
{
report_panic();
return false;
}
if (!panic_unmet().empty())
{
panic_settled = 0;
continue;
}
if (++panic_settled <= PANIC_SETTLE_SLICES)
continue;
report_panic();
return true;
}
if (memory.Read32(info.trace) != TRACE_MAGIC)
continue;
const u32 length = memory.Read32(info.trace + 4);
@@ -139,6 +189,14 @@ bool Run(const std::string& path, bool jit, ExpectedOutcome expected)
expected == ExpectedOutcome::Idle && trace.find("REV:BOOT:IDLE\n") != std::string::npos;
return startup_ok && outcome_ok;
}
if (info.panic_state != 0 && memory.Read32(info.panic_state) != 0)
{
report_panic();
const std::string unmet = panic_unmet();
std::cerr << "StarOS panic path unfinished after 2097152 ARM cycles: "
<< (unmet.empty() ? "it ended inside the settle slices" : unmet) << '\n';
return false;
}
std::cerr << "No terminal bootstrap marker after 2097152 ARM cycles, PC=0x" << std::hex
<< core.GetRegister(15) << std::dec << '\n';
return false;
@@ -148,13 +206,16 @@ bool Run(const std::string& path, bool jit, ExpectedOutcome expected)
int main(int argc, char** argv)
{
Common::RegisterMsgAlertHandler(FailOnAlert);
if (argc < 2 || argc > 4)
if (argc < 2 || argc > 6)
{
std::cerr << "Usage: StarletBootstrapProbe <ELF> [--jit] "
std::cerr << "Usage: StarletBootstrapProbe <ELF> [--jit] [--mmu-faults] "
"[--sbr1-record|--sbr1-absent] "
"[--expect-panic|--expect-fatal-data|--expect-undefined-fault|--expect-phase2]\n";
return 2;
}
bool jit = false;
bool mmu_faults = false;
SBR1Mode sbr1 = SBR1Mode::None;
ExpectedOutcome expected = ExpectedOutcome::Idle;
bool outcome_selected = false;
for (int index = 2; index < argc; ++index)
@@ -162,6 +223,17 @@ int main(int argc, char** argv)
const std::string_view option = argv[index];
if (option == "--jit")
jit = true;
else if (option == "--mmu-faults")
mmu_faults = true;
else if (option == "--sbr1-record" || option == "--sbr1-absent")
{
if (sbr1 != SBR1Mode::None)
{
std::cerr << "Select only one SBR1 mode\n";
return 2;
}
sbr1 = option == "--sbr1-record" ? SBR1Mode::Record : SBR1Mode::Absent;
}
else if (option == "--expect-panic" || option == "--expect-fatal-data" ||
option == "--expect-undefined-fault" || option == "--expect-phase2")
{
@@ -189,7 +261,35 @@ int main(int argc, char** argv)
auto& system = Core::System::GetInstance();
system.SetIsWii(true);
system.GetMemory().Init();
const bool passed = Run(argv[1], jit, expected);
auto& memory = system.GetMemory();
if (sbr1 == SBR1Mode::Record)
{
// As the Broadway loader and the ARM relay leave StarOS's SBR1 boot record before the image
// runs (StarOS docs/contracts/wii-dev-boot.md): header, the relay at its last step, EXI
// channel 1 given to the Starlet, the magic written last.
memory.Write_U32(SBR1_VERSION, SBR1_BASE + 0x04);
memory.Write_U32(SBR1_SIZE, SBR1_BASE + 0x08);
memory.Write_U32(SBR1_EXI_STARLET, SBR1_BASE + SBR1_EXI_OWNER);
memory.Write_U32(SBR1_RELAY_JUMPING, SBR1_BASE + SBR1_RELAY_STATUS);
memory.Write_U32(SBR1_MAGIC, SBR1_BASE);
}
const bool passed = Run(argv[1], jit, mmu_faults, sbr1, expected);
if (sbr1 != SBR1Mode::None)
{
// The record's summary after the run; with --sbr1-absent every word must still be zero (an
// expected panic fails otherwise).
std::cerr << std::hex << "SBR1 magic 0x" << memory.Read_U32(SBR1_BASE) << std::dec
<< " state " << memory.Read_U32(SBR1_BASE + SBR1_STARLET_STATE) << " reason "
<< memory.Read_U32(SBR1_BASE + SBR1_FATAL_REASON) << " owner "
<< memory.Read_U32(SBR1_BASE + SBR1_EXI_OWNER) << " fault "
<< memory.Read_U32(SBR1_BASE + SBR1_FAULT_CODE) << " ring "
<< memory.Read_U32(SBR1_BASE + SBR1_RING_LENGTH) << " dropped "
<< memory.Read_U32(SBR1_BASE + SBR1_RING_DROPPED);
u32 written = 0;
for (u32 offset = 0; offset < SBR1_SIZE; offset += 4)
written += memory.Read_U32(SBR1_BASE + offset) != 0;
std::cerr << " nonzero_words " << written << '\n';
}
system.GetMemory().Shutdown();
return passed ? 0 : 1;
}