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]>
This commit is contained in:
Yaya48andClaude Opus 5.5 committed 2026-10-13 21:09:18 +02:00
1 parent f9193539fb
commit 9b493a1e16
5 files changed
+1052 -34

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 separate I and D TLBs share one software TLB, so each operation reaches both.
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
+55
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();
@@ -386,6 +429,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;
@@ -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