summaryrefslogtreecommitdiff
path: root/arch/arm64/kernel/start.S
diff options
context:
space:
mode:
authorBradley Morgan <brads@mainlining.org>2026-10-03 20:02:29 +0000
committerBradley Morgan <brads@mainlining.org>2026-10-03 21:37:00 +0000
commit9dbdb15abf8ffb9dbdd972d6bbbcea9a3591e2a3 (patch)
tree0fc03f69c4ca60b3306afc7d3a7c86c1fee8437b /arch/arm64/kernel/start.S
tashaboot: arm64 bootloader
A small arm64 bootloader. No board code, no device tree porting, the architecture manual is the whole story: exception vectors in the fixed 16 slot layout (Table D1-7), ESR_ELx decoded by exception class (D1-2172), EL entry and eret chains per the programmers model (D1-2146), cache maintenance by set/way over the CLIDR_EL1 levels, semihosting for console and file io per DUI 0203, and the A64 boot protocol from Documentation/arch/arm64/booting.rst. The loader boots a stock mainline Image end to end on the qemu virt machine. Boot receipt with 7.3-rc3 (42MB Image): tashaboot 0.1 loaded 43450368 bytes at 40200000, entry 40200000 jumping [ 0.000000] Booting Linux on physical CPU 0x0000000000 [0x411fd070] [ 0.000000] Linux version 7.3.0-rc3 [ 0.000000] Machine model: linux,dummy-virt [ 0.000000] earlycon: pl11 MMIO32:0x0000000009000000 ... ---[ end Kernel panic - not syncing: VFS: Unable to mount root fs ]--- The panic is the expected end state, no root filesystem is handed over yet. The boot chain, state per stage, start to payload: +-----------+-----+--------------+----------------------------------+ | stage | EL | state | work | +-----------+-----+--------------+----------------------------------+ | firmware | any | MMU maybe on | x0 = dtb, jump in | +-----------+-----+--------------+----------------------------------+ | tashaboot | 3-2 | | SCR_EL3.NS = 1, eret to EL2 | +-----------+-----+--------------+----------------------------------+ | | 2 | virt scrub | HCR/CNTHCTL/CPTR/HSTR, CNTFRQ, | | | | | VBAR_EL2, MMU off, tlbi alle2 | +-----------+-----+--------------+----------------------------------+ | | 2 | | load Image over semihosting, | | | | | validate header, place per | | | | | booting.rst | +-----------+-----+--------------+----------------------------------+ | | 2 | caches clean | flush dcache, inval icache, | | | | | args ride x20/x21, regs last | +-----------+-----+--------------+----------------------------------+ | payload | 2 | fresh start | x0 = dtb, x1-x3 = 0, DAIF | | | | | masked, br to image entry | +-----------+-----+--------------+----------------------------------+ Two handoff bugs the kernel caught, both AAPCS clobbers in the final jump. Cache maintenance was called after the register setup, x0-x18 are caller saved, so tb_flush_dcache_all() wiped the dtb pointer and the kernel spun in setup_machine_fdt() with an invalid device tree blob. The flush helpers also clobbered x1 (u-boot's void call convention left mov x1, x0 in cache.S) which handed the kernel a wild x0. The arguments ride in x20/x21 across the cache calls now, callee saved, and the register setup is the last thing before the branch. What is missing on purpose: no SMP bringup (secondary cores park), no PSCI, no initrd or root filesystem handoff, single serial console. Those come next. Signed-off-by: Bradley Morgan <brads@mainlining.org>
Diffstat (limited to 'arch/arm64/kernel/start.S')
-rw-r--r--arch/arm64/kernel/start.S259
1 files changed, 259 insertions, 0 deletions
diff --git a/arch/arm64/kernel/start.S b/arch/arm64/kernel/start.S
new file mode 100644
index 0000000..705721f
--- /dev/null
+++ b/arch/arm64/kernel/start.S
@@ -0,0 +1,259 @@
+/* SPDX-License-Identifier: GPL-2.0+ */
+/*
+ * tashaboot arm64 entry. handles whatever EL the firmware left us in,
+ * EL3, EL2 or EL1, with the MMU either on or off, and arrives at a
+ * clean EL1 with the MMU off before calling C.
+ *
+ * the secondary cores park, spin table bringup is a later problem.
+ *
+ * Copyright (C) 2026 Bradley Morgan <brads@mainlining.org>
+ */
+
+#include <asm/macro.h>
+
+.section .text.boot
+.globl _start
+_start:
+ b reset
+
+ .balign 8
+.globl _text_base
+_text_base:
+ .quad 0x40000000
+
+reset:
+ /* keep the dtb pointer before anything clobbers x0 */
+ mov x19, x0
+
+ /* park secondary cores, they have nothing to do yet */
+ mrs x0, mpidr_el1
+ and x0, x0, #0xff
+ cbnz x0, park
+
+ /* which EL are we in, 0x8 per level shifted into bits 3:2 */
+ mrs x0, CurrentEL
+ lsr x0, x0, #2
+ cmp x0, #3
+ b.eq from_el3
+ cmp x0, #2
+ b.eq from_el2
+ cmp x0, #1
+ b.eq mmu_check
+ b park
+
+from_el3:
+ /*
+ * EL3 holds the security state. the kernel runs non-secure, so
+ * set SCR_EL3.NS before dropping to EL2, which the kernel
+ * prefers (booting.rst, EL2 RECOMMENDED).
+ */
+ mrs x0, scr_el3
+ orr x0, x0, #1 /* SCR_EL3.NS = 1, non-secure */
+ msr scr_el3, x0
+ isb
+
+ mov x0, #0x3c9 /* EL2h, DAIF masked */
+ msr spsr_el3, x0
+ adr x0, from_el2
+ msr elr_el3, x0
+ eret
+
+from_el2:
+ /*
+ * stay at EL2: the kernel wants it for the virtualization
+ * extensions and hands off from there. everything below scrubs
+ * the EL2 state so the kernel starts clean.
+ */
+
+ /* EL1 will be aarch64 when the kernel drops itself down */
+ mov x0, #(1 << 31) /* HCR_EL2.RW = 1 */
+ msr hcr_el2, x0
+
+ /* let EL1 reach the counter, booting.rst demands it */
+ mrs x0, cnthctl_el2
+ orr x0, x0, #(3 << 0) /* EL1PCTEN | EL1PCEN */
+ msr cnthctl_el2, x0
+
+ /* no traps to EL2 behind EL1's back */
+ msr cptr_el2, xzr
+ msr hstr_el2, xzr
+ msr vpidr_el2, xzr
+
+ b mmu_check
+
+mmu_check:
+ /*
+ * whether the firmware left an MMU on: M bit, bit 0, of sctlr at
+ * the current EL. writing the register off would not fault, but
+ * the page tables it built are in its own memory, better to kill
+ * it here than trip over a stale mapping.
+ */
+ mrs x0, CurrentEL
+ lsr x0, x0, #2
+ cmp x0, #2
+ b.lt mmu_el1
+ mrs x0, sctlr_el2
+ tbz x0, #0, c_entry
+
+ mov x0, xzr
+ msr sctlr_el2, x0
+ isb
+ tlbi alle2
+ dsb sy
+ isb
+ b c_entry
+
+mmu_el1:
+ mrs x0, sctlr_el1
+ tbz x0, #0, c_entry
+
+ mov x0, xzr
+ msr sctlr_el1, x0
+ isb
+ ic iallu
+ dsb sy
+ tlbi vmalle1
+ dsb sy
+ isb
+
+c_entry:
+ /*
+ * program the counter frequency, the kernel reads CNTFRQ right
+ * away (booting.rst). qemu virt runs the system counter at
+ * 62.5 MHz. the register is RW only at the highest implemented EL.
+ */
+ mrs x0, CurrentEL
+ lsr x0, x0, #2
+ cmp x0, #2
+ b.lt 1f
+ ldr x0, =62500000
+ msr cntfrq_el0, x0
+ isb
+1:
+ /* our own vectors, so aborts print instead of vanishing */
+ adr x0, vectors
+ mrs x1, CurrentEL
+ lsr x1, x1, #2
+ cmp x1, #2
+ b.lt 2f
+ msr vbar_el2, x0
+ b 3f
+2:
+ msr vbar_el1, x0
+3:
+ isb
+
+ /* stack for the bootloader, grows down from the image end */
+ ldr x0, =__image_end
+ mov sp, x0
+
+ /* clear bss */
+ ldr x0, =__bss_start
+ ldr x1, =__bss_end
+1: cmp x0, x1
+ b.hs 2f
+ str xzr, [x0], #8
+ b 1b
+2:
+
+ /* FP/SIMD access, some kernels assume it is on */
+ mov x0, #(3 << 20)
+ msr cpacr_el1, x0
+ isb
+
+ /* dtb pointer into C arg 0 */
+ mov x0, x19
+ bl tashaboot_main
+
+ /* if main returns there is nothing sensible to do */
+park:
+ wfe
+ b park
+
+/*
+ * exception vectors, the armv8 layout: 16 slots, 128 bytes each, in
+ * the order the manual fixes. taken from EL1h the interesting slots
+ * are 0x200 sync and 0x380 SError, irq and fiq just park, the
+ * bootloader never enables interrupts on purpose.
+ */
+ .balign 2048
+vectors:
+ /* 0x000: current EL, SP_EL0 */
+ .align 7
+ b exc_sync
+ .align 7
+ b exc_park_irq
+ .align 7
+ b exc_park_irq
+ .align 7
+ b exc_serr
+
+ /* 0x200: current EL, SP_ELx */
+ .align 7
+ b exc_sync
+ .align 7
+ b exc_park_irq
+ .align 7
+ b exc_park_irq
+ .align 7
+ b exc_serr
+
+ /* 0x400: lower EL, AArch64 */
+ .align 7
+ b exc_sync
+ .align 7
+ b exc_park_irq
+ .align 7
+ b exc_park_irq
+ .align 7
+ b exc_serr
+
+ /* 0x600: lower EL, AArch32 */
+ .align 7
+ b exc_sync
+ .align 7
+ b exc_park_irq
+ .align 7
+ b exc_park_irq
+ .align 7
+ b exc_serr
+
+exc_sync:
+ stp x29, x30, [sp, #-16]!
+ mov x29, sp
+ mrs x3, CurrentEL
+ lsr x3, x3, #2
+ cmp x3, #2
+ b.lt 1f
+ mrs x0, esr_el2
+ mrs x1, far_el2
+ b 2f
+1:
+ mrs x0, esr_el1
+ mrs x1, far_el1
+2:
+ mov x2, lr
+ bl exc_report
+ ldp x29, x30, [sp], #16
+ b park
+
+exc_serr:
+ stp x29, x30, [sp, #-16]!
+ mov x29, sp
+ mrs x3, CurrentEL
+ lsr x3, x3, #2
+ cmp x3, #2
+ b.lt 1f
+ mrs x0, esr_el2
+ b 2f
+1:
+ mrs x0, esr_el1
+2:
+ mov x1, #0
+ mov x2, lr
+ bl exc_report
+ ldp x29, x30, [sp], #16
+ b park
+
+exc_park_irq:
+ b park