Every CPU architecture needs a coherent story for how it addresses main memory — how virtual addresses generated by running programs get translated into physical RAM locations, how much memory can theoretically be addressed, and how the memory address space gets divided between the kernel, applications, and hardware devices. ARM’s story has evolved dramatically as the architecture moved from 32-bit to 64-bit, and understanding it is essential for anyone working on embedded systems, mobile OS internals, or low-level ARM software.
The Basics: Virtual vs. Physical Addressing on ARM
Like most modern architectures, ARM CPUs (from ARMv7 onward, and especially in ARMv8/AArch64) implement virtual memory through a Memory Management Unit (MMU). Software — including the OS kernel and applications — operates almost entirely in terms of virtual addresses. The MMU, guided by page tables set up by the OS, translates these virtual addresses into physical addresses that actually reach the memory controller and RAM chips.
CPU generates Virtual Address (VA)
|
v
MMU + Page Tables (Translation Lookaside Buffer caches recent translations)
|
v
Physical Address (PA) --> Memory Controller --> DRAM
Address Space Size: 32-bit vs. 64-bit ARM
AArch32 (32-bit ARM)
Classic 32-bit ARM (ARMv7-A and earlier) provides a 32-bit virtual address space, meaning a theoretical maximum of 4 GB of addressable memory per process. In practice, this space is typically split between user space and kernel space — a common Linux ARM convention reserves the top 1 GB (0xC0000000–0xFFFFFFFF) for the kernel and the bottom 3 GB for user space, though this split is configurable.
To work around the 4 GB physical RAM ceiling on systems that actually had more RAM installed (common in embedded and early smartphone designs), ARMv7 introduced LPAE (Large Physical Address Extension), which allows the MMU to map 32-bit virtual addresses to physical addresses up to 40 bits wide (1 TB), even though any single process is still limited to a 4 GB virtual view.
AArch64 (64-bit ARM, ARMv8-A and later)
ARMv8-A introduced the 64-bit execution state, AArch64, which dramatically expands addressable space. While the address registers are 64 bits wide, current implementations use a subset of those bits for actual addressing — typically 48 bits of virtual address space (256 TB) by default, with newer implementations supporting extensions up to 52-bit virtual addressing (4 PB) via the Large Virtual Address (LVA) feature, and matching physical address extensions for up to 52-bit physical addressing.
This massive expansion eliminates the practical addressing ceiling that 32-bit systems ran into, and is one of the primary motivations (alongside a larger general-purpose register file and improved instruction encoding) for the industry-wide shift to 64-bit ARM in phones (since the iPhone 5s in 2013, and Android flagship devices shortly after) and servers.
The ARM Translation Table System
ARM’s page table structure (formally the “translation table”) is a multi-level, hierarchical structure, conceptually similar to x86’s but with ARM-specific terminology and granularities.
Translation Granules
ARM supports multiple page granule sizes — the base unit of memory mapping:
- 4 KB pages — the most common granule, matching typical Linux/Android/iOS configurations.
- 16 KB pages — used by some systems (notably Apple’s iOS/macOS on Apple Silicon uses 16 KB pages) for reduced page table overhead on systems with abundant RAM.
- 64 KB pages — useful for large-memory server workloads to reduce TLB pressure, at the cost of increased internal fragmentation.
Translation Table Levels
AArch64 translation typically uses up to four levels of translation tables (Level 0 through Level 3), each level’s table selecting a portion of the physical address for the next lookup, terminating either at a full page-sized entry or, for very large regions, at a “block” entry (ARM’s equivalent of x86’s “huge pages”) that maps a large contiguous chunk (e.g., 2 MB or 1 GB) with a single table entry — reducing TLB pressure for OS kernels and large-memory applications.
VA (Virtual Address, 48-bit example)
[ Level 0 index ] [ Level 1 index ] [ Level 2 index ] [ Level 3 index ] [ Page offset ]
| | | | |
v v v v v
Table Base Reg -> L1 Table Entry -> L2 Table Entry -> L3 Table Entry -> Physical Page + offset
Two Translation Regimes: TTBR0 and TTBR1
A distinctive ARM feature is the split between two independently-managed translation table base registers:
- TTBR0_EL1 — governs the lower half of the virtual address space, conventionally used for user-space (application) addresses.
- TTBR1_EL1 — governs the upper half, conventionally used for kernel-space addresses.
This split lets the OS switch only the user-space page tables (TTBR0) on a context switch between processes, while the kernel’s mappings (TTBR1) remain constant and shared — a meaningful performance and security-relevant design, since it cleanly separates user and kernel address ranges without requiring a single unified page table walk to distinguish them, and supports mitigations like kernel page-table isolation.
Memory-Mapped I/O and the ARM Address Map
Unlike x86, which historically had separate I/O port address space (IN/OUT instructions) alongside memory addressing, ARM uses memory-mapped I/O exclusively — device registers (UARTs, GPIO controllers, interrupt controllers, timers) are simply mapped into specific regions of the physical address space and accessed with ordinary load/store instructions. The OS and device drivers must know (via Device Tree entries on embedded/mobile Linux, or ACPI tables on ARM servers) where in the physical address map each peripheral lives.
This is one reason ARM system memory maps are so SoC-specific: unlike the relatively standardized PCI/PCIe device enumeration on x86, each ARM SoC vendor defines its own physical address layout for peripherals, requiring the boot firmware and OS to be configured (via Device Tree or ACPI) with the specific memory map of that chip.
Address Translation and the TLB
Because walking multi-level page tables on every memory access would be prohibitively slow, ARM CPUs — like all modern architectures — cache recent virtual-to-physical translations in a Translation Lookaside Buffer (TLB). ARM’s TLB architecture is often split into separate instruction and data TLBs (I-TLB, D-TLB) at the L1 level, with a larger unified L2 TLB, and supports Address Space Identifiers (ASIDs) to tag TLB entries by process, avoiding a full TLB flush on every context switch (a meaningful performance benefit versus older approaches that required flushing the entire TLB whenever switching between processes).
Memory Attributes and Access Permissions
ARM’s translation table entries don’t just map addresses — they also encode rich memory attributes controlling caching behavior and access permissions:
- Memory type — Normal (cacheable RAM), Device (uncached, ordered, used for MMIO regions), or Strongly-Ordered (older terminology, largely superseded by refined Device memory types in later ARM revisions).
- Access permissions — read/write/execute permissions, separately controllable for privileged (kernel, EL1) vs. unprivileged (user, EL0) access, plus the ability to mark regions execute-never (XN bit), a critical security feature preventing code execution from data regions (mitigating many classic exploitation techniques).
- Shareability — whether a memory region is coherent across multiple CPU cores (Inner Shareable, Outer Shareable, Non-shareable), important for correct multi-core cache coherency.
Real-World Examples
Linux on ARM64
The Linux kernel’s arm64 architecture port configures the kernel’s virtual address layout using TTBR1, typically reserving the highest portion of the 48-bit (or 52-bit) address space for kernel mappings, with user space occupying the lower half via TTBR0. Tools like /proc/self/maps on an ARM64 Android or Linux device let you directly observe this layout for a running process.
Android
Android (built on the Linux kernel) inherits this ARM64 addressing model directly, and additionally leverages ARM’s Memory Tagging Extension (MTE), introduced in ARMv8.5-A, which tags 16-byte memory granules with metadata to help detect memory-safety bugs (use-after-free, buffer overflows) at the hardware level — a security feature that specifically depends on ARM’s address translation infrastructure to implement efficiently.
iOS / Apple Silicon
Apple’s iOS and macOS on Apple Silicon use a 16 KB translation granule (rather than the more common 4 KB), which reduces the number of page table entries needed for a given memory footprint, trading some internal fragmentation for reduced page-table-walk overhead — a deliberate performance tuning choice enabled by ARM’s flexible granule-size support.
Comparison: ARM vs. x86 Memory Addressing
| Aspect | ARM (AArch64) | x86-64 |
|---|---|---|
| Virtual address width (typical) | 48-bit (up to 52-bit with LVA) | 48-bit (57-bit with 5-level paging/LA57) |
| I/O addressing | Memory-mapped only | Both memory-mapped and separate I/O ports |
| Page table base registers | Split TTBR0 (user) / TTBR1 (kernel) | Single CR3 register |
| Common page granules | 4 KB, 16 KB, 64 KB | 4 KB (with 2 MB/1 GB huge pages) |
| TLB tagging | ASID-based | PCID-based (Process Context Identifiers) |
Troubleshooting Address-Related Issues on ARM
- Kernel panic referencing “translation fault” at boot: Usually a misconfigured Device Tree memory node or bootloader passing incorrect memory bank information to the kernel.
- Application crash with SIGSEGV at a suspiciously round address: Often a null pointer dereference or an execute-never (XN) violation from attempting to execute non-executable memory (e.g., a JIT bug writing code to a data page without marking it executable).
- Unexpectedly high memory overhead on 16 KB-page ARM systems: Small allocations pad up to the 16 KB granule; consider allocator tuning if targeting Apple Silicon specifically.
- Peripheral driver failing to find its device registers: Check the Device Tree (or ACPI table on ARM servers) for correct physical base addresses matching the actual SoC memory map.
Best Practices
- Use
madvise/huge-page hints appropriately to reduce TLB pressure for large, performance-critical memory regions on ARM64 systems. - Respect the execute-never (XN) bit discipline — never mark writable memory executable unless absolutely necessary (and even then, use write-xor-execute patterns).
- When developing Device Tree entries for new ARM hardware, double check physical address ranges against the SoC’s technical reference manual to avoid MMIO mapping errors.
- Be aware of page granule differences (4 KB vs. 16 KB) when tuning memory-sensitive software across ARM platforms (e.g., Linux/Android vs. iOS/macOS).
- Leverage ASID-based TLB tagging (already handled by the OS on modern ARM Linux/Android/iOS) by avoiding unnecessary full TLB flushes in custom kernel or hypervisor code.
Summary
ARM’s approach to main memory addressing has evolved from a constrained 32-bit, 4 GB virtual address space (extended via LPAE for larger physical memory) to a spacious 64-bit AArch64 model supporting up to 52-bit virtual and physical addressing. Its multi-level translation table system, split TTBR0/TTBR1 user/kernel addressing, flexible page granules, and rich memory-type/permission attributes give operating system designers considerable power and flexibility — while also introducing SoC-specific complexity around memory-mapped I/O that differs meaningfully from the more standardized x86 model. Understanding this system is foundational to working with ARM-based Linux, Android, and iOS internals.
FAQs
Q: How much memory can a 64-bit ARM system address? Typically up to 256 TB of virtual address space with 48-bit addressing, extendable to 4 PB with 52-bit Large Virtual Addressing support in newer ARM implementations.
Q: What replaced ARM’s 32-bit 4 GB memory limit? LPAE (Large Physical Address Extension) allowed 32-bit ARM systems to address up to 1 TB of physical memory even though each process still saw a 4 GB virtual space; the full solution came with the 64-bit AArch64 architecture.
Q: What is the difference between TTBR0 and TTBR1? TTBR0 governs translation tables for the lower half of the address space (typically user-space), while TTBR1 governs the upper half (typically kernel-space), allowing the OS to switch only user mappings on a context switch.
Q: Does ARM support huge pages like x86? Yes — ARM’s translation table “block” entries serve the same purpose as x86 huge pages, mapping large contiguous regions (e.g., 2 MB or 1 GB) with a single table entry to reduce TLB pressure.
Q: Why does Apple use 16 KB pages on Apple Silicon instead of 4 KB? It reduces the number of page table entries and translation overhead for typical modern memory footprints, trading a small amount of internal fragmentation for reduced page-table-walk cost.
References
- ARM Architecture Reference Manual for A-profile architecture (ARMv8-A/ARMv9-A)
- ARM Developer: “Understanding the ARMv8-A Translation System” — developer.arm.com
- Linux Kernel Documentation: arm64 memory layout (
Documentation/arch/arm64/memory.rst) - ARM Developer Documentation: Memory Tagging Extension (MTE)