Skip to content

Page tables on Apple Silicon: ASIDs, translation regimes, and APRR in detail

How XNU maps virtual to physical on Apple Silicon — the ARM translation regime, multi-level page tables, ASIDs that eliminate TLB flushes, and where APRR sits in the translation pipeline.

Published 6 min read

The Apple Silicon article covered APRR/SPRR at the "what" level. This is the "how" — the actual ARM translation regime XNU uses, the multi-level page-table walk, the ASID system that eliminates TLB flush penalties on context switch, and where APRR's permission bank fits into the translation pipeline.

Translation regimes — the ARM concept

ARMv8.x defines two translation regimes per exception level (EL):

  • EL0/EL1 with TTBR0 + TTBR1 — user/kernel split. TTBR0 holds the user-space page table base; TTBR1 holds the kernel's.
  • EL2 — hypervisor mode, separate table base (TTBR0_EL2).

XNU uses the EL0/EL1 split. Userspace virtual addresses (low half of the 64-bit space, starting at 0) translate through TTBR0_EL1. Kernel virtual addresses (high half, starting at 0xFFFF_0000_0000_0000 or higher) translate through TTBR1_EL1.

The kernel never changes TTBR1 during normal operation — kernel mappings are global. But TTBR0 does change on every context switch between tasks: the new task's page table base is written into TTBR0_EL1.

apple-oss-distributions/xnuosfmk/arm/pmap.cpmap_switch — installs the new task's TTBR0 + ASID on context switch.View on GitHub(line )

Multi-level translation — the page-table walk

Apple Silicon uses 4 KB pages with 4 levels of translation (some configs use 16 KB pages with 3 levels — the macOS kernel uses 16 KB on modern chips, since the page size is larger and 3 levels is enough to cover 48-bit addressing).

For a 48-bit virtual address with 16 KB pages:

| L1 idx (10 bits) | L2 idx (11 bits) | L3 idx (11 bits) | offset (14 bits) |

The MMU walks:

  1. Read TTBR0/TTBR1 to find the L1 table.
  2. Index the L1 table with the top 10 bits of the VA → get L2 table pointer.
  3. Index L2 with the next 11 bits → L3 table pointer.
  4. Index L3 with the next 11 bits → physical page frame.
  5. Apply the offset to get the final physical address.

Each table entry also encodes permissions (R/W/X), cacheability, shareability, and the permission group identifier (PGID) — the field APRR reinterprets.

Walking the table is expensive (4 memory reads). The TLB caches recent translations to avoid the walk for hot pages.

ASIDs — why context switch is cheap on Apple Silicon

A naive page-table-switch implementation invalidates the entire TLB — every cached translation becomes stale because they were for the old task's address space. On Intel, the equivalent of mov %rax, %cr3 does exactly this, costing thousands of cycles on subsequent accesses as the TLB refills.

ARM (and Apple Silicon) solves this with ASIDs — Address Space Identifiers. Each task is assigned an 8-bit ASID. Every TLB entry is tagged with the ASID it belongs to. The MMU on a translation lookup matches both the VA and the current ASID — entries for other tasks are invisible.

On context switch:

  1. Save the new task's ASID into the lower bits of TTBR0 (along with the page table base).
  2. The current ASID register is updated.
  3. No TLB flush needed — entries for the previous task remain in the TLB but are simply not hit on translations from the new task.

When the original task is dispatched again, its old TLB entries are still there (if they haven't been evicted by LRU), and translation is hot from the first access.

apple-oss-distributions/xnuosfmk/arm/pmap.cASID allocation — XNU recycles ASIDs across tasks; rare overflow forces a TLB flush.View on GitHub(line )

ASIDs are a limited resource (256 on most ARMv8 implementations). When XNU runs out, it bumps the ASID generation number and flushes the TLB once, then starts handing out fresh ASIDs from 0. This is rare in steady state.

Where APRR fits

The page-table entry's PGID field is what APRR/SPRR reinterprets. A standard ARM PTE encodes permissions directly: bits for R, W, X. With APRR, those direct bits are replaced by a permission group identifier that indexes into a permission map register:

PTE permissions field → PGID (e.g. 4)
APRR/SPRR register   → bank A: [PGID 4 = R+W],  bank B: [PGID 4 = R+X]

The thread's APRR_EL1 register selects which bank is active. The MMU consults the active bank's mapping for the PGID, gets the effective permissions, applies them to the access.

Critical implication: the page table itself doesn't change when a thread toggles W↔X for a JIT page. The PTE still has PGID 4. What changes is the register that says "PGID 4 means R+W right now" vs "PGID 4 means R+X right now."

This is why the toggle is so cheap — no page-table edit, no TLB flush, no IPI to other cores. Just a register write.

Per-thread: APRR_EL1 is part of the thread's saved register state and switched on context switch just like ASID is.

Memory-attribute encoding

PTEs also carry memory attributes that control caching, write-combining, and device-vs-normal behavior:

  • Normal cacheable — RAM. Reads/writes go through the data cache.
  • Normal non-cacheable — uncached RAM. Used for some DMA buffers.
  • Device memory — MMIO registers. Strongly ordered, no speculative loads.
  • Write-combining — coalesces writes. Used for GPU command buffers, framebuffers.

The attributes are encoded as an index into MAIR_EL1 (Memory Attribute Indirection Register), which XNU configures at boot.

Mapping a hardware register correctly requires the right attribute; mapping it as Normal cacheable would cause subtle bugs (writes coalesce, reads see stale cached values).

Translation faults and how XNU handles them

When the MMU walks the table and finds no entry, or finds a permission mismatch, it raises a synchronous exception. The XNU exception handler decodes the fault status (FAR_EL1, ESR_EL1) to determine:

  • The faulting virtual address.
  • Whether it was a load, store, or instruction fetch.
  • The fault type: translation fault (no entry), permission fault (wrong R/W/X), alignment fault, etc.
apple-oss-distributions/xnuosfmk/arm64/sleh.cARM64 synchronous exception handler — first responder for every page fault.View on GitHub(line )

From there it routes into vm_fault (see mmap walkthrough) for soft/hard fault handling, or into the COW path for permission faults on writable mappings.

What surprises newcomers

  • The kernel and userspace use different page-table roots (TTBR0 vs TTBR1) — there's no single "current page table." Kernel virtual addresses are always reachable; user virtual addresses depend on which task is current.
  • ASIDs make TLB shootdowns mostly unnecessary on context switch. This is huge — Intel's lack of ASIDs (until the recent PCID feature) made context switch much more expensive historically.
  • APRR exists because edit-the-page-table requires a TLB shootdown across every core. The whole point is to avoid that on the hot path JIT engines hit.
  • MAIR misconfiguration causes mystery bugs. Mapping device memory as Normal cacheable leads to writes coalescing, reads being cached, and confused drivers.

apple-oss-distributions/xnuosfmk/arm/pmap.hThe pmap interface — what every architecture-independent VM call ends up using.View on GitHub(line ) apple-oss-distributions/xnuosfmk/arm/cpu_data.hPer-CPU data — current ASID, TTBR, exception vector pointer, AMX state.View on GitHub(line )

And the virtual memory overview — that's the machine-independent half; this is the machine-dependent counterpart.

Related

What changed in XNU when Apple shipped its own ARM silicon — P/E cores, APRR page-permission switching, the AMX matrix coprocessor, and Rosetta 2.
What runs in the first microseconds of a Mac boot — the SoC's Boot ROM, the Apple-signed LLB and iBoot stages, the SEP coming up alongside, and how the chain of trust starts.
From plug-in to working app — IOUSBHost enumeration, IOKit matching, the DriverKit dext load, the user-space SDK. A complete trace of one device's journey through the stack.