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:
| Artifact | Role at handoff | Key concern |
|---|---|---|
Uncompressed ARM64 Image | Kernel executable image | Must be placed according to its header’s physical-placement requirements |
| DTB | Hardware description and boot parameters | Must remain intact and be passed in x0 |
| Optional initramfs | Temporary early root filesystem | Must not overlap kernel or DTB; it is referenced through /chosen |
| Boot arguments | Kernel command line | Normally 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:
| Register | Required value at kernel entry | Meaning |
|---|---|---|
x0 | Physical address of the DTB in system RAM | The one boot argument Linux needs |
x1 | 0 | Reserved |
x2 | 0 | Reserved |
x3 | 0 | Reserved |
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:
- U-Boot owns the board and DRAM layout until the branch to the kernel.
- Linux owns the CPU execution environment once its image entry point begins.
- 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.DAIFmasks 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 level | Typical responsibility |
|---|---|
| EL3 | Secure monitor and secure firmware services |
| EL2 | Hypervisor-level services and virtualization support |
| EL1 | Linux kernel |
| EL0 | Linux 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:
-
Are the planned addresses in DRAM?
bdinforeports DRAM banks and often the U-Boot relocation location. -
Do artifacts overlap?
The kernel image, DTB, initramfs, U-Boot’s own relocated image, malloc area, and stack must have separate memory regions. -
Is the intended DTB selected?
fdt print /chosencan reveal the effectivebootargs, initrd properties, and U-Boot-injected metadata. -
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
.bsssection; - 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 stage | Essential responsibility | Why it must precede the next stage |
|---|---|---|
| Preserve boot arguments | Save the DTB physical address received in x0 | General-purpose registers will be reused during setup |
| Record inherited state | Determine exception level and record MMU-related state | Early code must not make unverified assumptions about firmware setup |
| Establish CPU controls | Configure execution state needed for Linux | Translation, cache, and exception behavior must be deliberate |
| Build early page tables | Create identity and kernel virtual mappings | The processor needs valid translations before the MMU can be enabled |
| Enable MMU and caches | Install translation controls and activate virtual addressing | Kernel code can now execute at its linked virtual addresses |
| Establish initial kernel context | Set exception vectors, stack, global state, and cleared .bss | C code requires a valid runtime substrate |
Branch to start_kernel() | Begin architecture-independent kernel initialization | The 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:
| Mapping | Typical translation base | Purpose during the transition |
|---|---|---|
| Identity map | TTBR0_EL1 | Makes a physical address temporarily valid as the same virtual address, allowing execution to continue safely during the switch |
| Kernel map | TTBR1_EL1 | Maps 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:
| Register | Responsibility |
|---|---|
MAIR_EL1 | Defines memory attribute encodings, distinguishing normal cacheable memory from device memory |
TCR_EL1 | Defines translation granule, virtual-address size, physical-address range, and translation behavior |
TTBR0_EL1 | Points to the identity-map page tables |
TTBR1_EL1 | Points to the initial kernel page tables |
SCTLR_EL1 | Contains 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
.bssregion 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:
- Linux validates and reserves relevant physical memory.
- The flattened DTB is parsed into internal structures.
- 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 evidence | Likely investigation direction |
|---|---|
| U-Boot loads artifacts but no kernel banner or early output appears | Kernel 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 arguments | DTB selection, final DTB address in x0, DTB relocation, /chosen fixups, artifact overlap |
| Sporadic failure that changes with Ethernet or storage traffic | DMA 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
.bssestablished? - 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
x0and zeros inx1throughx3. - 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.Spreserves 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 atstart_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