diff options
| author | Bradley Morgan <brads@mainlining.org> | 2026-10-03 20:02:29 +0000 |
|---|---|---|
| committer | Bradley Morgan <brads@mainlining.org> | 2026-10-03 21:37:00 +0000 |
| commit | 9dbdb15abf8ffb9dbdd972d6bbbcea9a3591e2a3 (patch) | |
| tree | 0fc03f69c4ca60b3306afc7d3a7c86c1fee8437b /arch | |
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')
| -rw-r--r-- | arch/arm64/include/asm/linkage.h | 14 | ||||
| -rw-r--r-- | arch/arm64/include/asm/macro.h | 347 | ||||
| -rw-r--r-- | arch/arm64/kernel/boot.S | 43 | ||||
| -rw-r--r-- | arch/arm64/kernel/exceptions.c | 67 | ||||
| -rw-r--r-- | arch/arm64/kernel/halt.c | 18 | ||||
| -rw-r--r-- | arch/arm64/kernel/start.S | 259 | ||||
| -rw-r--r-- | arch/arm64/kernel/tashaboot.lds | 67 | ||||
| -rw-r--r-- | arch/arm64/lib/cache.S | 100 | ||||
| -rw-r--r-- | arch/arm64/lib/semihosting.S | 18 |
9 files changed, 933 insertions, 0 deletions
diff --git a/arch/arm64/include/asm/linkage.h b/arch/arm64/include/asm/linkage.h new file mode 100644 index 0000000..b5b9706 --- /dev/null +++ b/arch/arm64/include/asm/linkage.h @@ -0,0 +1,14 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef __ASM_LINKAGE_H +#define __ASM_LINKAGE_H + +#define ALIGN .p2align 4 +#define ENTRY(name) \ + .globl name; \ + ALIGN; \ + name: +#define ENDPROC(name) \ + .type name, %function; \ + .size name, .-name + +#endif diff --git a/arch/arm64/include/asm/macro.h b/arch/arm64/include/asm/macro.h new file mode 100644 index 0000000..1a1edc9 --- /dev/null +++ b/arch/arm64/include/asm/macro.h @@ -0,0 +1,347 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * include/asm-arm/macro.h + * + * Copyright (C) 2009 Jean-Christophe PLAGNIOL-VILLARD <plagnioj@jcrosoft.com> + */ + +#ifndef __ASM_ARM_MACRO_H__ +#define __ASM_ARM_MACRO_H__ + +#ifdef CONFIG_ARM64 +#include <asm/system.h> +#endif + +#ifdef __ASSEMBLY__ + +/* + * These macros provide a convenient way to write 8, 16 and 32 bit data + * to any address. + * Registers r4 and r5 are used, any data in these registers are + * overwritten by the macros. + * The macros are valid for any ARM architecture, they do not implement + * any memory barriers so caution is recommended when using these when the + * caches are enabled or on a multi-core system. + */ + +.macro write32, addr, data + ldr r4, =\addr + ldr r5, =\data + str r5, [r4] +.endm + +.macro write16, addr, data + ldr r4, =\addr + ldrh r5, =\data + strh r5, [r4] +.endm + +.macro write8, addr, data + ldr r4, =\addr + ldrb r5, =\data + strb r5, [r4] +.endm + +/* + * This macro generates a loop that can be used for delays in the code. + * Register r4 is used, any data in this register is overwritten by the + * macro. + * The macro is valid for any ARM architeture. The actual time spent in the + * loop will vary from CPU to CPU though. + */ + +.macro wait_timer, time + ldr r4, =\time +1: + nop + subs r4, r4, #1 + bcs 1b +.endm + +#ifdef CONFIG_ARM64 +/* + * Register aliases. + */ +lr .req x30 + +/* + * Branch according to exception level + */ +.macro switch_el, xreg, el3_label, el2_label, el1_label + mrs \xreg, CurrentEL + cmp \xreg, #0x8 + b.gt \el3_label + b.eq \el2_label + b.lt \el1_label +.endm + +/* + * Branch if we are not in the highest exception level + */ +.macro branch_if_not_highest_el, xreg, label + switch_el \xreg, 3f, 2f, 1f + +2: mrs \xreg, ID_AA64PFR0_EL1 + and \xreg, \xreg, #(ID_AA64PFR0_EL1_EL3) + cbnz \xreg, \label + b 3f + +1: mrs \xreg, ID_AA64PFR0_EL1 + and \xreg, \xreg, #(ID_AA64PFR0_EL1_EL3 | ID_AA64PFR0_EL1_EL2) + cbnz \xreg, \label + +3: +.endm + +/* + * Branch if current processor is a Cortex-A57 core. + */ +.macro branch_if_a57_core, xreg, a57_label + mrs \xreg, midr_el1 + lsr \xreg, \xreg, #4 + and \xreg, \xreg, #0x00000FFF + cmp \xreg, #0xD07 /* Cortex-A57 MPCore processor. */ + b.eq \a57_label +.endm + +/* + * Branch if current processor is a Cortex-A53 core. + */ +.macro branch_if_a53_core, xreg, a53_label + mrs \xreg, midr_el1 + lsr \xreg, \xreg, #4 + and \xreg, \xreg, #0x00000FFF + cmp \xreg, #0xD03 /* Cortex-A53 MPCore processor. */ + b.eq \a53_label +.endm + +/* + * Branch if current processor is a slave, + * choose processor with all zero affinity value as the master. + */ +.macro branch_if_slave, xreg, slave_label +#ifdef CONFIG_ARMV8_MULTIENTRY + mrs \xreg, mpidr_el1 + and \xreg, \xreg, 0xffffffffff /* clear bits [63:40] */ + and \xreg, \xreg, ~0x00ff000000 /* also clear bits [31:24] */ + cbnz \xreg, \slave_label +#endif +.endm + +/* + * Branch if current processor is a master, + * choose processor with all zero affinity value as the master. + */ +.macro branch_if_master, xreg, master_label +#ifdef CONFIG_ARMV8_MULTIENTRY + mrs \xreg, mpidr_el1 + and \xreg, \xreg, 0xffffffffff /* clear bits [63:40] */ + and \xreg, \xreg, ~0x00ff000000 /* also clear bits [31:24] */ + cbz \xreg, \master_label +#else + b \master_label +#endif +.endm + +/* + * Switch from EL3 to EL2 for ARMv8 + * @ep: kernel entry point + * @flag: The execution state flag for lower exception + * level, ES_TO_AARCH64 or ES_TO_AARCH32 + * @tmp: temporary register + * + * For loading 32-bit OS, x1 is machine nr and x2 is ftaddr. + * For loading 64-bit OS, x0 is physical address to the FDT blob. + * They will be passed to the guest. + */ +.macro armv8_switch_to_el2_m, ep, flag, tmp + msr cptr_el3, xzr /* Disable coprocessor traps to EL3 */ + mov \tmp, #CPTR_EL2_RES1 + msr cptr_el2, \tmp /* Disable coprocessor traps to EL2 */ + + /* Initialize Generic Timers */ + msr cntvoff_el2, xzr + + /* Initialize SCTLR_EL2 + * + * setting RES1 bits (29,28,23,22,18,16,11,5,4) to 1 + * and RES0 bits (31,30,27,26,24,21,20,17,15-13,10-6) + + * EE,WXN,I,SA,C,A,M to 0 + */ + ldr \tmp, =(SCTLR_EL2_RES1 | SCTLR_EL2_EE_LE |\ + SCTLR_EL2_WXN_DIS | SCTLR_EL2_ICACHE_DIS |\ + SCTLR_EL2_SA_DIS | SCTLR_EL2_DCACHE_DIS |\ + SCTLR_EL2_ALIGN_DIS | SCTLR_EL2_MMU_DIS) + msr sctlr_el2, \tmp + + mov \tmp, sp + msr sp_el2, \tmp /* Migrate SP */ + mrs \tmp, vbar_el3 + msr vbar_el2, \tmp /* Migrate VBAR */ + + /* Check switch to AArch64 EL2 or AArch32 Hypervisor mode */ + cmp \flag, #ES_TO_AARCH32 + b.eq 1f + + /* + * The next lower exception level is AArch64, 64bit EL2 | HCE | + * RES1 (Bits[5:4]) | Non-secure EL0/EL1. + * and the SMD depends on requirements. + */ +#ifdef CONFIG_ARMV8_PSCI + ldr \tmp, =(SCR_EL3_RW_AARCH64 | SCR_EL3_HCE_EN |\ + SCR_EL3_RES1 | SCR_EL3_NS_EN) +#else + ldr \tmp, =(SCR_EL3_RW_AARCH64 | SCR_EL3_HCE_EN |\ + SCR_EL3_SMD_DIS | SCR_EL3_RES1 |\ + SCR_EL3_NS_EN) +#endif + +#ifdef CONFIG_ARMV8_EA_EL3_FIRST + orr \tmp, \tmp, #SCR_EL3_EA_EN +#endif + msr scr_el3, \tmp + + /* Return to the EL2_SP2 mode from EL3 */ + ldr \tmp, =(SPSR_EL_DEBUG_MASK | SPSR_EL_SERR_MASK |\ + SPSR_EL_IRQ_MASK | SPSR_EL_FIQ_MASK |\ + SPSR_EL_M_AARCH64 | SPSR_EL_M_EL2H) + msr spsr_el3, \tmp + msr elr_el3, \ep + eret + +1: + /* + * The next lower exception level is AArch32, 32bit EL2 | HCE | + * SMD | RES1 (Bits[5:4]) | Non-secure EL0/EL1. + */ + ldr \tmp, =(SCR_EL3_RW_AARCH32 | SCR_EL3_HCE_EN |\ + SCR_EL3_SMD_DIS | SCR_EL3_RES1 |\ + SCR_EL3_NS_EN) + msr scr_el3, \tmp + + /* Return to AArch32 Hypervisor mode */ + ldr \tmp, =(SPSR_EL_END_LE | SPSR_EL_ASYN_MASK |\ + SPSR_EL_IRQ_MASK | SPSR_EL_FIQ_MASK |\ + SPSR_EL_T_A32 | SPSR_EL_M_AARCH32 |\ + SPSR_EL_M_HYP) + msr spsr_el3, \tmp + msr elr_el3, \ep + eret +.endm + +/* + * Switch from EL2 to EL1 for ARMv8 + * @ep: kernel entry point + * @flag: The execution state flag for lower exception + * level, ES_TO_AARCH64 or ES_TO_AARCH32 + * @tmp: temporary register + * + * For loading 32-bit OS, x1 is machine nr and x2 is ftaddr. + * For loading 64-bit OS, x0 is physical address to the FDT blob. + * They will be passed to the guest. + */ +.macro armv8_switch_to_el1_m, ep, flag, tmp, tmp2 + /* Initialize Generic Timers */ + mrs \tmp, cnthctl_el2 + /* Enable EL1 access to timers */ + orr \tmp, \tmp, #(CNTHCTL_EL2_EL1PCEN_EN |\ + CNTHCTL_EL2_EL1PCTEN_EN) + msr cnthctl_el2, \tmp + msr cntvoff_el2, xzr + + /* Initilize MPID/MPIDR registers */ + mrs \tmp, midr_el1 + msr vpidr_el2, \tmp + mrs \tmp, mpidr_el1 + msr vmpidr_el2, \tmp + + /* Disable coprocessor traps */ + mov \tmp, #CPTR_EL2_RES1 + msr cptr_el2, \tmp /* Disable coprocessor traps to EL2 */ + msr hstr_el2, xzr /* Disable coprocessor traps to EL2 */ + mov \tmp, #CPACR_EL1_FPEN_EN + msr cpacr_el1, \tmp /* Enable FP/SIMD at EL1 */ + + /* SCTLR_EL1 initialization + * + * setting RES1 bits (29,28,23,22,20,11) to 1 + * and RES0 bits (31,30,27,21,17,13,10,6) + + * UCI,EE,EOE,WXN,nTWE,nTWI,UCT,DZE,I,UMA,SED,ITD, + * CP15BEN,SA0,SA,C,A,M to 0 + */ + ldr \tmp, =(SCTLR_EL1_RES1 | SCTLR_EL1_UCI_DIS |\ + SCTLR_EL1_EE_LE | SCTLR_EL1_WXN_DIS |\ + SCTLR_EL1_NTWE_DIS | SCTLR_EL1_NTWI_DIS |\ + SCTLR_EL1_UCT_DIS | SCTLR_EL1_DZE_DIS |\ + SCTLR_EL1_ICACHE_DIS | SCTLR_EL1_UMA_DIS |\ + SCTLR_EL1_SED_EN | SCTLR_EL1_ITD_EN |\ + SCTLR_EL1_CP15BEN_DIS | SCTLR_EL1_SA0_DIS |\ + SCTLR_EL1_SA_DIS | SCTLR_EL1_DCACHE_DIS |\ + SCTLR_EL1_ALIGN_DIS | SCTLR_EL1_MMU_DIS) + msr sctlr_el1, \tmp + + mov \tmp, sp + msr sp_el1, \tmp /* Migrate SP */ + mrs \tmp, vbar_el2 + msr vbar_el1, \tmp /* Migrate VBAR */ + + /* Check switch to AArch64 EL1 or AArch32 Supervisor mode */ + cmp \flag, #ES_TO_AARCH32 + b.eq 1f + + /* Initialize HCR_EL2 */ + /* Only disable PAuth traps if PAuth is supported */ + mrs \tmp, id_aa64isar1_el1 + ldr \tmp2, =(ID_AA64ISAR1_EL1_GPI | ID_AA64ISAR1_EL1_GPA | \ + ID_AA64ISAR1_EL1_API | ID_AA64ISAR1_EL1_APA) + tst \tmp, \tmp2 + mov \tmp2, #(HCR_EL2_RW_AARCH64 | HCR_EL2_HCD_DIS) + orr \tmp, \tmp2, #(HCR_EL2_APK | HCR_EL2_API) + csel \tmp, \tmp2, \tmp, eq + msr hcr_el2, \tmp + + /* Return to the EL1_SP1 mode from EL2 */ + ldr \tmp, =(SPSR_EL_DEBUG_MASK | SPSR_EL_SERR_MASK |\ + SPSR_EL_IRQ_MASK | SPSR_EL_FIQ_MASK |\ + SPSR_EL_M_AARCH64 | SPSR_EL_M_EL1H) + msr spsr_el2, \tmp + msr elr_el2, \ep + eret + +1: + /* Initialize HCR_EL2 */ + ldr \tmp, =(HCR_EL2_RW_AARCH32 | HCR_EL2_HCD_DIS) + msr hcr_el2, \tmp + + /* Return to AArch32 Supervisor mode from EL2 */ + ldr \tmp, =(SPSR_EL_END_LE | SPSR_EL_ASYN_MASK |\ + SPSR_EL_IRQ_MASK | SPSR_EL_FIQ_MASK |\ + SPSR_EL_T_A32 | SPSR_EL_M_AARCH32 |\ + SPSR_EL_M_SVC) + msr spsr_el2, \tmp + msr elr_el2, \ep + eret +.endm + +#if defined(CONFIG_GICV3) +.macro gic_wait_for_interrupt_m xreg1 +0 : wfi + mrs \xreg1, ICC_IAR1_EL1 + msr ICC_EOIR1_EL1, \xreg1 + cbnz \xreg1, 0b +.endm +#elif defined(CONFIG_GICV2) +.macro gic_wait_for_interrupt_m xreg1, wreg2 +0 : wfi + ldr \wreg2, [\xreg1, GICC_AIAR] + str \wreg2, [\xreg1, GICC_AEOIR] + and \wreg2, \wreg2, #0x3ff + cbnz \wreg2, 0b +.endm +#endif + +#endif /* CONFIG_ARM64 */ + +#endif /* __ASSEMBLY__ */ +#endif /* __ASM_ARM_MACRO_H__ */ diff --git a/arch/arm64/kernel/boot.S b/arch/arm64/kernel/boot.S new file mode 100644 index 0000000..615be06 --- /dev/null +++ b/arch/arm64/kernel/boot.S @@ -0,0 +1,43 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * boot.S - the final jump to the payload. x0 = dtb, x1 = x2 = x3 = 0, + * MMU and caches off, D cache flushed, I cache invalidated. that is + * the whole contract from Documentation/arch/arm64/booting.rst. + * + * Copyright (C) 2026 Bradley Morgan <brads@mainlining.org> + */ + +#include <asm/linkage.h> + +.pushsection .text.tb_boot_linux, "ax" +ENTRY(tb_boot_linux) + /* ep in x0, dtb in x1, per the kernel boot protocol */ + + /* + * cache maintenance first, register setup last. x0-x18 are + * caller saved per the AAPCS, the flush helpers are free to + * clobber them, so the args ride in x20/x21 across the calls. + */ + mov x20, x0 /* entry point */ + mov x21, x1 /* dtb */ + + bl tb_flush_dcache_all + bl tb_invalidate_icache_all + + mov x8, x20 + mov x0, x21 + mov x1, xzr + mov x2, xzr + mov x3, xzr + + /* MMU off, caches off, the kernel sets up its own state */ + mrs x9, sctlr_el1 + bic x9, x9, #(1 << 0) /* M, MMU */ + bic x9, x9, #(1 << 2) /* C, D-cache */ + bic x9, x9, #(1 << 12) /* I, I-cache */ + msr sctlr_el1, x9 + isb + + br x8 +ENDPROC(tb_boot_linux) +.popsection diff --git a/arch/arm64/kernel/exceptions.c b/arch/arm64/kernel/exceptions.c new file mode 100644 index 0000000..7b40690 --- /dev/null +++ b/arch/arm64/kernel/exceptions.c @@ -0,0 +1,67 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * exceptions.c - report an abort through the console before parking, + * so a firmware handoff bug says why it died instead of hanging quiet. + * ESR/FAR decode follows armv8 DDI 0487, the EC and ISS fields. + * + * Copyright (C) 2026 Bradley Morgan <brads@mainlining.org> + */ + +#include <stdint.h> +#include <debug.h> + +struct exc_frame { + uint64_t esr; + uint64_t far; + uint64_t lr; +}; + +/* + * exception class from ESR, bits 31:26. the classes a bootloader can + * actually hit with any frequency. + */ +static const char *exc_class_str(uint64_t esr) +{ + switch (esr >> 26) { + case 0x04: return "data abort, lower EL"; + case 0x05: return "data abort, same EL"; + case 0x25: return "data abort, same EL"; + case 0x08: return "stack pointer misaligned"; + case 0x11: return "instruction abort, same EL"; + case 0x16: return "SError"; + case 0x1a: return "unhandled exception"; + case 0x22: return "pc alignment fault"; + case 0x24: return "unknown trap"; + case 0x26: return "same EL exception return"; + default: return "unknown EC"; + } +} + +/* + * far is only meaningful for the abort and alignment classes, note it + * for those and skip it otherwise so the report does not mislead. + */ +static int exc_far_valid(uint64_t esr) +{ + switch (esr >> 26) { + case 0x04: + case 0x05: + case 0x25: + case 0x11: + case 0x22: + return 1; + default: + return 0; + } +} + +void exc_report(uint64_t esr, uint64_t far, uint64_t lr) +{ + dprintf(CRITICAL, "tashaboot: exception %s\n", exc_class_str(esr)); + dprintf(CRITICAL, "esr %016llx lr %016llx\n", + (unsigned long long)esr, (unsigned long long)lr); + if (exc_far_valid(esr)) + dprintf(CRITICAL, "far %016llx\n", (unsigned long long)far); + + /* nothing recovers from an abort here, park after reporting */ +} diff --git a/arch/arm64/kernel/halt.c b/arch/arm64/kernel/halt.c new file mode 100644 index 0000000..d91d39a --- /dev/null +++ b/arch/arm64/kernel/halt.c @@ -0,0 +1,18 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * halt.c - stop the core, the ARM ARM's WFI loop. nothing recovers + * from a halt, the machine needs a reset. + * + * Copyright (C) 2026 Bradley Morgan <brads@mainlining.org> + */ + +#include <debug.h> + +void platform_halt(void) +{ + dprintf(ALWAYS, "HALT: spinning forever...\n"); + + for (;;) { + asm volatile("wfi"); + } +} 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 diff --git a/arch/arm64/kernel/tashaboot.lds b/arch/arm64/kernel/tashaboot.lds new file mode 100644 index 0000000..4f8dfb1 --- /dev/null +++ b/arch/arm64/kernel/tashaboot.lds @@ -0,0 +1,67 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * tashaboot arm64 memory layout. one segment, loaded at the bottom of + * RAM, right where qemu -kernel drops a raw image on virt. + * + * Copyright (C) 2026 Bradley Morgan <brads@mainlining.org> + */ + +OUTPUT_FORMAT("elf64-littleaarch64", "elf64-littleaarch64", "elf64-littleaarch64") +OUTPUT_ARCH(aarch64) +ENTRY(_start) + +SECTIONS +{ + . = 0x40000000; + + __image_copy_start = .; + _text_start = .; + + .text : + { + arch/arm64/kernel/start.o (.text.boot) + *(.text.boot) + *(.text*) + } + + . = ALIGN(8); + __text_end = .; + + .rodata : + { + *(SORT_BY_ALIGNMENT(.rodata*)) + } + + . = ALIGN(8); + __rodata_end = .; + + .data : + { + *(.data*) + } + + . = ALIGN(8); + __image_end = .; + + __bss_start = .; + .bss : + { + *(.bss*) + *(COMMON) + } + . = ALIGN(8); + __bss_end = .; + + __image_copy_end = .; + + /DISCARD/ : { *(.dynsym) } + /DISCARD/ : { *(.dynstr*) } + /DISCARD/ : { *(.dynamic*) } + /DISCARD/ : { *(.plt*) } + /DISCARD/ : { *(.interp*) } + /DISCARD/ : { *(.gnu*) } + /DISCARD/ : { *(.ARM.attributes) } + /DISCARD/ : { *(.comment) } + /DISCARD/ : { *(.note*) } + /DISCARD/ : { *(.eh_frame*) } +} diff --git a/arch/arm64/lib/cache.S b/arch/arm64/lib/cache.S new file mode 100644 index 0000000..d8ccea2 --- /dev/null +++ b/arch/arm64/lib/cache.S @@ -0,0 +1,100 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * cache.S - set/way cache maintenance, walked off CLIDR_EL1 the same + * way u-boot and the kernel's own __flush_dcache_all do it. needed + * before jumping to the payload so it starts from memory, not cache. + * + * Copyright (C) 2026 Bradley Morgan <brads@mainlining.org> + */ + +#include <asm/linkage.h> + +.pushsection .text.tb_dcache_level, "ax" +ENTRY(tb_dcache_level) + lsl x12, x0, #1 + msr csselr_el1, x12 /* select cache level */ + isb /* sync change of ccsidr_el1 */ + mrs x6, ccsidr_el1 /* read the new ccsidr_el1 */ + ubfx x2, x6, #0, #3 /* x2 <- log2(cache line size)-4 */ + ubfx x3, x6, #3, #10 /* x3 <- number of cache ways - 1 */ + ubfx x4, x6, #13, #15 /* x4 <- number of cache sets - 1 */ + add x2, x2, #4 /* x2 <- log2(cache line size) */ + clz w5, w3 /* x5 <- bit position of #ways */ + /* x12 <- cache level << 1 */ + /* x2 <- line length offset */ + /* x3 <- number of cache ways - 1 */ + /* x4 <- number of cache sets - 1 */ + /* x5 <- bit position of #ways */ + +loop_set: + mov x6, x3 /* x6 <- working copy of #ways */ +loop_way: + lsl x7, x6, x5 + orr x9, x12, x7 /* map way and level to cisw value */ + lsl x7, x4, x2 + orr x9, x9, x7 /* map set number to cisw value */ + dc cisw, x9 /* clean & invalidate by set/way */ + subs x6, x6, #1 /* decrement the way */ + b.ge loop_way + subs x4, x4, #1 /* decrement the set */ + b.ge loop_set + + ret +ENDPROC(tb_dcache_level) +.popsection + +/* + * void tb_flush_dcache_all(void) + * + * clean & invalidate the whole D cache by set/way. + */ +.pushsection .text.tb_flush_dcache_all, "ax" +ENTRY(tb_flush_dcache_all) + mov x1, x0 + dsb sy + mrs x10, clidr_el1 /* read clidr_el1 */ + ubfx x11, x10, #24, #3 /* x11 <- loc */ + cbz x11, finished /* if loc is 0, exit */ + mov x15, lr + mov x0, #0 /* start flush at cache level 0 */ + /* x0 <- cache level */ + /* x10 <- clidr_el1 */ + /* x11 <- loc */ + /* x15 <- return address */ + +loop_level: + add x12, x0, x0, lsl #1 /* x12 <- tripled cache level */ + lsr x12, x10, x12 + and x12, x12, #7 /* x12 <- cache type */ + cmp x12, #2 + b.lt skip /* skip if no cache or icache */ + bl tb_dcache_level /* flush this level */ +skip: + add x0, x0, #1 /* increment cache level */ + cmp x11, x0 + b.gt loop_level + + mov x0, #0 + msr csselr_el1, x0 /* restore csselr_el1 */ + dsb sy + isb + mov lr, x15 + +finished: + ret +ENDPROC(tb_flush_dcache_all) +.popsection + +/* + * void tb_invalidate_icache_all(void) + * + * I cache invalidation to PoU, one ic iallu covers the local core. + */ +.pushsection .text.tb_invalidate_icache_all, "ax" +ENTRY(tb_invalidate_icache_all) + ic iallu + dsb sy + isb + ret +ENDPROC(tb_invalidate_icache_all) +.popsection diff --git a/arch/arm64/lib/semihosting.S b/arch/arm64/lib/semihosting.S new file mode 100644 index 0000000..6e3fc31 --- /dev/null +++ b/arch/arm64/lib/semihosting.S @@ -0,0 +1,18 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * semihosting.S - the trap instruction itself. qemu answers this when + * it is started with -semihosting, and nothing happens without it, so + * every caller has to cope with the no-debugger case. + * + * Copyright (C) 2026 Bradley Morgan <brads@mainlining.org> + */ + +#include <asm/linkage.h> + +.pushsection .text.smh_trap, "ax" +/* long smh_trap(unsigned int sysnum, void *addr); */ +ENTRY(smh_trap) + hlt #0xf000 + ret +ENDPROC(smh_trap) +.popsection |
