Fix riscv64 boot crash: add missing kernel stack trampoline

kernel_main on riscv64 ran directly on EDK2's UEFI boot-time stack, with
no dedicated stack switch — amd64 has always had a kernel_entry.S
trampoline for exactly this reason (its own comment: "the FORTH
interpreter + DOE experiment loop can easily exceed that depth").
aarch64 happens to get away without one because its firmware's default
stack is apparently larger, but that was never a guarantee.

On riscv64 the VM bootstrap's call depth (27 word-registration modules
-> physics/SSM init -> Tripod capsule birth) overflowed that small
stack, corrupting a return address and producing a wild jump / page
fault right after vm_init_with_host() returned — reproduced consistently
across the 2026-08-01 DoE campaign logs.

- src/starkernel/arch/riscv64/kernel_entry.S (new): RISC-V stack-switch
  trampoline mirroring amd64's, giving the kernel a dedicated 2 MiB BSS
  stack before anything deep runs.
- kernel_main.c: riscv64 now builds kernel_main_impl (invoked via the
  trampoline) instead of kernel_main directly, same pattern as amd64.
- Makefile.starkernel: wires the new file into the riscv64 build.
- uefi_loader.c: RAW_LOG() was silently a no-op on every non-amd64 arch;
  added a real raw-UART writer for riscv64 (QEMU virt's uart8250 at MMIO
  0x10000000) so existing loader diagnostics actually produce output.

Verified: all three architectures boot clean to [Hera] ok> in the
required order (amd64, aarch64, riscv64); logs and DoE CSVs from these
runs included.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-08-02 06:59:48 -04:00
co-authored by Claude Sonnet 5
parent a852db2209
commit 736627510b
11 changed files with 1368435 additions and 6 deletions
@@ -0,0 +1,69 @@
/*
* kernel_entry.S kernel stack setup and entry trampoline (riscv64)
*
* UEFI hands control to kernel_main() running on EDK2's own boot-time stack.
* Unlike amd64 (see arch/amd64/kernel_entry.S, which has carried this same
* trampoline since early on), riscv64 previously called straight into
* kernel_main_impl with no stack switch at all. The VM bootstrap's call
* depth (27 chained word-registration modules, physics/SSM init, capsule
* birth, Tripod fleet spawn) overflowed EDK2's riscv64 boot stack partway
* through vm_init_with_host()'s return, corrupting the return address and
* producing a wild jump shortly after observed as a load/instruction
* page fault (scause 0xd or 0xc) whose exact address drifted between
* builds while the crash always landed in this same call region. amd64
* never had this problem because kernel_entry.S already switches to a
* dedicated 2 MiB BSS stack before anything deep runs; aarch64 apparently
* gets away with EDK2's default boot stack being large enough, but that's
* incidental, not a guarantee.
*
* kernel_main is the symbol the UEFI loader calls (extern in uefi_loader.c).
* kernel_main_impl is defined in kernel_main.c and contains all the C code.
*
* Calling convention: RISC-V LP64D boot_info pointer arrives in a0 and is
* passed unchanged to kernel_main_impl via the tail-call.
*/
#include "starkernel/boot_info_offsets.h"
/* 2 MiB kernel stack in BSS — fallback when no dynamic stack requested */
.section .bss
.align 4
.global g_kernel_stack
g_kernel_stack:
.space 0x200000
g_kernel_stack_top:
.global g_kernel_stack_top
.section .text
.extern kernel_main_impl
/*
* kernel_main stack-switch trampoline, then tail-call kernel_main_impl.
*
* a0 = BootInfo* (RISC-V LP64D ABI first argument register)
*
* Stack selection (checked in order):
* 1. boot_info->kernel_stack_base != 0 use loader-allocated stack
* (top = base + size, aligned down to 16)
* 2. Otherwise fall back to g_kernel_stack (2 MiB BSS)
*
* Never returns; kernel_main_impl runs the REPL forever or panics.
*/
.global kernel_main
kernel_main:
ld t0, BOOT_INFO_KERNEL_STACK_BASE_OFFSET(a0)
bnez t0, .Ldynamic_stack
/* BSS fallback */
la t0, g_kernel_stack_top
j .Lstack_ready
.Ldynamic_stack:
ld t1, BOOT_INFO_KERNEL_STACK_SIZE_OFFSET(a0)
add t0, t0, t1 /* top = base + size (stack grows down) */
.Lstack_ready:
andi t0, t0, -16 /* 16-byte alignment (RISC-V psABI requirement) */
mv sp, t0
mv s0, zero /* terminate frame-pointer chain */
tail kernel_main_impl /* a0 (boot_info) already in place */
+41
View File
@@ -180,6 +180,47 @@ static void raw_serial_puts(const char *s)
}
}
#define RAW_LOG(str) raw_serial_puts(str)
#elif defined(__riscv) || defined(ARCH_RISCV64)
/*
* QEMU's riscv "virt" machine exposes a 16550-compatible UART (OpenSBI
* reports "Platform Console Device: uart8250") as byte-addressed MMIO at
* 0x10000000 (matches Domain0 Region03 in the OpenSBI boot banner). This
* writes directly to the UART registers, exactly like the amd64
* raw_serial_* helpers above, so RAW_LOG() actually produces output on
* riscv64 instead of silently no-op'ing (previously the case for every
* arch except amd64).
*/
#define UART_MMIO_BASE 0x10000000UL
static inline void raw_mmio_outb(uint64_t addr, uint8_t val)
{
*(volatile uint8_t *)addr = val;
}
static inline uint8_t raw_mmio_inb(uint64_t addr)
{
return *(volatile uint8_t *)addr;
}
static void raw_serial_putc(char c)
{
while ((raw_mmio_inb(UART_MMIO_BASE + 5) & 0x20) == 0) { }
raw_mmio_outb(UART_MMIO_BASE + 0, (uint8_t)c);
}
static void raw_serial_puts(const char *s)
{
while (*s)
{
char c = *s++;
if (c == '\n') raw_serial_putc('\r');
raw_serial_putc(c);
}
}
#define RAW_LOG(str) raw_serial_puts(str)
#else
#define RAW_LOG(str) ((void)0)
#endif
+5 -5
View File
@@ -325,10 +325,10 @@ static void print_banner(void) {
/**
* @brief Main kernel entry point after UEFI handoff — executes milestones M0M6.
*
* On amd64, @c kernel_entry.S switches the stack from UEFI's default to a 2 MiB
* zero-initialised BSS stack and tail-calls this function as @c kernel_main_impl.
* On aarch64 and riscv64 the assembly trampoline is not yet implemented and the
* UEFI loader calls @c kernel_main directly.
* On amd64 and riscv64, @c kernel_entry.S switches the stack from UEFI's default
* to a 2 MiB zero-initialised BSS stack and tail-calls this function as
* @c kernel_main_impl. On aarch64 the assembly trampoline is not yet implemented
* and the UEFI loader calls @c kernel_main directly.
*
* Milestone sequence:
* - **M0 — Architecture early init** (@c arch_early_init()): On amd64, installs a
@@ -357,7 +357,7 @@ static void print_banner(void) {
* framebuffer descriptor, runtime services pointer, and parsed
* kernel command-line arguments.
*/
#if defined(__x86_64__)
#if defined(__x86_64__) || defined(__riscv)
void kernel_main_impl(BootInfo *boot_info) {
#else
void kernel_main(BootInfo *boot_info) {
+1 -1
View File
@@ -252,7 +252,7 @@ void vm_init_with_host(VM* vm, const VMHostServices *host)
/* Register Forth-79 wordset */
register_forth79_words(vm);
#ifdef __STARKERNEL__
#if SK_PARITY_DEBUG
console_println("[VM_BOOTSTRAP] register_forth79_words complete");