summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile2
-rw-r--r--arch/arm64/kernel/monitor.S132
-rw-r--r--arch/arm64/kernel/start.S65
-rw-r--r--arch/arm64/lib/gic.c14
-rw-r--r--common/console.c29
-rw-r--r--common/main.c32
6 files changed, 250 insertions, 24 deletions
diff --git a/Makefile b/Makefile
index 6112249..eff00a7 100644
--- a/Makefile
+++ b/Makefile
@@ -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);