Reorganize source tree: kernel/, v3/, v4/ split and board infrastructure

Source tree reorganization:
- Move StarForth v3 engine to v3/ (src/, include/, Makefile)
- Move kernel to kernel/ (src/, include/, linker/, Makefile)
- Create v4/ skeleton for F18-ISA golden model (DECOMPOSITION.md, JUSTIFICATION.md)
- Move FABRIC-0..4.md to docs/fabric/
- Move ONTOLOGY.md and ROADMAP.md to docs/

Board infrastructure:
- Add boards/ser5/, boards/raspi/, boards/milkv/, boards/zynq7020/
- Each board has board.mk (ISA, CPU flags, boot recipe) and README.md
- Root Makefile becomes thin dispatcher: boot_image, all, clean, docs take TARGET
- make boot_image TARGET=SER5|RASPI|MILKV builds one GPT/MBR image per board
- ZYNQ7020 target exists but stops with clear error (ARMv7 port not built yet)
- scripts/mkdiskimage.sh builds disk images for all boards

Docs pipeline:
- docs/book/ with LaTeX master (main.tex) and Makefile
- pandoc converts Markdown to LaTeX at build time
- Two Lua filters: table-widths.lua (wide tables wrap), code-breaks.lua (inline code breaks)
- make docs builds single PDF (754 pages, 0 missing characters)
- make docs TARGET=<board> adds board appendix
- build/docs/<book|board>/meta.tex stamps git commit into PDF

Bug fixes:
- 42 include paths that only worked by accident now use correct relative paths
- clang-18 hardcode replaced with configurable CC variable (fixed aarch64 build)
- Pi 5: kernel_2712.img linked at 0x80000, .bss zeroed, memory reserved
- Doxyfile, .clang-tidy, README.md, Kconfig paths updated

Verified:
- Hosted v3 build passes 1012 tests, 0 failures
- SER5 image boots in QEMU (OVMF), POST passes, K exact (65536 = Q48_ONE)
- Milk-V image boots in QEMU (OpenSBI + U-Boot + bootefi), POST passes
- make clean TARGET=<board> removes only that board and its ISA objects
- make all builds all boards, hosted v3, and docs in one run

Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
rajames
2026-10-01 15:40:09 -04:00
co-authored by Junie
parent 3c709c115b
commit a8b70e88d3
630 changed files with 2646 additions and 2036 deletions
+999
View File
@@ -0,0 +1,999 @@
/*
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 <stddef.h>
/* 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] <label>" and
* stalls a couple of seconds so a photo can be taken, then the boot
* continues. Purely additive instrumentation for a single debugging pass;
* set UEFI_DEBUG_CHECKPOINTS to 0 (or pass -DUEFI_DEBUG_CHECKPOINTS=0) to
* strip it back out once real console/logging exists.
*
* Deliberately confined to the region before the final GetMemoryMap() /
* ExitBootServices() pair below: that region must call nothing that could
* perturb the memory map (see the "DO NOTHING" comment on Phase B), so no
* checkpoint fires between GetMemoryMap() and ExitBootServices(), and none
* fires after ExitBootServices() returns (ConOut is not guaranteed usable
* post-EBS on real firmware, and this loader has no serial fallback that
* would be visible on the target board anyway).
*
* Checkpoint list (see also the cross-reference note in this file's
* directory README / commit message):
* 1. Entered efi_main - ConOut live
* 2. Serial (COM1) initialized [ARCH_AMD64 only]
* 3. kernel.elf loaded from ESP - OK [!MONOLITHIC_BUILD only]
* 4. Command line parsed
* 5. Kernel stack allocation decided
* 6. Boot info collected (ACPI table located)
* 7. GOP query complete (outcome-specific label)
* 8. About to enter ExitBootServices retry loop
*/
#ifndef UEFI_DEBUG_CHECKPOINTS
#define UEFI_DEBUG_CHECKPOINTS 1
#endif
#if UEFI_DEBUG_CHECKPOINTS
#define DEBUG_CHECKPOINT_STALL_US (2u * 1000u * 1000u)
static void debug_checkpoint(EFI_SYSTEM_TABLE *SystemTable, UINT32 id, const CHAR16 *label)
{
CHAR16 line[192];
UINTN pos = 0;
UINTN i;
UINTN val;
static const CHAR16 prefix[] = L"[CKPT ";
static const CHAR16 mid[] = L"] ";
static const CHAR16 nl[] = L"\r\n";
for (i = 0; prefix[i] != 0; ++i) line[pos++] = prefix[i];
/* zero-padded 3-digit checkpoint number */
val = id;
{
CHAR16 digits[3];
int d;
for (d = 2; d >= 0; --d) {
digits[d] = (CHAR16)(L'0' + (val % 10));
val /= 10;
}
for (d = 0; d < 3; ++d) line[pos++] = digits[d];
}
for (i = 0; mid[i] != 0; ++i) line[pos++] = mid[i];
if (label != NULL) {
for (i = 0; label[i] != 0 && pos < (sizeof(line) / sizeof(line[0])) - 3; ++i) {
line[pos++] = label[i];
}
}
for (i = 0; nl[i] != 0; ++i) line[pos++] = nl[i];
line[pos] = 0;
if (SystemTable != NULL && SystemTable->ConOut != NULL) {
SystemTable->ConOut->OutputString(SystemTable->ConOut, line);
}
if (SystemTable != NULL && SystemTable->BootServices != NULL &&
SystemTable->BootServices->Stall != NULL) {
SystemTable->BootServices->Stall(DEBUG_CHECKPOINT_STALL_US);
}
}
#else
#define debug_checkpoint(SystemTable, id, label) ((void)0)
#endif /* UEFI_DEBUG_CHECKPOINTS */
/**
* @brief Compare two EFI GUIDs for equality.
*
* Compares all 128 bits of the two GUIDs field by field:
* @c Data1 (32-bit), @c Data2 (16-bit), @c Data3 (16-bit), and all
* 8 bytes of @c Data4. Returns non-zero only if every field matches.
*
* Used by @c efi_main() to search the UEFI @c ConfigurationTable for
* the ACPI 2.0 and ACPI 1.0 table GUIDs (@c EFI_ACPI_20_TABLE_GUID and
* @c EFI_ACPI_TABLE_GUID) to populate @c g_boot_info.acpi_table before
* calling @c ExitBootServices().
*
* @param a Pointer to the first EFI GUID.
* @param b Pointer to the second EFI GUID.
* @return Non-zero if the GUIDs are equal, 0 otherwise.
*/
static int guid_equals(const EFI_GUID* a, const EFI_GUID* b)
{
return a->Data1 == b->Data1 &&
a->Data2 == b->Data2 &&
a->Data3 == b->Data3 &&
a->Data4[0] == b->Data4[0] &&
a->Data4[1] == b->Data4[1] &&
a->Data4[2] == b->Data4[2] &&
a->Data4[3] == b->Data4[3] &&
a->Data4[4] == b->Data4[4] &&
a->Data4[5] == b->Data4[5] &&
a->Data4[6] == b->Data4[6] &&
a->Data4[7] == b->Data4[7];
}
/**
* @brief Read @c /starforth.cfg from the EFI System Partition.
*
* Opens the ESP root directory using @c EFI_SIMPLE_FILE_SYSTEM_PROTOCOL
* via the device handle of the loaded image, then reads the file
* @c "starforth.cfg" into @p buf. On success, @p buf contains a
* NUL-terminated ASCII command-line string (at most @p buflen - 1 bytes)
* with trailing CR and LF characters stripped.
*
* This is the second-priority command-line source in @c efi_main() (after
* the NVRAM one-shot variable @c StarForthBootArgs). It is intended for
* installer-supplied or build-time-configured boot options that persist
* across reboots, unlike the ephemeral NVRAM variable.
*
* Both @c MONOLITHIC_BUILD and split-build paths call this function;
* it is not gated by a preprocessor condition.
*
* @param ImageHandle UEFI image handle for the current application;
* used to retrieve @c EFI_LOADED_IMAGE_PROTOCOL.
* @param BS UEFI Boot Services table pointer; must not be NULL.
* @param buf Caller-supplied buffer; receives the NUL-terminated
* ASCII config string on success.
* @param buflen Size of @p buf in bytes; at most @p buflen-1 bytes
* of the file are read.
* @return 1 if the file was read and contained non-empty content;
* 0 on any UEFI error or if the file is empty after stripping.
*/
static int load_cfg_from_esp(EFI_HANDLE ImageHandle, EFI_BOOT_SERVICES *BS,
char *buf, UINTN buflen)
{
EFI_LOADED_IMAGE_PROTOCOL *loaded_image = NULL;
EFI_SIMPLE_FILE_SYSTEM_PROTOCOL *fs = NULL;
EFI_FILE_PROTOCOL *root = NULL;
EFI_FILE_PROTOCOL *cfg_file = NULL;
EFI_STATUS status;
UINTN read_size;
buf[0] = '\0';
status = BS->HandleProtocol(ImageHandle,
(EFI_GUID *)&EFI_LOADED_IMAGE_PROTOCOL_GUID,
(void **)&loaded_image);
if (status != EFI_SUCCESS || !loaded_image) return 0;
status = BS->HandleProtocol(loaded_image->DeviceHandle,
(EFI_GUID *)&EFI_SIMPLE_FILE_SYSTEM_PROTOCOL_GUID,
(void **)&fs);
if (status != EFI_SUCCESS || !fs) return 0;
status = fs->OpenVolume(fs, &root);
if (status != EFI_SUCCESS || !root) return 0;
status = root->Open(root, &cfg_file, L"starforth.cfg", EFI_FILE_MODE_READ, 0);
if (status != EFI_SUCCESS) {
root->Close(root);
return 0;
}
read_size = buflen - 1;
status = cfg_file->Read(cfg_file, &read_size, buf);
cfg_file->Close(cfg_file);
root->Close(root);
if (status != EFI_SUCCESS) {
buf[0] = '\0';
return 0;
}
buf[read_size] = '\0';
/* Strip trailing CR/LF */
while (read_size > 0 &&
(buf[read_size - 1] == '\n' || buf[read_size - 1] == '\r'))
buf[--read_size] = '\0';
return read_size > 0;
}
#ifndef MONOLITHIC_BUILD
/* Kernel entry point signature */
typedef void (*KernelEntry)(BootInfo *boot_info);
/* Kernel ELF buffer - dynamically allocated via UEFI Boot Services */
#define KERNEL_MAX_SIZE (8 * 1024 * 1024)
static uint8_t *kernel_elf_buffer = NULL;
static uint64_t kernel_elf_size = 0;
/**
* @brief Load @c kernel.elf from the ESP into UEFI-allocated memory (split build).
*
* Used only in the split-build path (@c !MONOLITHIC_BUILD) where the kernel
* is a separate ELF file on the EFI System Partition rather than being
* linked directly into the loader image. Execution order:
*
* 1. Allocates @c KERNEL_MAX_SIZE (8 MB) of @c EfiLoaderData pages via
* @c AllocatePages. Stores the result in the module-static
* @c kernel_elf_buffer pointer.
* 2. Retrieves @c EFI_LOADED_IMAGE_PROTOCOL from @p ImageHandle to obtain
* the boot device handle.
* 3. Opens @c EFI_SIMPLE_FILE_SYSTEM_PROTOCOL on the device handle and
* opens the ESP volume root directory.
* 4. Opens @c "kernel.elf" for read-only access.
* 5. Queries file size via @c EFI_FILE_INFO; returns
* @c EFI_BUFFER_TOO_SMALL if the file exceeds @c KERNEL_MAX_SIZE.
* 6. Reads the entire file into @c kernel_elf_buffer and stores the byte
* count in @c kernel_elf_size.
*
* Must be called before @c ExitBootServices() — file system access via
* Boot Services is unavailable after EBS. The allocated buffer and size
* are then passed to @c elf_load_kernel() in Phase C.
*
* Only compiled when @c MONOLITHIC_BUILD is not defined.
*
* @param ImageHandle UEFI image handle for the current application.
* @param BS UEFI Boot Services table pointer.
* @return @c EFI_SUCCESS on success, or a UEFI status code on failure.
*/
static EFI_STATUS load_kernel_from_esp(EFI_HANDLE ImageHandle, EFI_BOOT_SERVICES *BS)
{
EFI_STATUS status;
EFI_LOADED_IMAGE_PROTOCOL *loaded_image = NULL;
EFI_SIMPLE_FILE_SYSTEM_PROTOCOL *fs = NULL;
EFI_FILE_PROTOCOL *root = NULL;
EFI_FILE_PROTOCOL *kernel_file = NULL;
RAW_LOG("Loading kernel.elf from ESP...\n");
/* Allocate buffer for kernel ELF using UEFI Boot Services */
UINTN pages_needed = (KERNEL_MAX_SIZE + 4095) / 4096;
EFI_PHYSICAL_ADDRESS buffer_addr = 0;
status = BS->AllocatePages(AllocateAnyPages, EfiLoaderData, pages_needed, &buffer_addr);
if (status != EFI_SUCCESS) {
RAW_LOG("Failed to allocate kernel buffer\n");
return status;
}
kernel_elf_buffer = (uint8_t *)(uintptr_t)buffer_addr;
RAW_LOG("Kernel buffer allocated\n");
/* Get loaded image protocol */
status = BS->HandleProtocol(ImageHandle, (EFI_GUID *)&EFI_LOADED_IMAGE_PROTOCOL_GUID,
(void **)&loaded_image);
if (status != EFI_SUCCESS) {
RAW_LOG("Failed to get LoadedImageProtocol\n");
return status;
}
/* Get file system protocol from device handle */
status = BS->HandleProtocol(loaded_image->DeviceHandle,
(EFI_GUID *)&EFI_SIMPLE_FILE_SYSTEM_PROTOCOL_GUID,
(void **)&fs);
if (status != EFI_SUCCESS) {
RAW_LOG("Failed to get FileSystemProtocol\n");
return status;
}
/* Open volume (root directory) */
status = fs->OpenVolume(fs, &root);
if (status != EFI_SUCCESS) {
RAW_LOG("Failed to open volume\n");
return status;
}
/* Open kernel.elf file */
status = root->Open(root, &kernel_file, L"kernel.elf",
EFI_FILE_MODE_READ, 0);
if (status != EFI_SUCCESS) {
RAW_LOG("Failed to open kernel.elf\n");
root->Close(root);
return status;
}
/* Get file size */
EFI_FILE_INFO file_info_buffer[128]; /* Should be enough for file info */
UINTN file_info_size = sizeof(file_info_buffer);
status = kernel_file->GetInfo(kernel_file, (EFI_GUID *)&EFI_FILE_INFO_GUID,
&file_info_size, file_info_buffer);
if (status != EFI_SUCCESS) {
RAW_LOG("Failed to get file info\n");
kernel_file->Close(kernel_file);
root->Close(root);
return status;
}
EFI_FILE_INFO *file_info = (EFI_FILE_INFO *)file_info_buffer;
kernel_elf_size = file_info->FileSize;
/* Check if kernel fits in buffer */
if (kernel_elf_size > KERNEL_MAX_SIZE) {
RAW_LOG("Kernel too large for buffer\n");
kernel_file->Close(kernel_file);
root->Close(root);
return EFI_BUFFER_TOO_SMALL;
}
/* Read kernel into buffer */
UINTN read_size = kernel_elf_size;
status = kernel_file->Read(kernel_file, &read_size, kernel_elf_buffer);
if (status != EFI_SUCCESS || read_size != kernel_elf_size) {
RAW_LOG("Failed to read kernel.elf\n");
kernel_file->Close(kernel_file);
root->Close(root);
return status != EFI_SUCCESS ? status : EFI_ABORTED;
}
RAW_LOG("Kernel loaded successfully\n");
/* Close files */
kernel_file->Close(kernel_file);
root->Close(root);
return EFI_SUCCESS;
}
#endif /* !MONOLITHIC_BUILD */
/**
* @brief UEFI application entry point — boot loader for StarKernel.
*
* @c efi_main() is the UEFI PE32+ entry point called by the firmware
* immediately after the image is loaded and validated. It drives the full
* boot sequence in two phases:
*
* **Phase A — UEFI Boot Services are active:**
*
* A-0. Resets the UEFI ConOut console and emits an identification banner.
* On amd64, initialises COM1 via @c raw_serial_init() for raw-serial
* boot logging (@c RAW_LOG() macro).
*
* A-1. Assembles the ASCII kernel command line from three sources in
* priority order:
* 1. @c StarForthBootArgs NVRAM variable (one-shot, set by REBOOT word;
* immediately cleared after reading to prevent re-use on next boot).
* 2. @c /starforth.cfg on the ESP (installer / build-time config).
* 3. @c LoadOptions from the UEFI boot manager (typically the EFI shell
* command line or boot entry options).
* Parses the result via @c cmdline_parse_ascii() into
* @c g_boot_info.args.
*
* A-2. If @c --stack=@<size@> was given, allocates @p stack_size bytes of
* @c EfiLoaderData pages for the kernel stack and stores the base
* address and size in @c g_boot_info.
*
* A-3. Pre-fills non-EBS-dependent @c BootInfo fields: runtime services
* pointer, ACPI table (found in @c ConfigurationTable by GUID),
* and GOP framebuffer geometry.
*
* A-4. (Split build only) Loads @c kernel.elf from the ESP via
* @c load_kernel_from_esp() into the pre-allocated 8 MB buffer.
*
* **Phase B — ExitBootServices:**
*
* Calls @c GetMemoryMap() to obtain the current map key, then immediately
* calls @c ExitBootServices(). Retries up to 16 times on
* @c EFI_INVALID_PARAMETER (map key stale) as required by the UEFI spec.
* No UEFI Boot Services calls are made between @c GetMemoryMap and
* @c ExitBootServices — this is the UEFI "golden path" requirement.
*
* **Phase C — Post-ExitBootServices:**
*
* - **Monolithic build**: calls @c kernel_main(@c &g_boot_info) directly
* (kernel is linked into the same PE image).
* - **Split build**: parses and loads the kernel ELF via
* @c elf_load_kernel(), then performs an indirect call to the resolved
* @c kernel_main() entry point address.
*
* If the kernel returns (which it must not), @c efi_main() halts the
* processor in an infinite @c arch_halt() loop.
*
* @param ImageHandle UEFI handle for this loaded application image.
* @param SystemTable Pointer to the UEFI system table (Boot/Runtime
* Services, ConOut, ConfigurationTable, etc.).
* @return @c EFI_SUCCESS if the sequence completes without a fatal error
* before @c ExitBootServices (in practice, control never returns
* here because the kernel takes over permanently).
*/
EFI_STATUS EFIAPI efi_main(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable)
{
EFI_STATUS status;
EFI_BOOT_SERVICES *BS = SystemTable->BootServices;
/* Static map buffer (256 KiB) to avoid allocations in Phase B */
static EFI_MEMORY_DESCRIPTOR memory_map_static[256 * 1024 / sizeof(EFI_MEMORY_DESCRIPTOR)];
EFI_MEMORY_DESCRIPTOR *memory_map = memory_map_static;
UINTN memory_map_capacity = sizeof(memory_map_static);
UINTN map_key = 0;
UINTN descriptor_size = 0;
UINT32 descriptor_version = 0;
int exited_boot_services = 0;
/* Phase A: safe to use UEFI console and file services */
SystemTable->ConOut->Reset(SystemTable->ConOut, FALSE);
SystemTable->ConOut->OutputString(SystemTable->ConOut, L"StarKernel UEFI Loader\r\n");
SystemTable->ConOut->OutputString(SystemTable->ConOut, L"Loading kernel from ESP...\r\n");
debug_checkpoint(SystemTable, 1, L"Entered efi_main - ConOut live");
#if defined(ARCH_AMD64)
raw_serial_init();
RAW_LOG("RAW SERIAL UP\n");
debug_checkpoint(SystemTable, 2, L"Serial (COM1) initialized");
#endif
#ifndef MONOLITHIC_BUILD
/* Load kernel.elf from ESP BEFORE ExitBootServices */
status = load_kernel_from_esp(ImageHandle, BS);
if (status != EFI_SUCCESS) {
SystemTable->ConOut->OutputString(SystemTable->ConOut, L"FATAL: Failed to load kernel.elf\r\n");
RAW_LOG("FATAL: Failed to load kernel.elf\n");
while (1) arch_halt();
return status;
}
debug_checkpoint(SystemTable, 3, L"kernel.elf loaded from ESP - OK");
#else
RAW_LOG("Monolithic build - kernel linked directly\n");
#endif
SystemTable->ConOut->OutputString(SystemTable->ConOut, L"Collecting boot information...\r\n");
/* ------------------------------------------------------------------ */
/* Phase A-1: Parse kernel command line */
/* */
/* Priority (highest first): */
/* 1. StarForthBootArgs NVRAM variable (one-shot, set by REBOOT) */
/* 2. EFI_LOADED_IMAGE_PROTOCOL->LoadOptions (firmware/boot mgr) */
/* 3. Empty string → all KernelArgs defaults */
/* ------------------------------------------------------------------ */
{
char cmdline_buf[KERNEL_ARGS_CMDLINE_MAX];
int used_nvram = 0;
int used_cfg = 0;
cmdline_buf[0] = '\0';
/* Priority 1: one-shot NVRAM variable (set by REBOOT word) */
{
EFI_GUID vendor_guid = STARFORTH_VENDOR_GUID;
EFI_GET_VARIABLE GetVariable =
(EFI_GET_VARIABLE)SystemTable->RuntimeServices->GetVariable;
UINTN data_size = (UINTN)(KERNEL_ARGS_CMDLINE_MAX - 1);
UINT32 attrs = 0;
EFI_STATUS vs = GetVariable(
(CHAR16 *)SF_VAR_BOOT_ARGS,
&vendor_guid,
&attrs,
&data_size,
cmdline_buf);
if (vs == EFI_SUCCESS && data_size > 0) {
cmdline_buf[data_size] = '\0';
used_nvram = 1;
RAW_LOG("CmdLine: from NVRAM StarForthBootArgs\n");
/* One-shot: clear the variable immediately */
EFI_SET_VARIABLE SetVariable =
(EFI_SET_VARIABLE)SystemTable->RuntimeServices->SetVariable;
SetVariable(
(CHAR16 *)SF_VAR_BOOT_ARGS,
&vendor_guid,
EFI_VARIABLE_NON_VOLATILE |
EFI_VARIABLE_BOOTSERVICE_ACCESS |
EFI_VARIABLE_RUNTIME_ACCESS,
0, NULL);
}
}
/* Priority 2: /starforth.cfg on the ESP (build-time / installer config) */
if (!used_nvram) {
if (load_cfg_from_esp(ImageHandle, BS,
cmdline_buf, sizeof(cmdline_buf))) {
used_cfg = 1;
RAW_LOG("CmdLine: from starforth.cfg\n");
}
}
/* Priority 3: LoadOptions from firmware / boot manager */
if (!used_nvram && !used_cfg) {
EFI_LOADED_IMAGE_PROTOCOL *loaded_image = NULL;
EFI_STATUS li_status = BS->HandleProtocol(
ImageHandle,
(EFI_GUID *)&EFI_LOADED_IMAGE_PROTOCOL_GUID,
(void **)&loaded_image);
if (li_status == EFI_SUCCESS && loaded_image &&
loaded_image->LoadOptionsSize > 0 && loaded_image->LoadOptions) {
CHAR16 *opts = (CHAR16 *)loaded_image->LoadOptions;
UINTN n = loaded_image->LoadOptionsSize / sizeof(CHAR16);
UINTN i;
for (i = 0; i + 1 < (UINTN)(KERNEL_ARGS_CMDLINE_MAX) && i < n && opts[i]; i++)
cmdline_buf[i] = (char)(opts[i] & 0xFFu);
cmdline_buf[i] = '\0';
RAW_LOG("CmdLine: from LoadOptions\n");
}
}
cmdline_parse_ascii(cmdline_buf, &g_boot_info.args);
RAW_LOG("CmdLine: parsed OK\n");
}
debug_checkpoint(SystemTable, 4, L"Command line parsed");
/* ------------------------------------------------------------------ */
/* Phase A-2: Allocate dynamic kernel stack if --stack= was given */
/* ------------------------------------------------------------------ */
{
uint64_t stack_sz = g_boot_info.args.stack_size
? g_boot_info.args.stack_size
: (uint64_t)0;
g_boot_info.kernel_stack_base = NULL;
g_boot_info.kernel_stack_size = 0;
if (stack_sz > 0) {
UINTN pages = (UINTN)((stack_sz + 4095u) / 4096u);
EFI_PHYSICAL_ADDRESS stack_addr = 0;
EFI_STATUS sa = BS->AllocatePages(
AllocateAnyPages, EfiLoaderData, pages, &stack_addr);
if (sa == EFI_SUCCESS) {
g_boot_info.kernel_stack_base = (void *)(uintptr_t)stack_addr;
g_boot_info.kernel_stack_size = (uint64_t)(pages * 4096u);
RAW_LOG("Stack: dynamic stack allocated\n");
} else {
RAW_LOG("Stack: dynamic alloc failed — using BSS fallback\n");
}
}
}
debug_checkpoint(SystemTable, 5, L"Kernel stack allocation decided");
/* Fill BootInfo fields that do NOT require EBS first */
g_boot_info.runtime_services = SystemTable->RuntimeServices;
g_boot_info.acpi_table = NULL;
g_boot_info.dtb = NULL;
g_boot_info.framebuffer.base = NULL;
g_boot_info.framebuffer.size = 0;
g_boot_info.framebuffer.width = 0;
g_boot_info.framebuffer.height = 0;
g_boot_info.framebuffer.pixels_per_scanline = 0;
g_boot_info.framebuffer.pixel_format = (UINT32)PixelBltOnly;
g_boot_info.uefi_boot_services_exited = 0;
/* Locate ACPI table (safe pre-EBS). Prefer the ACPI 2.0+ RSDP (has an
* XSDT) over the legacy ACPI 1.0 one (RSDT only, revision 0) -- a
* single OR'd loop that takes whichever GUID appears first in the
* firmware's configuration table can silently hand back the 1.0
* pointer even when a 2.0 one is also present, which then fails any
* XSDT-based table lookup (e.g. FADT PM_TMR_BLK discovery in
* timer_init()). Two explicit passes: 2.0 first, 1.0 only as fallback. */
{
EFI_CONFIGURATION_TABLE *config_tables =
(EFI_CONFIGURATION_TABLE *)SystemTable->ConfigurationTable;
void *acpi10_table = NULL;
for (UINTN i = 0; i < SystemTable->NumberOfTableEntries; ++i) {
if (guid_equals(&config_tables[i].VendorGuid, &EFI_ACPI_20_TABLE_GUID)) {
g_boot_info.acpi_table = config_tables[i].VendorTable;
break;
}
if (!acpi10_table && guid_equals(&config_tables[i].VendorGuid, &EFI_ACPI_TABLE_GUID)) {
acpi10_table = config_tables[i].VendorTable;
}
}
if (!g_boot_info.acpi_table) {
g_boot_info.acpi_table = acpi10_table;
}
/* Devicetree blob, separate pass because the ACPI loop above breaks
* early on a 2.0 hit. Absent on ACPI-only platforms; consumers must
* handle NULL. riscv64 needs it for timebase-frequency (item 0.3) and
* aarch64 for the GIC bases and timer PPI (item 0.6). */
for (UINTN i = 0; i < SystemTable->NumberOfTableEntries; ++i)
{
if (guid_equals(&config_tables[i].VendorGuid, &EFI_DTB_TABLE_GUID))
{
g_boot_info.dtb = config_tables[i].VendorTable;
break;
}
}
}
debug_checkpoint(SystemTable, 6, L"Boot info collected (ACPI table located)");
/* Locate GOP and populate framebuffer info (safe pre-EBS) */
{
EFI_GUID gop_guid = EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID;
EFI_GRAPHICS_OUTPUT_PROTOCOL *gop = NULL;
EFI_STATUS gop_status = BS->LocateProtocol(&gop_guid, NULL, (void **)&gop);
if (gop_status == EFI_SUCCESS && gop && gop->Mode && gop->Mode->Info) {
EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE *mode = gop->Mode;
if (mode->Info->PixelFormat != PixelBltOnly) {
g_boot_info.framebuffer.base = (void *)(UINTN)mode->FrameBufferBase;
g_boot_info.framebuffer.size = (UINTN)mode->FrameBufferSize;
g_boot_info.framebuffer.width = mode->Info->HorizontalResolution;
g_boot_info.framebuffer.height = mode->Info->VerticalResolution;
g_boot_info.framebuffer.pixels_per_scanline = mode->Info->PixelsPerScanLine;
g_boot_info.framebuffer.pixel_format = (UINT32)mode->Info->PixelFormat;
RAW_LOG("GOP: linear framebuffer found\n");
debug_checkpoint(SystemTable, 7, L"GOP: linear framebuffer found");
} else {
RAW_LOG("GOP: PixelBltOnly - no linear framebuffer\n");
debug_checkpoint(SystemTable, 7, L"GOP: BltOnly, no linear fb");
}
} else {
RAW_LOG("GOP: protocol not found\n");
debug_checkpoint(SystemTable, 7, L"GOP: protocol not found");
}
}
/*
* Phase B: ExitBootServices loop
* RULE: Between the final GetMemoryMap and ExitBootServices => DO NOTHING.
* No ConOut, no extra services, no protocol opens, no "verification" GetMemoryMap.
*
* (Checkpoint 8 fires immediately below, before this loop starts, rather
* than between GetMemoryMap and ExitBootServices, specifically to respect
* this rule -- see the debug_checkpoint doc comment near the top of this
* file.)
*/
debug_checkpoint(SystemTable, 8, L"About to enter ExitBootServices retry loop");
for (int attempt = 0; attempt < 16; ++attempt) {
UINTN map_sz = memory_map_capacity;
status = BS->GetMemoryMap(
&map_sz,
memory_map,
&map_key,
&descriptor_size,
&descriptor_version
);
if (status == EFI_BUFFER_TOO_SMALL) {
RAW_LOG("GetMemoryMap: BUFFER_TOO_SMALL\r\n");
return status;
}
if (status != EFI_SUCCESS) {
RAW_LOG("GetMemoryMap: ERROR\r\n");
return status;
}
/* Stash map into BootInfo (still pre-EBS) */
g_boot_info.memory_map = memory_map;
g_boot_info.memory_map_size = map_sz;
g_boot_info.memory_map_descriptor_size = descriptor_size;
g_boot_info.runtime_services = SystemTable->RuntimeServices;
#if defined(ARCH_AMD64)
RAW_LOG("EBS...\r\n");
#endif
/* Call ExitBootServices immediately after GetMemoryMap */
status = BS->ExitBootServices(ImageHandle, map_key);
if (status == EFI_SUCCESS) {
exited_boot_services = 1;
#if defined(ARCH_AMD64)
RAW_LOG("EBS OK\r\n");
#endif
break;
}
if (status != EFI_INVALID_PARAMETER) {
#if defined(ARCH_AMD64)
RAW_LOG("EBS fail non-invalid\r\n");
#endif
return status;
}
#if defined(ARCH_AMD64)
RAW_LOG("EBS invalid -> retry\r\n");
#endif
/* loop will retry */
}
g_boot_info.uefi_boot_services_exited = exited_boot_services ? 1u : 0u;
#ifdef MONOLITHIC_BUILD
/*
* Monolithic build: kernel is linked directly, call kernel_main
*/
RAW_LOG("Calling kernel_main (monolithic)...\n");
kernel_main(&g_boot_info);
#else
/*
* Phase C: Post-ExitBootServices - Parse and load kernel
* BootServices are GONE, can only use runtime services
*/
RAW_LOG("Parsing kernel ELF...\n");
Elf64_Addr entry_point = 0;
if (!elf_load_kernel(kernel_elf_buffer, kernel_elf_size, &entry_point)) {
RAW_LOG("FATAL: Failed to load kernel ELF\n");
while (1) arch_halt();
}
RAW_LOG("Jumping to kernel entry point...\n");
/* Jump to kernel entry point */
KernelEntry kernel_entry = (KernelEntry)entry_point;
kernel_entry(&g_boot_info);
#endif
/* Should never reach here */
RAW_LOG("FATAL: Kernel returned\n");
while (1) {
arch_halt();
}
return EFI_SUCCESS;
}