/* StarForth — Steady-State Virtual Machine Runtime Copyright (c) 2023–2025 Robert A. James All rights reserved. This file is part of the StarForth project. Licensed under the StarForth License, Version 1.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at: https://github.com/star.4th@proton.me/StarForth/LICENSE.txt This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and noninfringement. See the License for the specific language governing permissions and limitations under the License. StarForth — Steady-State Virtual Machine Runtime Copyright (c) 2023–2025 Robert A. James All rights reserved. This file is part of the StarForth project. Licensed under the StarForth License, Version 1.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at: https://github.com/star.4th@proton.me/StarForth/LICENSE.txt This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and noninfringement. See the License for the specific language governing permissions and limitations under the License. */ /** * uefi_loader.c - UEFI boot loader for StarKernel * * This loader loads the StarKernel ELF binary from the ESP, * parses it, loads segments, applies relocations, and jumps to entry. */ #include "uefi.h" #include "arch.h" #include "elf_loader.h" #include "elf64.h" #include "starkernel/cmdline.h" #include "starkernel/boot_info_offsets.h" #include /* Verify BootInfo field offsets match boot_info_offsets.h (BOOT_INFO_OFFSETS) */ _Static_assert(offsetof(BootInfo, kernel_stack_base) == BOOT_INFO_KERNEL_STACK_BASE_OFFSET, "BOOT_INFO_KERNEL_STACK_BASE_OFFSET mismatch — update boot_info_offsets.h"); _Static_assert(offsetof(BootInfo, kernel_stack_size) == BOOT_INFO_KERNEL_STACK_SIZE_OFFSET, "BOOT_INFO_KERNEL_STACK_SIZE_OFFSET mismatch — update boot_info_offsets.h"); /* For monolithic build: kernel_main is linked directly */ #ifdef MONOLITHIC_BUILD extern void kernel_main(BootInfo *boot_info); #endif #if defined(ARCH_AMD64) #define COM1_BASE 0x3F8 /* Set by raw_serial_init() only after a 16550 presence probe succeeds. * When 0, raw_serial_putc() drops bytes immediately so boot can never * wedge on a board whose 0x3F8 decode has no UART behind it (real * mini-PCs like the Beelink SER5 typically expose no legacy COM port, * whereas QEMU always emulates one — this is why this hang only bites * on hardware). */ static int raw_serial_ready = 0; /* Bounded THRE poll bound: provides the same "drop, don't hang" safety * even if the probe above passes on a phantom decode. */ #define RAW_SERIAL_THRE_MAX_SPIN 100000u /** * @brief Write a byte to an x86 I/O port via @c OUT (early-boot raw path). * * Used exclusively by the amd64 early-boot serial helper functions * (@c raw_serial_init(), @c raw_serial_putc()) before the kernel console * subsystem is initialised. Operates identically to @c outb() in * @c interrupts.c but is a separate inline to avoid a cross-unit dependency * in the UEFI loader compilation unit. * * Only compiled when @c ARCH_AMD64 is defined. * * @param port 16-bit I/O port address (e.g., @c COM1_BASE + 0 = 0x3F8). * @param val Byte value to write. */ static inline void raw_outb(uint16_t port, uint8_t val) { __asm__ volatile ("outb %0, %1" : : "a"(val), "Nd"(port)); } /** * @brief Read a byte from an x86 I/O port via @c IN (early-boot raw path). * * Used by @c raw_serial_putc() to poll the UART Line Status Register * (COM1 + 5) for the Transmitter Holding Register Empty (THRE) bit * before writing a character, preventing serial output corruption. * Only compiled when @c ARCH_AMD64 is defined. * * @param port 16-bit I/O port address (e.g., @c COM1_BASE + 5 = 0x3FD). * @return Byte value read from the port. */ static inline uint8_t raw_inb(uint16_t port) { uint8_t ret; __asm__ volatile ("inb %1, %0" : "=a"(ret) : "Nd"(port)); return ret; } /** * @brief Initialise COM1 (0x3F8) to 115200 8N1 before UEFI console exits. * * Programs the 16550-compatible UART at base address @c COM1_BASE using * direct port I/O. Sequence: * 1. Disable all UART interrupts (IER = 0x00). * 2. Assert DLAB (Divisor Latch Access Bit) to access the divisor registers. * 3. Write divisor = 1 (115200 baud at 115200 Hz base clock) to DLL and DLH. * 4. Clear DLAB; configure 8N1 (8 data bits, no parity, 1 stop bit; LCR=0x03). * 5. Enable FIFO, clear TX/RX FIFOs, set 14-byte interrupt threshold (FCR=0xC7). * 6. Assert RTS and DSR (MCR=0x0B). * * Called once at the beginning of @c efi_main() on amd64 to ensure the * @c RAW_LOG() macro can emit diagnostic messages throughout the boot * sequence, including phases where the UEFI console is no longer available. * Only compiled when @c ARCH_AMD64 is defined. */ static void raw_serial_init(void) { /* Disable interrupts */ raw_outb(COM1_BASE + 1, 0x00); /* Enable DLAB */ raw_outb(COM1_BASE + 3, 0x80); /* Divisor 1 = 115200 baud */ raw_outb(COM1_BASE + 0, 0x01); raw_outb(COM1_BASE + 1, 0x00); /* 8N1 */ raw_outb(COM1_BASE + 3, 0x03); /* Enable FIFO, clear, 14-byte threshold */ raw_outb(COM1_BASE + 2, 0xC7); /* RTS/DSR set */ raw_outb(COM1_BASE + 4, 0x0B); /* 16550 presence probe: the scratch register (COM1+7) reads back what * was written only when a real UART owns this decode. Unclaimed ports * (or no I/O bridge routing) return garbage/0xFF, so the probe fails * and serial is disabled — otherwise raw_serial_putc()'s THRE poll * could spin forever on a board with no COM port. */ raw_outb(COM1_BASE + 7, 0x5A); raw_serial_ready = (raw_inb(COM1_BASE + 7) == 0x5A) ? 1 : 0; if (raw_serial_ready) { /* Sanity-check that THRE can actually be observed; if not, the * decode is a phantom — disable serial rather than risk a spin. */ unsigned int spin = 0; while ((raw_inb(COM1_BASE + 5) & 0x20) == 0 && spin < RAW_SERIAL_THRE_MAX_SPIN) { ++spin; } if (spin >= RAW_SERIAL_THRE_MAX_SPIN) { raw_serial_ready = 0; } } } /** * @brief Write a single character to COM1 with THRE polling. * * Spins on bit 5 (Transmitter Holding Register Empty) of the Line Status * Register (COM1 + 5) until the UART is ready to accept a new byte, then * writes @p c to the Transmitter Holding Register (COM1 + 0). This busy- * wait is acceptable in the UEFI loader phase because interrupts are * either managed by UEFI or not yet configured by the kernel. * * Only compiled when @c ARCH_AMD64 is defined. * * @param c Character to transmit. */ static void raw_serial_putc(char c) { /* No UART behind 0x3F8 (or probe failed): drop the byte, never block. */ if (!raw_serial_ready) return; unsigned int spin = 0; while ((raw_inb(COM1_BASE + 5) & 0x20) == 0) { /* Bounded: on hardware whose probe passed but THRE never asserts * (phantom decode), drop the character instead of hanging boot. */ if (++spin >= RAW_SERIAL_THRE_MAX_SPIN) { raw_serial_ready = 0; return; } } raw_outb(COM1_BASE + 0, (uint8_t)c); } /** * @brief Write a NUL-terminated string to COM1 with implicit LF→CRLF conversion. * * Iterates over @p s and calls @c raw_serial_putc() for each character. * A bare @c '\\n' is preceded by a @c '\\r' to produce proper CRLF line * endings expected by serial terminals. Used via the @c RAW_LOG() macro * for diagnostic output before the kernel console subsystem is live. * * Only compiled when @c ARCH_AMD64 is defined. * * @param s NUL-terminated string to transmit. */ 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) #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 static BootInfo g_boot_info = {0}; /* * Numbered UEFI debug checkpoints (pre-ExitBootServices bring-up aid). * * First-boot debugging on hardware with no serial/UART and no framebuffer * driver yet: ConOut (the UEFI firmware's own text console on the * HDMI/DP-connected monitor) is the only output available before * ExitBootServices(). Each checkpoint prints "[CKPT nnn]