Chapter 18: The MMU
- What the Memory Management Unit (MMU) is and how it works
- How the MMU performs address translation on every memory access
- Key ARM64 MMU control registers: TCR_EL1, MAIR_EL1, SCTLR_EL1
- How to enable and configure the MMU
- How the MMU enforces memory permissions and cache policies
- Our implementation of MMU initialization
18.1 What is the MMU?
The Memory Management Unit (MMU) is a hardware component in the CPU that translates virtual addresses to physical addresses on every memory access. It sits between the CPU core and the memory bus, intercepting every load and store instruction. The MMU is controlled by system registers and page tables in memory.
Without the MMU, all addresses passed to load/store instructions are physical addresses. With the MMU enabled, every address is a virtual address that must be translated.
18.2 MMU Control Registers
Three main registers control MMU behavior on ARM64:
TCR_EL1: Translation Control Register
TCR_EL1 configures the translation regime: page size, address width, and cacheability of page table walks.
#define TCR_TG0_4KB (0 << 30) /* Granule size for TTBR0: 4 KB */
#define TCR_TG1_4KB (2 << 30) /* Granule size for TTBR1: 4 KB */
#define TCR_T0SZ_48 (16 << 0) /* 48-bit VA space for TTBR0 (64-16=48) */
#define TCR_T1SZ_48 (16 << 16) /* 48-bit VA space for TTBR1 */
#define TCR_IRGN0_WBWA (1 << 8) /* Inner cacheability: write-back, write-allocate */
#define TCR_ORGN0_WBWA (1 << 10) /* Outer cacheability: write-back, write-allocate */
#define TCR_IRGN1_WBWA (1 << 24) /* Inner cacheability for TTBR1 */
#define TCR_ORGN1_WBWA (1 << 26) /* Outer cacheability for TTBR1 */
#define TCR_SH0_INNER (3 << 12) /* Shareability: inner shareable */
#define TCR_SH1_INNER (3 << 28) /* Shareability for TTBR1 */
#define TCR_IPS_40B (2 << 32) /* Physical address size: 40 bits (1 TB) */
void init_mmu(void) {
uint64_t tcr = TCR_TG0_4KB | TCR_TG1_4KB |
TCR_T0SZ_48 | TCR_T1SZ_48 |
TCR_IRGN0_WBWA | TCR_ORGN0_WBWA |
TCR_IRGN1_WBWA | TCR_ORGN1_WBWA |
TCR_SH0_INNER | TCR_SH1_INNER |
TCR_IPS_40B;
asm volatile("msr tcr_el1, %0" : : "r"(tcr));
}
MAIR_EL1: Memory Attribute Indirection Register
MAIR_EL1 maps 8 attribute indices (0-7) to specific memory types (device memory, normal cacheable, etc.). Page table entries reference these indices.
#define MAIR_ATTR(idx, attr) ((uint64_t)(attr) << ((idx) * 8))
#define MAIR_NORMAL_CACHE 0xFF /* Normal memory, WB, RW alloc */
#define MAIR_DEVICE_nGnRnE 0x00 /* Device memory, non-cacheable */
#define MAIR_NORMAL_NC 0x44 /* Normal memory, non-cacheable */
#define MAIR_VAL (MAIR_ATTR(0, MAIR_NORMAL_CACHE) | \
MAIR_ATTR(1, MAIR_DEVICE_nGnRnE) | \
MAIR_ATTR(2, MAIR_NORMAL_NC))
void init_mair(void) {
asm volatile("msr mair_el1, %0" : : "r"(MAIR_VAL));
}
SCTLR_EL1: System Control Register
SCTLR_EL1 contains the master enable bit for the MMU (bit 0, the M bit). Setting this bit enables the MMU.
#define SCTLR_MMU_ENABLE (1 << 0)
#define SCTLR_CACHE_ENABLE (1 << 2) /* Data cache enable */
#define SCTLR_ICACHE_ENABLE (1 << 12) /* Instruction cache enable */
#define SCTLR_SA0 (1 << 4) /* Stack alignment check EL0 */
#define SCTLR_SA (1 << 3) /* Stack alignment check EL1 */
#define SCTLR_I (1 << 12) /* Instruction cache enable */
#define SCTLR_SPAN (1 << 23) /* Stack pointer alignment */
void enable_mmu(void) {
uint64_t sctlr;
asm volatile("mrs %0, sctlr_el1" : "=r"(sctlr));
sctlr |= SCTLR_MMU_ENABLE | SCTLR_CACHE_ENABLE | SCTLR_ICACHE_ENABLE;
asm volatile("msr sctlr_el1, %0" : : "r"(sctlr));
isb(); /* Instruction synchronization barrier */
}
18.3 TTBR0_EL1 and TTBR1_EL1
ARM64 has two translation table base registers:
- TTBR0_EL1: used when the virtual address has bit 47 = 0 (lower half, 0x0000_0000_0000_0000 to 0x0000_FFFF_FFFF_FFFF)
- TTBR1_EL1: used when the virtual address has bit 47 = 1 (upper half, 0xFFFF_0000_0000_0000 to 0xFFFF_FFFF_FFFF_FFFF)
This split divides the 48-bit address space into two equal parts. Typically the kernel runs in the upper half (TTBR1) and user space runs in the lower half (TTBR0). Each process has its own TTBR0 value, while TTBR1 is the same for all processes (kernel mappings).
18.4 Enabling the MMU
The sequence to enable the MMU requires care to avoid a crash when the instruction pipeline suddenly starts using virtual addresses:
/* start.S: MMU initialization sequence */
enable_mmu:
/* Configure TCR, MAIR (from C functions) */
bl init_tcr_mair
/* Load kernel page table base into TTBR1 */
ldr x0, =kernel_page_table
msr ttbr1_el1, x0
/* Load identity-mapped page table into TTBR0
(only until we jump to the higher-half address) */
ldr x0, =identity_page_table
msr ttbr0_el1, x0
/* Ensure all previous memory accesses complete */
dsb ish
isb
/* Enable MMU, data cache, instruction cache */
mrs x0, sctlr_el1
orr x0, x0, #(1 << 0) /* M: MMU enable */
orr x0, x0, #(1 << 2) /* C: data cache enable */
orr x0, x0, #(1 << 12) /* I: instruction cache enable */
msr sctlr_el1, x0
isb
/* Now running with virtual addresses.
Jump to higher-half address and switch TTBR0 to user page table */
ldr x1, =high_half_entry
br x1
high_half_entry:
/* Set up kernel stack (now using virtual address) */
ldr x0, =kernel_stack_top
mov sp, x0
/* Clear TTBR0 (no user process running yet) */
msr ttbr0_el1, xzr
isb
/* Continue to C kernel main */
bl kernel_main
18.5 Memory Permissions and Domains
The MMU enforces access permissions from the page table entries:
| AP[2:1] | EL1 (Kernel) | EL0 (User) |
|---|---|---|
| 00 | Read/Write | No access |
| 01 | Read/Write | Read/Write |
| 10 | Read-Only | No access |
| 11 | Read-Only | Read-Only |
This means: user pages must have AP[1] = 1 (the PTE_AP_EL0 bit). Kernel pages have AP[1] = 0, making them inaccessible from user space.
18.6 Our Implementation
In our kernel, the MMU is initialized during boot (Chapter 9) after the exception vector tables and GIC. The sequence is:
- Set up identity mapping for the first 2 MB (kernel code and data, UART, GIC)
- Set up the kernel's higher-half mapping at 0xFFFF_0000_0000_0000
- Configure TCR_EL1 and MAIR_EL1
- Enable MMU via SCTLR_EL1.M
- Jump to higher-half virtual address
- Replace TTBR0 with empty table (no user process running)
After this point, all code executes using virtual addresses. The kernel never accesses physical memory directly except through the identity mapping.
18.7 Exercises
Exercise 1: MMU State Dump
Write a function that reads and prints TCR_EL1, MAIR_EL1, TTBR0_EL1, and SCTLR_EL1 registers.
Exercise 2: Disable and Re-enable MMU
Write assembly code that safely disables the MMU (setting SCTLR_EL1.M = 0), performs a physical memory access, then re-enables it.
18.8 Summary
The MMU is the hardware component that translates virtual addresses to physical addresses on every memory access. On ARM64, it is configured through TCR_EL1 (translation control), MAIR_EL1 (memory attributes), and SCTLR_EL1 (master enable). The address space is split between TTBR0 (user, lower half) and TTBR1 (kernel, upper half). Enabling the MMU requires careful sequencing: identity map a small region, configure the registers, enable the MMU, then jump to the higher-half address.