Create your own
Lesson illustration

ARM64 Boot Handoff: From U-Boot Through MMU Activation to start_kernel

Hello again. Last lesson established that a Device Tree is not merely source text: the compiled DTB must accurately describe the board, pass structural and binding checks, and match the tree Linux ultimately receives. This lesson follows that DTB across the next critical boundary. U-Boot has loaded the kernel artifacts into DRAM; now it must enter the ARM64 kernel in a precisely defined CPU state, with the DTB address in the correct register.

By the end, you should be able to explain the ARM64 boot ABI at the U-Boot-to-kernel boundary, distinguish physical from virtual execution in the first assembly instructions, and trace the conceptual path from arch/arm64/kernel/head.S through early page tables and MMU enablement into start_kernel().

For an AM62x gateway, this boundary sits after SoC-specific firmware and U-Boot initialization but before Linux has parsed board topology, mounted storage, or started a console. A failure here can look like a complete silent boot failure because ordinary kernel logging may not yet be available.


The handoff is an ABI contract, not a convenient convention

At this point, U-Boot has normally placed these artifacts in DRAM:

ArtifactRole at handoffKey concern
Uncompressed ARM64 ImageKernel executable imageMust be placed according to its header’s physical-placement requirements
DTBHardware description and boot parametersMust remain intact and be passed in x0
Optional initramfsTemporary early root filesystemMust not overlap kernel or DTB; it is referenced through /chosen
Boot argumentsKernel command lineNormally inserted by U-Boot into the DTB’s /chosen/bootargs property

The AArch64 Linux boot protocol requires U-Boot to call the first instruction of the uncompressed kernel image. Unlike older ARM32 Linux arrangements, ARM64 does not contain a general decompressor for a compressed Image.gz; if a compressed image is used, the bootloader performs decompression before the final jump.

The primary CPU register contract is deliberately small:

RegisterRequired value at kernel entryMeaning
x0Physical address of the DTB in system RAMThe one boot argument Linux needs
x10Reserved
x20Reserved
x30Reserved

The most important detail is physical address. The MMU is off at the entry boundary, so the processor is executing with physical addressing. A high kernel virtual address is not meaningful yet.

A reliable mental model is:

  1. U-Boot owns the board and DRAM layout until the branch to the kernel.
  2. Linux owns the CPU execution environment once its image entry point begins.
  3. The DTB is the shared data structure that tells Linux what hardware it inherited and how U-Boot prepared the boot.

The architecture also requires more than four register values. Before entering the kernel, U-Boot or earlier firmware must ensure that:

  • execution is in the non-secure state, at EL2 or EL1;
  • PSTATE.DAIF masks debug, SError, IRQ, and FIQ exceptions;
  • the MMU is off;
  • the loaded kernel image is not obscured by stale instruction-cache contents;
  • DMA-capable devices are quiesced, preventing a device from modifying memory while Linux establishes its own drivers;
  • architected timer and coherency state are valid for all CPUs that Linux will use.

The “quiesce DMA” requirement deserves emphasis in gateway design. For example, Ethernet receive DMA or a peripheral DMA engine left active by a bootloader can overwrite memory Linux considers free. Such failures often appear non-deterministic: the same image may boot repeatedly until a packet arrives at exactly the wrong time.

Booting AArch64 Linux — The Linux Kernel documentation

Read the Linux kernel’s official ARM64 boot-protocol documentation. It is the authoritative specification for what U-Boot must provide, rather than a description of one board or one U-Boot version.

In Section 4, “Call the kernel image,” first read the placement rules for Image and any initramfs. Then read the primary handoff contract, focusing on the distinction between the DTB physical address in x0, masked interrupts, and the MMU-off requirement. Finally, in the concluding CPU-startup material after the system-register requirements, read the primary and secondary CPU rules. Notice that only the primary CPU receives the DTB address; secondaries enter with x0 through x3 cleared.


Exception levels: what Linux inherits

ARM64 privilege is organized into exception levels:

Exception levelTypical responsibility
EL3Secure monitor and secure firmware services
EL2Hypervisor-level services and virtualization support
EL1Linux kernel
EL0Linux userspace applications

An AM62x-class production system may have multiple firmware components before U-Boot, including secure and system-control firmware. Their exact loading and authentication sequence is SoC-specific. The Linux handoff itself, however, follows the standard ARM64 contract: Linux arrives in the non-secure world, typically at EL2 or sometimes EL1.

Entering at EL2 is recommended by the architecture specification because it leaves virtualization extensions available. The kernel’s early assembly examines the current exception level and configures the execution environment appropriately. On systems using Virtualization Host Extensions, the kernel can make a different EL2-oriented choice than it does on a conventional non-VHE system. Therefore, avoid reducing the rule to “the kernel always drops from EL2 to EL1.” The stable engineering statement is:

Linux detects and normalizes the privileged execution context it inherited before enabling its final memory-management configuration.

The Linux Foundation presentation gives useful context for placing this kernel boundary within a full ARM platform boot chain.

How ARM Systems are Booted: An Introduction to the ARM Boot Flow - Rouven Czerwinski

Watch “How ARM Systems are Booted: An Introduction to the ARM Boot Flow” by Rouven Czerwinski on The Linux Foundation channel. It connects ARM exception levels to the firmware, bootloader, kernel, and userspace responsibilities that occur around the handoff.

Watch exception levels to establish the roles of EL3, EL2, EL1, and EL0 across a typical ARM64 boot. Then watch kernel handoff. Focus on the bootloader’s final work: decompression, DTB placement, interrupt masking, cache hygiene, MMU shutdown, and the single meaningful ARM64 register argument, x0.


A concrete U-Boot view: the final moment before Linux

At the U-Boot shell, a command such as the following expresses the final logical handoff:

booti ${kernel_addr_r} - ${fdt_addr_r}

Here, booti is for an ARM64 Linux Image; the middle - says no separate initramfs argument is supplied in this invocation, and the last argument identifies the DTB. In a real boot script, addresses may come from environment variables, a FIT image, extlinux configuration, or a board-specific boot flow.

Do not equate the text of this command with the full physical handoff. U-Boot may resize the DTB to create room for fixups, update /chosen, relocate artifacts to avoid overlap, and then perform architecture-specific cleanup before branching. The final DTB location handed to the kernel is the physical address actually loaded into x0, not necessarily the initial address you typed.

During a boot investigation, these U-Boot commands help establish the evidence:

bdinfo
printenv kernel_addr_r fdt_addr_r ramdisk_addr_r
fdt addr ${fdt_addr_r}
fdt print /chosen

Use them to answer four concrete questions:

  1. Are the planned addresses in DRAM?
    bdinfo reports DRAM banks and often the U-Boot relocation location.

  2. Do artifacts overlap?
    The kernel image, DTB, initramfs, U-Boot’s own relocated image, malloc area, and stack must have separate memory regions.

  3. Is the intended DTB selected?
    fdt print /chosen can reveal the effective bootargs, initrd properties, and U-Boot-injected metadata.

  4. Did U-Boot announce a final DTB relocation?
    Serial output such as “Loading Device Tree to ...” is useful evidence. Capture it alongside the boot command and artifact hashes.

This is also why the previous lesson’s live-tree inspection matters. A correct source DTS can be compiled into a correct DTB, while U-Boot may still boot a different file, modify the selected tree, or pass a corrupted blob.


head.S: the kernel begins without its normal environment

The entry assembly for the ARM64 kernel lives in:

arch/arm64/kernel/head.S

The first instructions of the kernel image ultimately reach an early primary-CPU entry path. Symbol names and fine-grained ordering change across kernel releases, so treat labels such as primary_entry, __primary_switch, and __primary_switched as source-navigation landmarks, not as a stable external interface. The architectural jobs are much more stable.

At first entry, Linux does not yet have:

  • its normal kernel virtual address mapping;
  • a C stack;
  • a cleared .bss section;
  • a configured exception-vector base;
  • a parsed Device Tree;
  • the general allocator, scheduler, interrupt subsystem, or driver model.

It is therefore unsafe to think of head.S as “just some startup code before C.” It is a carefully constrained bridge from a bootloader-owned physical execution environment to the kernel’s virtual-memory environment.

A representative trace looks like this:

Early stageEssential responsibilityWhy it must precede the next stage
Preserve boot argumentsSave the DTB physical address received in x0General-purpose registers will be reused during setup
Record inherited stateDetermine exception level and record MMU-related stateEarly code must not make unverified assumptions about firmware setup
Establish CPU controlsConfigure execution state needed for LinuxTranslation, cache, and exception behavior must be deliberate
Build early page tablesCreate identity and kernel virtual mappingsThe processor needs valid translations before the MMU can be enabled
Enable MMU and cachesInstall translation controls and activate virtual addressingKernel code can now execute at its linked virtual addresses
Establish initial kernel contextSet exception vectors, stack, global state, and cleared .bssC code requires a valid runtime substrate
Branch to start_kernel()Begin architecture-independent kernel initializationThe kernel can parse the DTB and initialize core subsystems

The DTB pointer is preserved very early precisely because x0 is scratch space during assembly setup. You should never expect a later routine to find the FDT merely because x0 happened to contain it at image entry. Early assembly stores it in kernel-owned state, from which architecture setup code can later recover it.


Why two early mappings are needed

The MMU maps virtual addresses to physical addresses. Before it is enabled, an instruction fetch uses the physical address where U-Boot loaded the image. Once it is enabled, Linux intends to run in its kernel virtual address space.

This creates a transition problem: if Linux enables the MMU but has mapped only its final high virtual addresses, the currently executing instruction stream may immediately become unreachable. The processor is still fetching from the old physical program-counter location.

Early ARM64 startup solves this with two complementary mappings:

MappingTypical translation basePurpose during the transition
Identity mapTTBR0_EL1Makes a physical address temporarily valid as the same virtual address, allowing execution to continue safely during the switch
Kernel mapTTBR1_EL1Maps the kernel at its intended high virtual address range

The names reflect their roles, not a permanent user/kernel split at this exact instant. Later, Linux develops complete address spaces for user processes and the full kernel mapping. At this early moment, the identity map is a narrow bridge: enough translation to remain alive during the changeover, but not a complete operating-system memory map.

Before enabling translation, head.S also programs key system registers:

RegisterResponsibility
MAIR_EL1Defines memory attribute encodings, distinguishing normal cacheable memory from device memory
TCR_EL1Defines translation granule, virtual-address size, physical-address range, and translation behavior
TTBR0_EL1Points to the identity-map page tables
TTBR1_EL1Points to the initial kernel page tables
SCTLR_EL1Contains the enable controls for the EL1 MMU and caches

A useful distinction is that page tables are data structures in physical RAM, while TTBR0_EL1 and TTBR1_EL1 tell the CPU where to find them. The CPU must be given valid physical addresses for those tables before translation begins.

The critical architectural act is setting the MMU-enable control in SCTLR_EL1, followed by an instruction synchronization barrier. The barrier ensures subsequent instruction execution observes the new translation and control state. This is not optional defensive programming: changing address-translation state without the required ordering rules creates behavior that is not architecturally reliable.


From the MMU switch to C code

After enabling the MMU, the early assembly transfers to code accessible through the kernel virtual mapping. At this point the kernel can establish the final elements C code expects:

  • install the exception-vector address in VBAR_EL1;
  • establish the boot CPU’s initial kernel stack;
  • preserve the original FDT physical address in a kernel variable;
  • clear the .bss region so uninitialized global variables begin as zero;
  • retain a record of the inherited boot mode and relevant CPU state.

Only then does assembly branch to:

start_kernel();

This function is defined in:

init/main.c

start_kernel() is the beginning of general kernel initialization, not the beginning of ordinary process execution. It runs with the MMU on and an initial kernel stack available, but with interrupts still tightly controlled. One of its early architecture-dependent responsibilities is to invoke setup code that consumes the saved DTB pointer.

Conceptually, this is where the DTB becomes live kernel knowledge:

  1. Linux validates and reserves relevant physical memory.
  2. The flattened DTB is parsed into internal structures.
  3. Memory ranges, reserved-memory regions, CPU topology, interrupt controllers, clocks, consoles, and peripherals become inputs to architecture and driver initialization.

That link matters for debugging. If U-Boot passed the wrong address in x0, the kernel may fail before normal logging, report an FDT problem, use unexpected memory information, or appear to hang shortly after “Starting kernel.” Conversely, a correct x0 value does not rescue a malformed DTB: the handoff and the DTB content are separate parts of the same boot contract.

Failure localization around this boundary

Observed evidenceLikely investigation direction
U-Boot loads artifacts but no kernel banner or early output appearsKernel address, final branch, MMU-off state, stale cache state, execution level, image corruption
Linux reports an invalid FDT, cannot find memory, or uses unexpected boot argumentsDTB selection, final DTB address in x0, DTB relocation, /chosen fixups, artifact overlap
Sporadic failure that changes with Ethernet or storage trafficDMA was not quiesced, cache coherency issue, or a buffer overlaps a boot artifact
Primary CPU begins booting but secondary CPUs fail later/cpus DT content, PSCI or spin-table enable method, firmware CPU-on service

Secondary cores are deliberately not part of the primary handoff argument convention. The primary CPU enters Linux with the DTB address in x0; secondary CPUs enter with x0 through x3 set to zero. Linux later brings them online using the method described by the Device Tree, commonly PSCI on modern ARM64 SoCs. This separation prevents each CPU from independently interpreting or modifying the boot DTB.


A source-reading workflow for real kernel trees

When you have a vendor kernel tree for your AM62x board, use search rather than assuming all labels exactly match a tutorial or another kernel release:

cd <kernel-source>
grep -RIn --include='*.S' 'start_kernel' arch/arm64
grep -RIn --include='*.S' 'create.*page.table' arch/arm64/kernel
grep -RIn --include='*.S' 'enable.*mmu' arch/arm64/kernel
grep -RIn --include='*.c' 'void __init start_kernel' init

Then read the matching code with enough surrounding context to answer these questions:

  • Where is the incoming boot-argument pointer preserved?
  • How does this kernel version identify the boot exception level?
  • Which structures serve as the identity map and early kernel mapping?
  • Where are translation control registers programmed?
  • What instruction sequence activates the MMU?
  • When are the vector base, initial stack, and .bss established?
  • Where does execution first branch into start_kernel()?

This practice is especially valuable in supplier integration work. Vendor boot logs and patches often describe symptoms using labels from a particular kernel revision. Confirm the behavior against the exact source revision and build configuration used for the image under investigation.

For your private project evidence, retain a short boot-boundary record containing the U-Boot version, kernel commit, exact booti or boot-script command, kernel/DTB hashes, DRAM load ranges, the /chosen contents, and the serial log surrounding the final U-Boot message and first Linux output. It is compact evidence for later bring-up reviews and does not require publishing sensitive board configuration.


Key takeaways

The ARM64 U-Boot-to-kernel transition is a strict ABI and machine-state handoff.

  • The primary CPU enters the kernel with the physical DTB address in x0 and zeros in x1 through x3.
  • U-Boot must present a non-secure EL2 or EL1 context with interrupts masked, the MMU off, coherent and clean kernel memory, and DMA-capable devices quiesced.
  • head.S preserves the boot arguments, normalizes inherited CPU state, builds minimal page tables, and configures translation registers.
  • An identity mapping allows the CPU to survive the MMU transition while a kernel mapping provides Linux’s intended virtual address space.
  • After enabling the MMU, early assembly establishes exception vectors, stack, .bss, and saved boot state before entering C at start_kernel().
  • start_kernel() begins core kernel initialization; it is where the saved DTB starts to become Linux’s operational model of memory, CPUs, and devices.

Next, you will trace the memory side of that transition in more detail: from the DTB’s memory and reserved-memory descriptions through memblock, into the buddy allocator and SLUB initialization.

Can't find a good explanation? Sign up and we'll make it for you

Sign up