From 9dbdb15abf8ffb9dbdd972d6bbbcea9a3591e2a3 Mon Sep 17 00:00:00 2001 From: Bradley Morgan Date: Sat, 3 Oct 2026 20:02:29 +0000 Subject: 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 --- arch/arm64/kernel/start.S | 259 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 259 insertions(+) create mode 100644 arch/arm64/kernel/start.S (limited to 'arch/arm64/kernel/start.S') 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 + */ + +#include + +.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 -- cgit v1.2.3