Skip to content

ARM64 Boot Sequence

From reset vector to start_kernel: head.S, page table setup, and MMU enable

Overview

When power is applied to an ARM64 system, the CPU starts executing at the reset vector with the MMU disabled, in the highest exception level supported by the hardware.

Power on / Reset
EL3 (Secure Monitor, if present) → firmware (ATF / TF-A)
     │  Sets up security configuration, drops to EL2
EL2 (Hypervisor, if present) → firmware (QEMU, KVM host)
     │  Sets up virtualization, drops to EL1
EL1 (Kernel) → arch/arm64/kernel/head.S
     └─► primary_entry
          ├─ el2_setup: configure EL2 system registers
          ├─ set_cpu_boot_mode_flag
          ├─ __create_page_tables: build early page tables
          ├─ __cpu_setup: enable caches, initialize SCTLR_EL1
          ├─ __primary_switch:
          │    ├─ adrp x0, init_pg_dir  (set TTBR1_EL1)
          │    ├─ enable_mmu (set SCTLR_EL1.M=1)
          │    └─ b __primary_switched
          └─ __primary_switched:
               ├─ set up kernel stack
               ├─ zero BSS
               └─ b start_kernel  (C code)

head.S: the entry point

/* arch/arm64/kernel/head.S */

    /*
     * Linux/arm64 Image header: first 64 bytes used by bootloader.
     * Bootloader jumps here with:
     *   x0 = physical address of device tree blob (DTB)
     *   x1 = 0 (reserved)
     *   x2 = 0 (reserved)
     *   x3 = 0 (reserved)
     */
    .section ".head.text","ax"

SYM_CODE_START(primary_entry)
    /* Disable all interrupts */
    msr     daifset, #0xf

    /* Preserve x0 (DTB address) for later */
    mov     x21, x0

    /* Are we running in EL2? Set up hypervisor configuration */
    bl      record_mmu_state
    bl      preserve_boot_args

    /*
     * Set up EL1 or EL2 system registers.
     * CPU starts with MMU off; endianness and cache state set via
     * __cpu_setup before MMU enable.
     */
    bl      __create_page_tables
    /* x23 = TTBR1 (kernel VA page tables physical address) */
    /* x25 = VA_BITS */

    bl      __cpu_setup
    adr_l   x8, __primary_switch
    br      x8
SYM_CODE_END(primary_entry)

Exception level setup

/* arch/arm64/kernel/head.S */

/*
 * Set up EL2 registers if booting at EL2 (common with KVM/Xen):
 * - Configure HCR_EL2: virtualization configuration
 * - Set up EL1 trap configuration
 * - Drop to EL1 via eret
 */
SYM_FUNC_START_LOCAL(el2_setup)
    msr     SPsel, #1           /* use SP_EL1 for EL1 */

    /* Check current exception level: */
    mrs     x0, CurrentEL
    cmp     x0, #CurrentEL_EL2
    b.eq    1f

    /* Running at EL1: nothing to do */
    mov     w0, #BOOT_CPU_MODE_EL1
    ret

1:  /* Running at EL2: configure for kernel at EL1 */
    mov_q   x0, HCR_HOST_NVHE_FLAGS  /* no-VHE host configuration */
    msr     HCR_EL2, x0

    /* Allow kernel to access physical timers and PMU: */
    mov     x0, #(CNTHCTL_EL2_EL1PCTEN | CNTHCTL_EL2_EL1PTEN)
    msr     CNTHCTL_EL2, x0

    /* Drop to EL1 via ERET: */
    mov     x0, #(PSR_F_BIT | PSR_I_BIT | PSR_A_BIT | PSR_D_BIT | \
                  PSR_MODE_EL1h)
    msr     SPSR_EL2, x0
    msr     ELR_EL2, lr
    mov     w0, #BOOT_CPU_MODE_EL2
    eret
SYM_FUNC_END(el2_setup)

Early page tables

Before the MMU is enabled, the kernel creates minimal page tables mapping: 1. The kernel itself (identity map for the boot path) 2. The kernel virtual address space (high addresses, TTBR1)

/* arch/arm64/kernel/head.S */
SYM_FUNC_START_LOCAL(__create_page_tables)
    /*
     * ARM64 uses two sets of page tables:
     *   TTBR0_EL1: user space (VA [0, VA_BITS-1])
     *   TTBR1_EL1: kernel space (VA [-(1<<VA_BITS), -1])
     *
     * On boot: create identity map for [_text, _end) in TTBR0
     *          and kernel map in TTBR1
     */

    /* Clear init_pg_dir (kernel page tables) */
    adrp    x0, init_pg_dir
    adrp    x1, init_pg_end
    sub     x1, x1, x0
    bl      __pi_memset             /* zero init_pg_dir (__memzero is ARM32 only) */

    /* Create the initial kernel page tables: */
    /* Map kernel text/data at KIMAGE_VADDR */
    adrp    x0, init_pg_dir
    adrp    x6, _text
    bl      map_kernel_segment      /* maps _text.._end at kernel VA */
    ret
SYM_FUNC_END(__create_page_tables)

CPU setup: enabling the MMU

/* arch/arm64/kernel/head.S */
SYM_FUNC_START(__primary_switch)
    /*
     * Set TTBR0 (identity map, used during MMU enable)
     * and TTBR1 (kernel VA map)
     */
    adrp    x1, idmap_pg_dir    /* TTBR0: identity map for boot path */
    adrp    x2, init_pg_dir     /* TTBR1: kernel VA map */
    bl      __enable_mmu

    /* After MMU enable: we're now running at kernel VAs */
    ldr     x8, =__primary_switched
    adrp    x0, KERNEL_START        /* physical address of _text */
    br      x8
SYM_FUNC_END(__primary_switch)

SYM_FUNC_START_LOCAL(__enable_mmu)
    /*
     * Set up MAIR_EL1: memory attribute indirection register
     * Defines 8 memory types (Normal WB, Device, etc.)
     */
    mov_q   x5, MAIR_EL1_SET
    msr     mair_el1, x5

    /*
     * Set up TCR_EL1: Translation Control Register
     * - IPS: physical address size
     * - T0SZ: VA range for TTBR0 (user, 48-bit)
     * - T1SZ: VA range for TTBR1 (kernel, 48-bit)
     * - TG0/TG1: 4KB granule
     * - SH0/SH1: inner shareable
     * - ORGN/IRGN: outer/inner write-back cacheable
     */
    mrs     x10, ID_AA64MMFR0_EL1
    ...
    msr     tcr_el1, x10

    /* Set TTBR0 and TTBR1: */
    msr     ttbr0_el1, x1
    msr     ttbr1_el1, x2
    isb

    /* Enable MMU: set SCTLR_EL1.M = 1 */
    mrs     x0, sctlr_el1
    orr     x0, x0, #SCTLR_EL1_M     /* MMU enable */
    orr     x0, x0, #SCTLR_EL1_C     /* cache enable */
    orr     x0, x0, #SCTLR_EL1_I     /* instruction cache enable */
    msr     sctlr_el1, x0
    isb     /* ISB required after SCTLR_EL1 write */
    ret
SYM_FUNC_END(__enable_mmu)

After MMU enable: __primary_switched

SYM_FUNC_START_LOCAL(__primary_switched)
    /* Set up the exception vector table (VBAR_EL1) */
    adr_l   x4, vectors             /* arch/arm64/kernel/entry.S */
    msr     vbar_el1, x4
    isb

    /* Set up the initial kernel stack */
    adr_l   x5, init_task           /* struct task_struct for init */
    msr     sp_el0, x5              /* used for current macro */
    adr_l   x6, init_thread_union
    add     sp, x6, #THREAD_SIZE    /* kernel stack top (init_thread_union + THREAD_SIZE) */

    /* Record which exception level we booted at */
    str_l   x5, __boot_cpu_mode, x6

    /* Zero BSS: */
    adrp    x0, __bss_start
    adrp    x1, __bss_stop
    sub     x1, x1, x0
    bl      __pi_memset

    /* Preserve DTB address for setup_arch: */
    str_l   x21, __fdt_pointer, x5

    /* Jump to C code! */
    b       start_kernel
SYM_FUNC_END(__primary_switched)

SMP secondary CPU boot

Secondary CPUs start at secondary_entry after being released by the primary:

/* arch/arm64/kernel/head.S */
SYM_CODE_START(secondary_entry)
    /* Same setup as primary: EL2, CPU features, page tables */
    bl      el2_setup           /* configure EL2 (shared with primary) */
    bl      __cpu_setup

    adr_l   x0, KERNEL_START
    bl      __secondary_switch
    /* → secondary_start_kernel() */
SYM_CODE_END(secondary_entry)

/* arch/arm64/kernel/smp.c */
void secondary_start_kernel(void)
{
    struct mm_struct *mm = &init_mm;
    unsigned int cpu = smp_processor_id();

    mmgrab(mm);
    current->active_mm = mm;

    /* Initialize per-CPU data structures */
    notify_cpu_starting(cpu);

    /* Set up interrupts and timers for this CPU */
    local_irq_enable();
    cpu_startup_entry(CPUHP_AP_ONLINE_IDLE);
    /* → idle loop */
}

Observing the boot

# See kernel boot messages:
dmesg | head -50
# [    0.000000] Booting Linux on physical CPU 0x0000000000 [0x410fd034]
# [    0.000000] Linux version 6.8.0 (gcc 13.2.0)
# [    0.000000] Machine model: Raspberry Pi 4 Model B
# [    0.000000] efi: UEFI not found.
# [    0.000000] OF: fdt: Ignoring memory range 0x0 - 0x80000
# [    0.000000] Zone ranges:
# [    0.000000]   DMA      [mem 0x0000000000000000-0x000000003fffffff]
# [    0.000000]   DMA32    empty
# [    0.000000]   Normal   [mem 0x0000000040000000-0x000000013fffffff]

# Check exception level we booted at:
dmesg | grep "booted at EL"
# Kernel booted at EL2

# KASLR offset (if enabled):
dmesg | grep "kaslr"
# KASLR enabled: offset 0x5500000

# Check the arm64 CPU features detected:
dmesg | grep "CPU features"
cat /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_cur_freq

Further reading

  • ARM64 Exception Model — EL0-EL3, exception entry/exit
  • ARM64 Memory Model — TTBR0/TTBR1, page table format
  • Page Tables — host-side page table internals
  • SMP — secondary CPU initialization
  • arch/arm64/kernel/head.S — complete boot assembly
  • arch/arm64/mm/mmu.c — page table creation
  • ARM Architecture Reference Manual (ARM DDI 0487) — authoritative reference