/* * Copyright (C) 2012-2014 Apple Inc. All rights reserved. * * This document is the property of Apple Inc. * It is considered confidential and proprietary. * * This document may not be reproduced or transmitted in any form, * in whole or in part, without the express written permission of * Apple Inc. */ /* * See docs/mmu64.txt for an in-depth version of what is going on here. * Short version: * - H6, H7 use 4KB page granule. * - H8 uses 16KB page granule (4KB not supported by hardware). * - Start at L1 for 4KB page granule. * - Start at L2 for 16KB page granule. * - Set virtual memory limit so the first level (L0/L1) has just two entries. * - ...then never touch the top level so we don't suffer from NS/S coherency. */ #include #include #include #include #include #include #include #ifndef TEST # include # include # include # include #endif #define LAST_TABLE_LEVEL 3 extern uint64_t arm_read_sctlr(); // Start level page table entries static uint64_t *tt_top_level; // Allocation area and current allocation pointer. static addr_t tt_alloc_start, tt_alloc_next, tt_alloc_end; #ifdef TEST // Mock test interfaces for mmu.c, implemented in mmu_test.c void mmu_printf(int verbosity, const char *fmt, ...); unsigned get_page_granule_shift(void); void mmu_get_memmap(addr_t *page_tables_base, size_t *page_tables_size); void platform_mmu_setup(bool resume); void get_table_entry_hook(unsigned level, addr_t table_base, size_t index); void set_table_entry_hook(unsigned level, addr_t table_base, size_t index, uint64_t value); #else // !defined(TEST) // Real interfaces, designed to be inlined. These have no host-side coverage, // so they should be no-ops or simple constants. // No printf spew. static void mmu_printf(int verbosity, const char *fmt, ...) { (void) verbosity; (void) fmt; } // Constants. static unsigned get_page_granule_shift(void) { return PAGE_GRANULE_SHIFT; } static void mmu_get_memmap(addr_t *page_tables_base, size_t *page_tables_size) { *page_tables_base = PAGE_TABLES_BASE; *page_tables_size = PAGE_TABLES_SIZE; } // Remove expensive validity checking. static void get_table_entry_hook(unsigned level, addr_t table_base, size_t index) { (void) level; (void) table_base; (void) index; } static void set_table_entry_hook(unsigned level, addr_t table_base, size_t index, uint64_t value) { (void) level; (void) table_base; (void) index; (void) value; } #endif // !defined(TEST) #define mmu_spew(x...) mmu_printf(1, x) #define mmu_extra_spew(x...) mmu_printf(2, x) static void assert_valid_level(unsigned level) { ASSERT((get_page_granule_shift() == 12 || level >= 1) && level <= LAST_TABLE_LEVEL); } static bool level_permits_blocks(unsigned level) { assert_valid_level(level); // No granule supports L0 blocks. // 4KB granule supports L1 blocks. // All granules support L2 and L3 blocks. return (level == 1 && get_page_granule_shift() == 12) || level >= 2; } static uint64_t get_table_entry(unsigned level, addr_t table_base, size_t index) { get_table_entry_hook(level, table_base, index); mmu_extra_spew("get_table_entry(%u, 0x%llx, %u) ", level, (unsigned long long) table_base, (unsigned) index); // No possible aliases, known aligned. uint64_t result = *(uint64_t *) (table_base + index * 8); mmu_extra_spew("= 0x%016llx\n", (unsigned long long) result); return result; } static void set_table_entry(unsigned level, addr_t table_base, size_t index, uint64_t value) { set_table_entry_hook(level, table_base, index, value); mmu_extra_spew("set_table_entry(%u, 0x%llx, %u, 0x%llx)\n", level, (unsigned long long) table_base, (unsigned) index, (unsigned long long) value); *((volatile uint64_t *) (table_base + index * 8)) = value; } static unsigned get_start_level(void) { switch (get_page_granule_shift()) { case 12: return 1; case 14: return 2; case 16: return 2; default: panic("Bad granule"); } } static unsigned get_virt_address_bits(void) { switch (get_page_granule_shift()) { case 12: return 36; case 14: return 36; case 16: return 36; default: panic("Bad granule"); } } static uint64_t get_mair(void) { return (MAIR_WRITEBACK << MAIR_ATTR_SHIFT(CACHE_ATTRINDX_WRITEBACK)) | (MAIR_DISABLE << MAIR_ATTR_SHIFT(CACHE_ATTRINDX_DISABLE)); } static uint64_t get_tcr(void) { // Form the contents of the TCR register for the selected // virtual address limit and page granule. uint64_t tcr; uint32_t txsz = 64 - get_virt_address_bits(); #if WITH_EL3 switch (get_page_granule_shift()) { case 12: tcr = TCR_ELx_TG0_GRANULE_4KB; break; case 14: tcr = TCR_ELx_TG0_GRANULE_16KB; break; case 16: tcr = TCR_ELx_TG0_GRANULE_64KB; break; default: panic("Bad granule"); } tcr |= TCR_EL3_PS_36BITS | TCR_ELx_SH0_OUTER | TCR_ELx_ORGN0_WRITEBACK | TCR_ELx_IRGN0_WRITEBACK | (txsz << TCR_ELx_T0SZ_SHIFT); #else switch (get_page_granule_shift()) { case 12: tcr = TCR_ELx_TG0_GRANULE_4KB | TCR_EL1_TG1_GRANULE_4KB; break; case 14: tcr = TCR_ELx_TG0_GRANULE_16KB | TCR_EL1_TG1_GRANULE_16KB; break; case 16: tcr = TCR_ELx_TG0_GRANULE_64KB | TCR_EL1_TG1_GRANULE_64KB; break; default: panic("Bad granule"); } tcr |= TCR_EL1_IPS_36BITS | TCR_ELx_SH0_OUTER | TCR_ELx_ORGN0_WRITEBACK | TCR_ELx_IRGN0_WRITEBACK | (txsz << TCR_ELx_T0SZ_SHIFT) | TCR_EL1_EPD1_TTBR1_DISABLED | TCR_EL1_SH1_OUTER | TCR_EL1_ORGN1_WRITEBACK | TCR_EL1_IRGN1_WRITEBACK | (txsz << TCR_EL1_T1SZ_SHIFT); #endif return tcr; } static uint64_t get_table_flags(void) { // All table walks after the start level are non-secure. return ARM_LPAE_NSTABLE | ARM_LPAE_TYPE_TABLE | ARM_LPAE_VALID; } static uint64_t get_block_flags(unsigned level, arm_mmu_extended_attr_t attr) { assert_valid_level(level); RELEASE_ASSERT(level_permits_blocks(level)); bool normal, writable, executable; switch (attr) { case kARMMMUDeviceR: normal = false; writable = false; executable = false; break; case kARMMMUDeviceRX: normal = false; writable = false; executable = true; break; case kARMMMUDeviceRW: normal = false; writable = true; executable = false; break; case kARMMMUNormalR: normal = true; writable = false; executable = false; break; case kARMMMUNormalRX: normal = true; writable = false; executable = true; break; case kARMMMUNormalRW: normal = true; writable = true; executable = false; break; default: panic("Bad attr %d", (int) attr); } // Always set Access Flag, Non-Secure and Valid. uint64_t value = ARM_LPAE_AF | ARM_LPAE_NS | ARM_LPAE_VALID; if (level < LAST_TABLE_LEVEL) { value |= ARM_LPAE_TYPE_L0L1L2BLOCK; } else { value |= ARM_LPAE_TYPE_L3BLOCK; } uint64_t sh, attr_indx; if (normal) { sh = SH_OUTER_MEMORY; attr_indx = CACHE_ATTRINDX_WRITEBACK; } else { sh = SH_NONE; attr_indx = CACHE_ATTRINDX_DISABLE; } value |= ARM_LPAE_SH(sh); value |= ARM_LPAE_ATTRINDX(attr_indx); uint64_t ap = writable ? 0 : 2; value |= ARM_LPAE_AP(ap); if (!executable) { value |= ARM_LPAE_PXN | ARM_LPAE_XN; } return value; } static uint64_t get_virt_size_bytes(void) { return 1ULL << get_virt_address_bits(); } static uint64_t get_phys_size_bytes(void) { return 1ULL << 40; } static size_t get_level_shift(unsigned level) { assert_valid_level(level); switch (get_page_granule_shift()) { case 12: return 39 - (level * 9); case 14: return 47 - (level * 11); case 16: return 55 - (level * 13); default: panic("Granule 2^%u not supported\n", get_page_granule_shift()); } } static size_t get_level_entries(unsigned level) { assert_valid_level(level); // 1 granule per sub-table, 8 bytes per entry. return ((size_t) 1) << (get_page_granule_shift() - 3); } static addr_t tt_alloc(unsigned level) { // Allocate a translation table. The start level was // allocated as tt_top_level in arm_mmu_init and should not be // requested to be allocated. All levels are the // same size: one granule. RELEASE_ASSERT(level > get_start_level() && level <= LAST_TABLE_LEVEL); size_t bytes = ((size_t) 1) << get_page_granule_shift(); mmu_spew("Allocate L%u size 0x%llx\n", level, (unsigned long long) bytes); if (bytes > tt_alloc_end || tt_alloc_next > tt_alloc_end - bytes) { panic("Out of table allocation space L%u\n", level); } addr_t ret = tt_alloc_next; // Alignment is guaranteed by arm_mmu_init() and all // allocations being the same size (1 granule), but it's cheap // to double-check allocation alignment. if ((ret & (bytes - 1)) != 0) { panic("Bad aligned pte alloc 0x%llx\n", (unsigned long long) ret); } tt_alloc_next += bytes; mmu_spew("Allocated L%u size 0x%llx at 0x%llx\n", level, (unsigned long long) bytes, (unsigned long long) ret); return ret; } static size_t map_at_level(unsigned level, addr_t table_base, addr_t vaddr, addr_t paddr, size_t size, arm_mmu_extended_attr_t attr, bool blank_table) { mmu_spew("map_at_level(level=%u,\n" " table_base=0x%llx,\n" " vaddr=0x%llx,\n" " paddr=0x%llx,\n" " size=0x%llx,\n" " attr=%u\n", level, (unsigned long long) table_base, (unsigned long long) vaddr, (unsigned long long) paddr, (unsigned long long) size, (unsigned) attr); // Check this mapping is bounded by virtual and physical address limits. assert_valid_level(level); RELEASE_ASSERT(size <= get_virt_size_bytes() && vaddr <= get_virt_size_bytes() - size); RELEASE_ASSERT(size <= get_phys_size_bytes() && paddr <= get_phys_size_bytes() - size); // Calculate constants about this translation level. size_t level_shift = get_level_shift(level); size_t level_entries = get_level_entries(level); size_t level_size = 1ULL << level_shift; size_t level_mask = level_size - 1; bool permit_block = level_permits_blocks(level); size_t index = (vaddr >> level_shift) & (level_entries - 1); mmu_spew("shift:%d entries:%d size:0x%llx mask:0x%llx " "block:%d index:%d\n", (int) level_shift, (int) level_entries, (unsigned long long) level_size, (unsigned long long) level_mask, (int) permit_block, (int) index); // Can we make block entries here? Must be permitted at this // level, have enough bytes remaining, and both virtual and // physical addresses aligned to a block. if (permit_block && size >= level_size && ((vaddr | paddr) & level_mask) == 0) { // Map contiguous blocks. mmu_spew("Array of blocks\n"); size_t mapped_size = 0; size_t remaining_size = size; // Common block flags for all entries at this level. uint64_t block_flags = get_block_flags(level, attr); while (remaining_size >= level_size && index < level_entries) { // Check the existing entry is unused. uint64_t entry = get_table_entry(level, table_base, index); if (entry != 0) { // Not difficult but no use case. panic("Remapping an existing L%u at 0x%016llx", level, (unsigned long long) vaddr); } // Form an entry. uint64_t ent = block_flags | paddr; // Write a block entry to the table. set_table_entry(level, table_base, index, ent); // Next index. mapped_size += level_size; remaining_size -= level_size; paddr += level_size; vaddr += level_size; ++index; } // Must make forward progress... and not too much. RELEASE_ASSERT(mapped_size > 0 && mapped_size <= size); return mapped_size; } else { // Sub-divide into a next level table. mmu_spew("Sub-divide\n"); // Only so much granularity available. RELEASE_ASSERT(level < LAST_TABLE_LEVEL); // Get the existing entry. uint64_t entry = blank_table ? 0 : get_table_entry(level, table_base, index); if ((entry & ARM_LPAE_VALID) && (entry & ARM_LPAE_TYPE_MASK) == ARM_LPAE_TYPE_L0L1L2BLOCK) { // Not hard but no use case. panic("Breaking down blocks not implemented"); } unsigned sub_level = level + 1; bool blank_sub_table; addr_t sub_base; if (entry & ARM_LPAE_VALID) { mmu_spew("Existing entry\n"); sub_base = entry & ARM_LPAE_TABLE_MASK; blank_sub_table = false; } else { mmu_spew("Allocating sub-level table\n"); sub_base = tt_alloc(sub_level); // Form an entry. uint64_t ent = get_table_flags() | sub_base; // Store an entry pointing to the sub-level table. set_table_entry(level, table_base, index, ent); blank_sub_table = true; } // Recurse at next level. return map_at_level(sub_level, sub_base, vaddr, paddr, size, attr, blank_sub_table); } } void arm_mmu_map_range(addr_t vaddr, addr_t paddr, size_t size, arm_mmu_extended_attr_t attr) { // Ensure all addresses and sizes are at least L3 aligned. size_t l3_align = ((size_t) 1) << get_level_shift(LAST_TABLE_LEVEL); RELEASE_ASSERT(((vaddr | paddr | size) & (l3_align - 1)) == 0); while (size > 0) { // Map at the top level first, allowing the mapper function // to break it down into smaller chunks as necessary. size_t mapped = map_at_level(get_start_level(), (addr_t) tt_top_level, vaddr, paddr, size, attr, false); RELEASE_ASSERT(mapped <= size); size -= mapped; vaddr += mapped; paddr += mapped; } if ((arm_read_sctlr() & SCTLR_M_ENABLED) != 0) { arm_flush_tlbs(); } } void arm_mmu_map_ro(addr_t addr, size_t size) { arm_mmu_map_range(addr, addr, size, kARMMMUNormalR); } void arm_mmu_map_rw(addr_t addr, size_t size) { arm_mmu_map_range(addr, addr, size, kARMMMUNormalRW); } void arm_mmu_map_rx(addr_t addr, size_t size) { arm_mmu_map_range(addr, addr, size, kARMMMUNormalRX); } void arm_mmu_map_device_ro(addr_t addr, size_t size) { arm_mmu_map_range(addr, addr, size, kARMMMUDeviceR); } void arm_mmu_map_device_rw(addr_t addr, size_t size) { arm_mmu_map_range(addr, addr, size, kARMMMUDeviceRW); } void arm_mmu_map_device_rx(addr_t addr, size_t size) { arm_mmu_map_range(addr, addr, size, kARMMMUDeviceRX); } void arm_mmu_init(bool resume) { // Setup memory attributes. arm_write_mair(get_mair()); // Setup translation control register. arm_write_tcr(get_tcr()); /* * Sub-level page table allocations starts in the book-keeping * region after all stacks, rounded up. Allocation rounded down. */ size_t granule_mask = (((size_t) 1) << get_page_granule_shift()) - 1; addr_t page_tables_base; size_t page_tables_size; mmu_get_memmap(&page_tables_base, &page_tables_size); RELEASE_ASSERT((page_tables_base & granule_mask) == 0); RELEASE_ASSERT((page_tables_size & granule_mask) == 0); tt_alloc_start = page_tables_base; tt_alloc_next = tt_alloc_start; tt_alloc_end = (page_tables_base + page_tables_size) & ~granule_mask; // allocate top-level page table tt_top_level = (uint64_t *)tt_alloc_start; tt_alloc_next += 1 << get_page_granule_shift(); // Defer to platform code for the memory map setup. platform_mmu_setup(resume); // Write the translation table base. arm_write_ttbr0(tt_top_level); // Flush TLBs. arm_flush_tlbs(); } #ifdef TEST // static->global stubs for mmu_test.c uint64_t *mmu_get_tt_top_level(void) { return tt_top_level; } unsigned mmu_get_start_level(void) { return get_start_level(); } uint64_t mmu_get_tcr(void) { return get_tcr(); } uint64_t mmu_tt_alloc(unsigned level) { return tt_alloc(level); } size_t mmu_get_level_entries(unsigned level) { return get_level_entries(level); } uint64_t mmu_get_table_flags(void) { return get_table_flags(); } uint64_t mmu_get_block_flags(unsigned level, arm_mmu_extended_attr_t attr) { return get_block_flags(level, attr); } size_t mmu_map_at_level(unsigned level, addr_t table_base, addr_t vaddr, addr_t paddr, size_t size, arm_mmu_extended_attr_t attr) { return map_at_level(level, table_base, vaddr, paddr, size, attr, false); } void mmu_get_tt_alloc_range(addr_t *start, addr_t *next, addr_t *end) { *start = tt_alloc_start; *next = tt_alloc_next; *end = tt_alloc_end; } void mmu_reset_state(void) { tt_top_level = 0; tt_alloc_start = 0; tt_alloc_next = 0; tt_alloc_end = 0; } #endif // defined(TEST)