From 9b493a1e161fe7492b48a78f83ba4a65855b0c56 Mon Sep 17 00:00:00 2001 From: Yaya48 Date: Tue, 13 Oct 2026 21:09:18 +0200 Subject: [PATCH] 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 --- Source/Core/Core/IOS/Starlet/ARMCore.cpp | 443 ++++++++++++++-- Source/Core/Core/IOS/Starlet/ARMCore.h | 55 ++ Source/Core/Core/IOS/Starlet/ARMJitX64.cpp | 89 +++- Source/Core/Core/IOS/Starlet/ARMJitX64.h | 7 + .../Core/IOS/Starlet/ARMCoreTest.cpp | 492 ++++++++++++++++++ 5 files changed, 1052 insertions(+), 34 deletions(-) diff --git a/Source/Core/Core/IOS/Starlet/ARMCore.cpp b/Source/Core/Core/IOS/Starlet/ARMCore.cpp index a17d6c337e..3a65087326 100644 --- a/Source/Core/Core/IOS/Starlet/ARMCore.cpp +++ b/Source/Core/Core/IOS/Starlet/ARMCore.cpp @@ -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(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 registers = m_registers; + const std::array 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 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((static_cast(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((high << 8) | read8(address + 1)); + } return m_big_endian ? value : static_cast((value >> 8) | (value << 8)); } u32 ARMCore::Read32(u32 address) const { - const u32 value = - (address & 3) == 0 ? - m_bus.Read32(TranslateVirtualAddress(address)) : - (static_cast(m_bus.Read8(TranslateVirtualAddress(address))) << 24) | - (static_cast(m_bus.Read8(TranslateVirtualAddress(address + 1))) << 16) | - (static_cast(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((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(value >> 8)); - m_bus.Write8(TranslateVirtualAddress(address + 1), static_cast(value)); + WriteByte(address, static_cast(value >> 8)); + WriteByte(address + 1, static_cast(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(value >> 24)); - m_bus.Write8(TranslateVirtualAddress(address + 1), static_cast(value >> 16)); - m_bus.Write8(TranslateVirtualAddress(address + 2), static_cast(value >> 8)); - m_bus.Write8(TranslateVirtualAddress(address + 3), static_cast(value)); + WriteByte(address, static_cast(value >> 24)); + WriteByte(address + 1, static_cast(value >> 16)); + WriteByte(address + 2, static_cast(value >> 8)); + WriteByte(address + 3, static_cast(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 diff --git a/Source/Core/Core/IOS/Starlet/ARMCore.h b/Source/Core/Core/IOS/Starlet/ARMCore.h index e2ae487f3b..a457b4ec6e 100644 --- a/Source/Core/Core/IOS/Starlet/ARMCore.h +++ b/Source/Core/Core/IOS/Starlet/ARMCore.h @@ -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 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 m_jit; #endif diff --git a/Source/Core/Core/IOS/Starlet/ARMJitX64.cpp b/Source/Core/Core/IOS/Starlet/ARMJitX64.cpp index bee36fcbdb..ca296d47cf 100644 --- a/Source/Core/Core/IOS/Starlet/ARMJitX64.cpp +++ b/Source/Core/Core/IOS/Starlet/ARMJitX64.cpp @@ -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(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(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(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); } diff --git a/Source/Core/Core/IOS/Starlet/ARMJitX64.h b/Source/Core/Core/IOS/Starlet/ARMJitX64.h index 35e0a84bd2..7dd5924356 100644 --- a/Source/Core/Core/IOS/Starlet/ARMJitX64.h +++ b/Source/Core/Core/IOS/Starlet/ARMJitX64.h @@ -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; diff --git a/Source/UnitTests/Core/IOS/Starlet/ARMCoreTest.cpp b/Source/UnitTests/Core/IOS/Starlet/ARMCoreTest.cpp index 9a48f571a1..903f3da7eb 100644 --- a/Source/UnitTests/Core/IOS/Starlet/ARMCoreTest.cpp +++ b/Source/UnitTests/Core/IOS/Starlet/ARMCoreTest.cpp @@ -6,7 +6,9 @@ #include #include #include +#include #include +#include #include #include @@ -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 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 +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