diff options
| -rw-r--r-- | Makefile | 2 | ||||
| -rw-r--r-- | arch/arm64/kernel/monitor.S | 132 | ||||
| -rw-r--r-- | arch/arm64/kernel/start.S | 65 | ||||
| -rw-r--r-- | arch/arm64/lib/gic.c | 14 | ||||
| -rw-r--r-- | common/console.c | 29 | ||||
| -rw-r--r-- | common/main.c | 32 |
6 files changed, 250 insertions, 24 deletions
@@ -18,7 +18,7 @@ CFLAGS := -nostdlib -ffreestanding -mgeneral-regs-only \ LDFLAGS := -T arch/arm64/kernel/tashaboot.lds -OBJS := arch/arm64/kernel/start.o \ +OBJS := arch/arm64/kernel/start.o arch/arm64/kernel/monitor.o \ arch/arm64/kernel/exceptions.o \ arch/arm64/kernel/halt.o \ arch/arm64/kernel/boot.o \ diff --git a/arch/arm64/kernel/monitor.S b/arch/arm64/kernel/monitor.S new file mode 100644 index 0000000..c6f5ec8 --- /dev/null +++ b/arch/arm64/kernel/monitor.S @@ -0,0 +1,132 @@ +/* + * monitor.S - the EL3 secure monitor, the resident layer real + * firmware ships. the loader drops to non-secure and never + * returns, but the kernel keeps calling into firmware: PSCI + * through the SMC conduit, and on hardware with the security + * extension the group routing of the interrupt controller is + * only writable from here. + * + * the entry path runs once per PE: configure EL3, install the + * monitor vectors, hand the next stage non-secure EL2 in the + * manual's boot state. SMCCC calls from the kernel trap into + * the SMC slot, the C dispatcher behind it is the same one the + * hvc path uses. + * + * Copyright (C) 2026 Bradley Morgan <brads@mainlining.org> + */ + +/* + * the monitor stack. SP_EL3 needs memory no non-secure stage + * will touch, the region after the loader stack, sixteen + * bytes a call deep at most. + */ +.section .bss.el3stack, "aw", %nobits +.align 4 +.globl __el3_stack_bottom +__el3_stack_bottom: + .quad 0, 0, 0, 0 + .quad 0, 0, 0, 0 +.globl __el3_stack_top +__el3_stack_top: + +/* + * EL3 vectors, same sixteen slot layout every exception level + * uses. only the lower EL sync slot carries work, the SMC + * conduit, everything else parks. + */ +.balign 2048 +.globl tb_el3_vectors +tb_el3_vectors: + /* 0x000: current EL, SP_EL0, unused */ + .align 7 + b el3_park + .align 7 + b el3_park + .align 7 + b el3_park + .align 7 + b el3_park + + /* 0x200: current EL, SP_ELx, unused */ + .align 7 + b el3_park + .align 7 + b el3_park + .align 7 + b el3_park + .align 7 + b el3_park + + /* 0x400: lower EL, AArch64, the SMC conduit lives here */ + .align 7 + b el3_park + .align 7 + b el3_park + .align 7 + b el3_park + .align 7 + b el3_smc + + /* 0x600: lower EL, AArch32, unused */ + .align 7 + b el3_park + .align 7 + b el3_park + .align 7 + b el3_park + .align 7 + b el3_park + +/* + * one time per PE, from the reset path. x30 = the next stage + * entry in non-secure EL2, x0 = the dtb pointer. + */ +.globl tb_monitor_init +tb_monitor_init: + /* SP_EL3 on its own region */ + adr x1, __el3_stack_top + msr spsel, #0 + mov sp, x1 + msr spsel, #1 + + /* the monitor vectors */ + adr x1, tb_el3_vectors + msr vbar_el3, x1 + isb + + /* + * SMC as the conduit, SVE traps off, no interrupt routing + * into EL3: FIQ/IRQ stay whatever SCR_EL3.SCR left them, + * the kernel owns the world below. + */ + mrs x1, scr_el3 + bic x1, x1, #(1 << 2) /* SMD, SMC enabled */ + msr scr_el3, x1 + isb + + ret + +el3_park: + b el3_park + +/* + * the SMC trap from lower EL. the SMCCC calling convention is + * the SMC register set, function id in x0, arguments x1 to + * x3, results in x0 to x3. x17 and x18 are caller save in + * this convention, the dispatcher clobbers x0 to x18. + */ +el3_smc: + stp x29, x30, [sp, #-16]! + mov x29, sp + stp x19, x20, [sp, #-16]! + stp x21, x22, [sp, #-16]! + stp x23, x24, [sp, #-16]! + + bl tb_psci_dispatch + + ldp x23, x24, [sp], #16 + ldp x21, x22, [sp], #16 + ldp x19, x20, [sp], #16 + ldp x29, x30, [sp], #16 + + eret diff --git a/arch/arm64/kernel/start.S b/arch/arm64/kernel/start.S index 6ee9941..2a5e2ae 100644 --- a/arch/arm64/kernel/start.S +++ b/arch/arm64/kernel/start.S @@ -38,12 +38,16 @@ reset: /* keep the dtb pointer before anything clobbers x0 */ mov x19, x0 - /* park secondary cores, they have nothing to do yet */ + /* + * park secondary cores, they have nothing to do yet. at + * EL3 they still get the monitor: a firmware call on any + * PE must land in a handler, a secondary with no EL3 + * vectors traps into nothing. + */ mrs x0, mpidr_el1 and x0, x0, #0xff - cbnz x0, park + cbnz x0, secondary_boot - /* which EL are we in, 0x8 per level shifted into bits 3:2 */ mrs x0, CurrentEL lsr x0, x0, #2 cmp x0, #3 @@ -54,12 +58,34 @@ reset: b.eq mmu_check b park +secondary_boot: + mrs x0, CurrentEL + lsr x0, x0, #2 + cmp x0, #3 + b.ne park + /* + * the same security state as the primary: SCR_EL3.NS + * clear leaves a PE secure, and a secondary released + * into the kernel secure is the inconsistent mode boot + * the kernel warns about, its calls trap to EL3 as if + * they were firmware's own. + */ + mrs x0, scr_el3 + orr x0, x0, #1 + msr scr_el3, x0 + isb + bl tb_monitor_init + 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). + * EL3 holds the security state, so the monitor lives here: + * vectors, its own stack, the SMC conduit. it is resident + * after this, the kernel's firmware calls trap into it. */ + bl tb_monitor_init + + /* the kernel runs non-secure, drop to the EL2 it prefers */ mrs x0, scr_el3 orr x0, x0, #1 /* SCR_EL3.NS = 1, non-secure */ msr scr_el3, x0 @@ -222,6 +248,24 @@ park: wfe b 1b 2: + /* interrupts masked at release, the manual's boot state */ + msr daifset, #0xf + /* + * every PE must read the same virtual counter. whatever + * ran before this loader could have left a per cpu offset + * in the virtual counter view, the kernel has no way to + * repair that itself. CNTVOFF_EL2 is writable at EL2 and + * the write holds for the EL1 virtual timer the kernel + * runs on. below EL2 it is out of reach, the reset value + * is the best a lower EL can do. + */ + mrs x4, CurrentEL + lsr x4, x4, #2 + cmp x4, #2 + b.lt 3f + msr cntvoff_el2, xzr + isb +3: mov x0, xzr /* secondaries enter with x0-x3 zero */ mov x1, xzr mov x2, xzr @@ -396,6 +440,15 @@ exc_serr: mov x1, #0 mov x2, lr bl exc_report + /* + * an SError while this loader runs means the machine is + * broken. handing the kernel a cpu that already lost is + * worse than stopping: report, then drive the reset domain + * the same way PSCI SYSTEM_RESET does. the reset call does + * not return, the park below is the fallback if a reset + * domain ignores the request. + */ + bl tb_system_reset ldp x29, x30, [sp], #16 b park diff --git a/arch/arm64/lib/gic.c b/arch/arm64/lib/gic.c index 11bb9cd..647e987 100644 --- a/arch/arm64/lib/gic.c +++ b/arch/arm64/lib/gic.c @@ -27,7 +27,6 @@ /* distributor registers, offsets from the GICD base */ #define GICD_CTLR 0x000 #define GICD_TYPER 0x004 -#define GICD_IGROUPR(n) (0x080 + (n) * 4) #define GICD_ISENABLER(n) (0x100 + (n) * 4) #define GICD_ICENABLER(n) (0x180 + (n) * 4) #define GICD_ICPENDR(n) (0x280 + (n) * 4) @@ -79,13 +78,14 @@ int tb_gic_init(uintptr_t gicd, uintptr_t gicc) lines = gicd_irq_lines(gicd); /* - * every interrupt in group 1, the non-secure group. the - * kernel does not see group 0 interrupts on non-secure - * hardware, and a bootloader that leaves any line in the - * secure group strands it. + * the group routing is deliberately untouched. the group + * registers are the secure world's, a non-secure loader's + * writes are dropped on hardware that implements the + * security extension, and on emulators that accept them + * the timer's per cpu interrupts stop reaching the + * kernel. group config belongs to the EL3 monitor, this + * loader runs without one. */ - for (n = 0; n < lines; n++) - writel(0xffffffff, REG32(gicd + GICD_IGROUPR(n))); /* no per interrupt enables, nothing pending */ for (n = 0; n < lines; n++) { diff --git a/common/console.c b/common/console.c index bdd24c1..64e5128 100644 --- a/common/console.c +++ b/common/console.c @@ -54,14 +54,23 @@ #define UART_IBRD_VAL 13 #define UART_FBRD_VAL 44 -#define PL011_BASE 0x09000000UL +/* the base comes from the devicetree walk, the qemu default + * only covers the dev path before the walk runs + */ +static uintptr_t pl011_base = 0x09000000UL; + +void tb_console_set_pl011(uintptr_t base) +{ + if (base) + pl011_base = base; +} static int console_uart_ok; static void uart_putc(char c) { - volatile uint32_t *fr = (volatile uint32_t *)(PL011_BASE + UART_FR); - volatile uint32_t *dr = (volatile uint32_t *)(PL011_BASE + UART_DR); + volatile uint32_t *fr = (volatile uint32_t *)(pl011_base + UART_FR); + volatile uint32_t *dr = (volatile uint32_t *)(pl011_base + UART_DR); /* TXFF can happen mid line on slow consoles, wait it out */ while (*fr & UART_FR_TXFF) @@ -77,12 +86,12 @@ static void uart_putc(char c) */ static int uart_init(void) { - volatile uint32_t *cr = (volatile uint32_t *)(PL011_BASE + UART_CR); - volatile uint32_t *ibrd = (volatile uint32_t *)(PL011_BASE + UART_IBRD); - volatile uint32_t *fbrd = (volatile uint32_t *)(PL011_BASE + UART_FBRD); - volatile uint32_t *lcrh = (volatile uint32_t *)(PL011_BASE + UART_LCRH); - volatile uint32_t *imsc = (volatile uint32_t *)(PL011_BASE + UART_IMSC); - volatile uint32_t *icr = (volatile uint32_t *)(PL011_BASE + UART_ICR); + volatile uint32_t *cr = (volatile uint32_t *)(pl011_base + UART_CR); + volatile uint32_t *ibrd = (volatile uint32_t *)(pl011_base + UART_IBRD); + volatile uint32_t *fbrd = (volatile uint32_t *)(pl011_base + UART_FBRD); + volatile uint32_t *lcrh = (volatile uint32_t *)(pl011_base + UART_LCRH); + volatile uint32_t *imsc = (volatile uint32_t *)(pl011_base + UART_IMSC); + volatile uint32_t *icr = (volatile uint32_t *)(pl011_base + UART_ICR); /* disable, mask irq, clear pending, divisors, fifo, enable tx */ *cr = 0; @@ -95,7 +104,7 @@ static int uart_init(void) /* self test write, TXFF clearing means the uart answers */ uart_putc('\0'); - while (*(volatile uint32_t *)(PL011_BASE + UART_FR) & UART_FR_BUSY) + while (*(volatile uint32_t *)(pl011_base + UART_FR) & UART_FR_BUSY) ; return 0; diff --git a/common/main.c b/common/main.c index a0237af..b53c4b0 100644 --- a/common/main.c +++ b/common/main.c @@ -67,6 +67,23 @@ void tashaboot_main(uintptr_t fw_arg) struct tb_image img; int ret; + /* + * the console uart the machine named, before the first + * print. the base is the first reg pair of the pl011 + * node, the same walk the gic used, no board hardcodes. + */ + { + extern int tb_dtb_find_reg0(uintptr_t dtb, + const char *name, + uintptr_t *addr, size_t *size); + extern void tb_console_set_pl011(uintptr_t base); + uintptr_t uart = 0; + size_t usz = 0; + + if (tb_dtb_find_reg0(fw_arg, "pl011", &uart, &usz) == 0) + tb_console_set_pl011(uart); + } + if (tb_console_init()) return; @@ -236,6 +253,21 @@ void tashaboot_main(uintptr_t fw_arg) dprintf(ALWAYS, "loaded %llu bytes at %lx, entry %lx\n", (unsigned long long)img.size, img.load, img.ep); + +#ifdef TB_TEST_SMC + { + register uint64_t r0 asm("x0") = 0x84000000; + register uint64_t r1 asm("x1") = 0; + register uint64_t r2 asm("x2") = 0; + register uint64_t r3 asm("x3") = 0; + + asm volatile("smc #0" + : "+r"(r0), "+r"(r1), "+r"(r2), "+r"(r3)); + dprintf(ALWAYS, "smc conduit: psci version %lx\n", + (unsigned long)r0); + } +#endif + dprintf(ALWAYS, "jumping\n"); tb_boot_linux(img.ep, fw_arg); |
