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
+13
View File
@@ -0,0 +1,13 @@
# src/starkernel/boot/
UEFI boot, kernel-image loading, and boot-time command-line handling.
- `uefi_loader.c` — the UEFI application entry point (`BOOTX64.EFI`);
calls `ExitBootServices()` to take ownership of hardware, builds
`BootInfo` (memory map, ACPI, framebuffer), and hands off to
`kernel_main()`.
- `elf_loader.c` — ELF64 kernel image parsing/loading.
- `cmdline.c` — parses the boot-time kernel command line / `starforth.cfg`
contents (see `include/starkernel/cmdline.h`, `kernel_args.h`).
- `reloc_stub.c`, `reloc.S` — position-independent relocation support for
the loaded image.
+224
View File
@@ -0,0 +1,224 @@
/*
* cmdline.c — kernel boot command-line parser
*
* Pure C99 / freestanding. No UEFI types, no libc.
* The caller (uefi_loader.c) converts LoadOptions from UCS-2 to ASCII
* before passing the string here.
*/
#include "starkernel/cmdline.h"
#include "starkernel/kernel_args.h"
/* ---- freestanding string helpers --------------------------------------- */
/**
* @brief Test whether a character is ASCII whitespace.
*
* Returns non-zero for space (@c ' '), horizontal tab (@c '\t'),
* carriage return (@c '\r'), and line feed (@c '\n'). Used by
* @c cmdline_parse_ascii() to skip inter-token whitespace and by
* @c load_cfg_from_esp() to strip trailing CR/LF from the config file.
* Replaces the libc @c isspace() which is not available in a freestanding
* build; this version does not consider locale.
*
* @param c Character to test.
* @return Non-zero if @p c is ASCII whitespace, 0 otherwise.
*/
static int sf_isspace(char c) {
return c == ' ' || c == '\t' || c == '\r' || c == '\n';
}
/**
* @brief Compare at most @p n characters of two strings (freestanding).
*
* Iterates up to @p n bytes, stopping at the first differing character
* or when either string's null terminator is reached. Returns 0 if the
* first @p n characters are equal (or both strings end before @p n),
* positive if @p a is lexicographically greater, negative if @p b is.
* Comparison is on @c unsigned @c char values per C standard convention.
*
* Used exclusively by @c cmdline_parse_ascii() and @c parse_log_level()
* to match fixed-length flag prefixes without a libc dependency.
*
* @param a First string.
* @param b Second string.
* @param n Maximum number of characters to compare.
* @return 0 if equal, positive if @p a > @p b, negative if @p a < @p b.
*/
static int sf_strncmp(const char *a, const char *b, unsigned int n) {
while (n && *a && *a == *b) { a++; b++; n--; }
if (!n) return 0;
return (unsigned char)*a - (unsigned char)*b;
}
/**
* @brief Copy a string into a fixed-size buffer with guaranteed NUL termination.
*
* Copies at most @p n-1 bytes from @p src into @p dst and always writes
* a NUL terminator at @c dst[@p n-1] (if @p n > 0). This is the
* BSD @c strlcpy() semantics without returning the source length.
* If @p n is 0 the function is a no-op (no write, no UB).
*
* Used by @c cmdline_parse_ascii() to copy the raw command-line string
* into @c KernelArgs.raw with a guaranteed NUL, avoiding the truncation
* risk of @c strncpy().
*
* @param dst Destination buffer; receives at most @p n-1 bytes of @p src
* followed by a NUL terminator.
* @param src NUL-terminated source string.
* @param n Size of @p dst in bytes (including the NUL slot).
*/
static void sf_strlcpy(char *dst, const char *src, unsigned int n) {
unsigned int i;
for (i = 0; i + 1 < n && src[i]; i++) dst[i] = src[i];
if (n) dst[i] = '\0';
}
/* ---- Size-suffix parser ------------------------------------------------ */
/**
* @brief Parse a decimal integer with optional K/M/G size suffix.
*
* Reads a non-negative decimal integer from @p s and multiplies it by:
* - 1024 for 'K' or 'k' (kibibytes)
* - 1024² for 'M' or 'm' (mebibytes)
* - 1024³ for 'G' or 'g' (gibibytes)
* - 1 for any other or absent suffix
*
* Returns 0 if @p s is NULL, empty, or does not begin with a digit.
* Overflow is not checked; caller must validate the resulting value
* is sensible for its intended use (stack size, heap size, etc.).
*
* Used by @c cmdline_parse_ascii() to decode the @c --stack=@<size@> and
* @c --heap=@<size@> arguments from the boot command line.
*
* @param s NUL-terminated string beginning with a decimal integer
* and optionally followed by a size suffix character.
* @return Parsed byte count (0 on parse failure or zero input).
*/
uint64_t cmdline_parse_size(const char *s) {
uint64_t val = 0;
if (!s || !(*s >= '0' && *s <= '9')) return 0;
while (*s >= '0' && *s <= '9') {
val = val * 10U + (uint64_t)(*s - '0');
s++;
}
if (*s == 'K' || *s == 'k') val *= 1024ULL;
else if (*s == 'M' || *s == 'm') val *= 1024ULL * 1024ULL;
else if (*s == 'G' || *s == 'g') val *= 1024ULL * 1024ULL * 1024ULL;
return val;
}
/* ---- Log-level parser -------------------------------------------------- */
/**
* @brief Map a log-level name string to a @c KARGS_LOG_* constant.
*
* Performs case-insensitive prefix matching against the known level names:
* - "debug" / "DEBUG" → @c KARGS_LOG_DEBUG
* - "info" / "INFO" → @c KARGS_LOG_INFO
* - "warn" / "WARN" → @c KARGS_LOG_WARN
* - "error" / "ERROR" → @c KARGS_LOG_ERROR
*
* Any unrecognised string (including empty) maps to @c KARGS_LOG_INFO
* as a safe default. Called from @c cmdline_parse_ascii() after
* recognising the @c --log-level= prefix.
*
* @param s String beginning at the first character of the level name
* (i.e., after the @c "=" separator has been skipped by the caller).
* @return One of the @c KARGS_LOG_* constants defined in @c kernel_args.h;
* defaults to @c KARGS_LOG_INFO for unknown input.
*/
static int parse_log_level(const char *s) {
if (sf_strncmp(s, "debug", 5) == 0 || sf_strncmp(s, "DEBUG", 5) == 0)
return KARGS_LOG_DEBUG;
if (sf_strncmp(s, "info", 4) == 0 || sf_strncmp(s, "INFO", 4) == 0)
return KARGS_LOG_INFO;
if (sf_strncmp(s, "warn", 4) == 0 || sf_strncmp(s, "WARN", 4) == 0)
return KARGS_LOG_WARN;
if (sf_strncmp(s, "error", 5) == 0 || sf_strncmp(s, "ERROR", 5) == 0)
return KARGS_LOG_ERROR;
return KARGS_LOG_INFO; /* unknown → info (safe default) */
}
/* ---- Main parser ------------------------------------------------------- */
/**
* @brief Parse a NUL-terminated ASCII command-line string into @c KernelArgs.
*
* Populates @p args from space-separated tokens in @p cmdline. Always
* initialises all fields to their defaults first (so partial or empty
* command lines produce safe values):
* - @c stack_size = 0 (use BSS static stack)
* - @c heap_size = 0 (use built-in kmalloc arena)
* - @c run_doe = @c KARGS_DEFAULT_RUN_DOE
* - @c log_level = @c KARGS_DEFAULT_LOG_LEVEL
* - @c raw[0] = '\0'
*
* Recognised flags:
* - @c --doe — sets @c args->run_doe = 1.
* - @c --log-level=@<level@> — sets @c args->log_level via
* @c parse_log_level().
* - @c --stack=@<size@> — sets @c args->stack_size via
* @c cmdline_parse_size().
* - @c --heap=@<size@> — sets @c args->heap_size via
* @c cmdline_parse_size().
*
* Unknown flags are silently ignored for forward-compatibility with
* flags introduced in future kernel versions. The raw command-line string
* is copied verbatim into @c args->raw (truncated to
* @c KERNEL_ARGS_CMDLINE_MAX - 1) for diagnostic logging.
*
* Called from @c efi_main() (in @c uefi_loader.c) after assembling the
* ASCII command line from the three priority sources: NVRAM variable,
* @c starforth.cfg on the ESP, and EFI @c LoadOptions.
*
* @param cmdline NUL-terminated ASCII command line; may be NULL or empty.
* @param args Output @c KernelArgs structure; must not be NULL.
*/
void cmdline_parse_ascii(const char *cmdline, KernelArgs *args) {
const char *p;
const char *tok;
unsigned int tok_len;
/* Fill defaults first */
args->stack_size = 0;
args->heap_size = 0;
args->run_doe = KARGS_DEFAULT_RUN_DOE;
args->log_level = KARGS_DEFAULT_LOG_LEVEL;
args->raw[0] = '\0';
if (!cmdline || !*cmdline) return;
sf_strlcpy(args->raw, cmdline, KERNEL_ARGS_CMDLINE_MAX);
p = cmdline;
while (*p) {
/* Skip leading whitespace */
while (*p && sf_isspace(*p)) p++;
if (!*p) break;
/* Measure token */
tok = p;
while (*p && !sf_isspace(*p)) p++;
tok_len = (unsigned int)(p - tok);
/* --doe */
if (tok_len == 5 && sf_strncmp(tok, "--doe", 5) == 0) {
args->run_doe = 1;
/* --log-level=<level> (prefix = 12 chars) */
} else if (tok_len > 12 && sf_strncmp(tok, "--log-level=", 12) == 0) {
args->log_level = parse_log_level(tok + 12);
/* --stack=<size> (prefix = 8 chars) */
} else if (tok_len > 8 && sf_strncmp(tok, "--stack=", 8) == 0) {
args->stack_size = cmdline_parse_size(tok + 8);
/* --heap=<size> (prefix = 7 chars) */
} else if (tok_len > 7 && sf_strncmp(tok, "--heap=", 7) == 0) {
args->heap_size = cmdline_parse_size(tok + 7);
}
/* Unknown flags are silently ignored (forward-compatibility) */
}
}
+433
View File
@@ -0,0 +1,433 @@
/*
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.
*/
/**
* elf_loader.c - ELF64 kernel loader for StarKernel
*
* Loads the StarKernel ELF binary from ESP, parses segments,
* applies relocations, and jumps to entry point.
*/
#include "uefi.h"
#include "elf64.h"
#include "arch.h"
/**
* @brief Fill a memory region with a byte value (freestanding memset).
*
* Writes @p n copies of the low byte of @p c to the memory starting at
* @p s. Used by @c elf_load_segments() to zero-fill BSS regions
* (@c p_memsz > @c p_filesz) without a libc dependency. The byte loop
* is correct for all sizes; a future optimisation could use SIMD or
* word-width stores for large BSS regions.
*
* @param s Pointer to the start of the destination region.
* @param c Value to fill (only the low 8 bits are used).
* @param n Number of bytes to write.
* @return @p s (matches standard @c memset signature).
*/
static void *elf_memset(void *s, int c, uint64_t n)
{
unsigned char *p = (unsigned char *)s;
while (n--) *p++ = (unsigned char)c;
return s;
}
/**
* @brief Copy a non-overlapping memory region (freestanding memcpy).
*
* Copies @p n bytes from @p src to @p dest using a byte-at-a-time loop.
* Behaviour is undefined if the source and destination ranges overlap.
* Used by @c elf_load_segments() to copy each PT_LOAD segment's
* @c p_filesz bytes from the ELF image in memory to its load address.
*
* @param dest Destination buffer; must not overlap with @p src.
* @param src Source buffer.
* @param n Number of bytes to copy.
* @return @p dest (matches standard @c memcpy signature).
*/
static void *elf_memcpy(void *dest, const void *src, uint64_t n)
{
unsigned char *d = (unsigned char *)dest;
const unsigned char *s = (const unsigned char *)src;
while (n--) *d++ = *s++;
return dest;
}
/**
* @brief Validate an ELF64 header for the current target architecture.
*
* Checks the following fields in the ELF identification block and header:
* - Magic bytes @c e_ident[0..3]: must be @c 0x7F 'E' 'L' 'F'.
* - Class @c e_ident[EI_CLASS]: must be @c ELFCLASS64 (64-bit).
* - Data encoding @c e_ident[EI_DATA]: must be @c ELFDATA2LSB
* (little-endian, the only byte order used by all three supported ISAs).
* - Version @c e_ident[EI_VERSION]: must be @c EV_CURRENT (1).
* - Type @c e_type: must be @c ET_EXEC (statically linked) or
* @c ET_DYN (position-independent executable).
* - Machine @c e_machine: must match the build target —
* @c EM_X86_64 for @c ARCH_AMD64,
* @c EM_AARCH64 for @c ARCH_AARCH64,
* @c EM_RISCV for @c ARCH_RISCV64.
*
* @param ehdr Pointer to the ELF64 header at the start of the image.
* @return 1 if the header is valid for this architecture, 0 otherwise.
*/
static int elf_validate_header(const Elf64_Ehdr *ehdr)
{
/* Check magic number */
if (ehdr->e_ident[EI_MAG0] != ELFMAG0 ||
ehdr->e_ident[EI_MAG1] != ELFMAG1 ||
ehdr->e_ident[EI_MAG2] != ELFMAG2 ||
ehdr->e_ident[EI_MAG3] != ELFMAG3)
{
return 0; /* Not an ELF file */
}
/* Check class (64-bit) */
if (ehdr->e_ident[EI_CLASS] != ELFCLASS64) {
return 0;
}
/* Check data encoding (little-endian) */
if (ehdr->e_ident[EI_DATA] != ELFDATA2LSB) {
return 0;
}
/* Check version */
if (ehdr->e_ident[EI_VERSION] != EV_CURRENT) {
return 0;
}
/* Check type (ET_EXEC or ET_DYN) */
if (ehdr->e_type != ET_EXEC && ehdr->e_type != ET_DYN) {
return 0;
}
/* Check machine type matches architecture */
#if defined(ARCH_AMD64)
if (ehdr->e_machine != EM_X86_64) {
return 0;
}
#elif defined(ARCH_AARCH64)
if (ehdr->e_machine != EM_AARCH64) {
return 0;
}
#elif defined(ARCH_RISCV64)
if (ehdr->e_machine != EM_RISCV) {
return 0;
}
#else
#error "Unknown architecture"
#endif
return 1;
}
/**
* @brief Map all PT_LOAD segments from an ELF64 image into memory.
*
* Iterates the program header table (@c e_phnum entries starting at
* offset @c e_phoff) and processes every entry with @c p_type == @c PT_LOAD:
*
* 1. Computes the destination address as @p load_base + @c p_vaddr.
* 2. Copies @c p_filesz bytes from @p elf_data + @c p_offset using
* @c elf_memcpy().
* 3. If @c p_memsz > @c p_filesz, zero-fills the remaining
* (@c p_memsz - @c p_filesz) bytes with @c elf_memset() to
* initialise the BSS region within the segment.
*
* The function does not check for address-range overlaps or that
* destination memory is physically available; the UEFI loader is
* responsible for allocating pages before calling @c elf_load_kernel().
*
* @param elf_data Pointer to the start of the ELF64 file in memory.
* @param load_base Base address at which to load the ELF image
* (0 for @c ET_EXEC, 0x400000 for @c ET_DYN).
* @return 1 always (errors are silently ignored at this milestone; a
* future revision should return 0 on a failed memory access).
*/
static int elf_load_segments(const uint8_t *elf_data, Elf64_Addr load_base)
{
const Elf64_Ehdr *ehdr = (const Elf64_Ehdr *)elf_data;
const Elf64_Phdr *phdr = (const Elf64_Phdr *)(elf_data + ehdr->e_phoff);
/* Iterate through program headers */
for (uint16_t i = 0; i < ehdr->e_phnum; i++) {
if (phdr[i].p_type == PT_LOAD) {
/* Calculate load address */
uint8_t *dest = (uint8_t *)(load_base + phdr[i].p_vaddr);
const uint8_t *src = elf_data + phdr[i].p_offset;
/* Copy file contents to memory */
elf_memcpy(dest, src, phdr[i].p_filesz);
/* Zero out BSS (p_memsz > p_filesz) */
if (phdr[i].p_memsz > phdr[i].p_filesz) {
elf_memset(dest + phdr[i].p_filesz, 0,
phdr[i].p_memsz - phdr[i].p_filesz);
}
}
}
return 1;
}
/**
* @brief Apply all RELA relocation sections from an ELF64 image.
*
* Scans the section header table for sections of type @c SHT_RELA. For
* each such section, resolves the associated symbol table (via
* @c sh_link) and applies every relocation entry to the loaded image.
*
* Supported relocation types are ISA-specific and compile-time selected:
*
* - **amd64 (@c ARCH_AMD64)**:
* - @c R_X86_64_NONE — no-op.
* - @c R_X86_64_RELATIVE — absolute address = @c load_base + @c r_addend.
* - @c R_X86_64_64 — @c load_base + symbol value + @c r_addend (64-bit).
* - @c R_X86_64_32 / @c R_X86_64_32S — same, truncated to 32 bits.
* - @c R_X86_64_PC32 / @c R_X86_64_PLT32 — @c symbol + @c r_addend - P
* (PC-relative, 32 bits), where P is the relocation's own runtime
* address; PLT32 has no real PLT indirection in this static, non-PIE
* link, so it resolves the same as PC32.
*
* - **aarch64 (@c ARCH_AARCH64)**:
* - @c R_AARCH64_NONE — no-op.
* - @c R_AARCH64_RELATIVE — @c load_base + @c r_addend.
* - @c R_AARCH64_ABS64 — @c load_base + symbol + @c r_addend.
*
* - **riscv64 (@c ARCH_RISCV64)**:
* - @c R_RISCV_NONE — no-op.
* - @c R_RISCV_RELATIVE — @c load_base + @c r_addend.
* - @c R_RISCV_64 — @c load_base + symbol + @c r_addend.
*
* Returns 0 (failure) if a symbol index in a relocation entry is out
* of bounds, if the symbol table entry size is zero, or if an unknown
* relocation type is encountered. Returns 1 on success.
*
* @param elf_data Pointer to the ELF64 file image.
* @param load_base Load base address applied during @c elf_load_segments().
* @return 1 on success, 0 on any relocation error.
*/
static int elf_apply_relocations(const uint8_t *elf_data, Elf64_Addr load_base)
{
const Elf64_Ehdr *ehdr = (const Elf64_Ehdr *)elf_data;
const Elf64_Shdr *shdr = (const Elf64_Shdr *)(elf_data + ehdr->e_shoff);
for (uint16_t i = 0; i < ehdr->e_shnum; i++) {
const Elf64_Shdr *rela_sh = &shdr[i];
if (rela_sh->sh_type != SHT_RELA) {
continue;
}
if (rela_sh->sh_link >= ehdr->e_shnum) {
return 0;
}
const Elf64_Shdr *symtab_sh = &shdr[rela_sh->sh_link];
if (symtab_sh->sh_entsize == 0) {
return 0;
}
const Elf64_Rela *rela = (const Elf64_Rela *)(elf_data + rela_sh->sh_offset);
uint64_t num_rela = rela_sh->sh_size / sizeof(Elf64_Rela);
const Elf64_Sym *symtab = (const Elf64_Sym *)(elf_data + symtab_sh->sh_offset);
uint64_t sym_count = symtab_sh->sh_size / symtab_sh->sh_entsize;
for (uint64_t j = 0; j < num_rela; j++) {
uint32_t type = ELF64_R_TYPE(rela[j].r_info);
uint32_t sym_index = ELF64_R_SYM(rela[j].r_info);
uint64_t reloc_addr = load_base + rela[j].r_offset;
uint64_t sym_value = 0;
if (sym_index < sym_count) {
sym_value = symtab[sym_index].st_value;
} else if (sym_index != 0) {
return 0;
}
#if defined(ARCH_AMD64)
switch (type) {
case R_X86_64_NONE:
break;
case R_X86_64_RELATIVE: {
uint64_t *target = (uint64_t *)reloc_addr;
*target = load_base + rela[j].r_addend;
break;
}
case R_X86_64_64: {
uint64_t *target = (uint64_t *)reloc_addr;
*target = load_base + sym_value + rela[j].r_addend;
break;
}
case R_X86_64_32:
case R_X86_64_32S: {
uint32_t *target32 = (uint32_t *)reloc_addr;
uint64_t value = load_base + sym_value + rela[j].r_addend;
*target32 = (uint32_t)value;
break;
}
case R_X86_64_PC32:
case R_X86_64_PLT32: {
/* value = S + A - P, where P is the address of the relocation
* itself (reloc_addr already includes load_base). PLT32 is
* treated identically to PC32: this is a static, non-PIE
* link with every symbol resolved in-image, so there is no
* real PLT indirection to apply. */
uint32_t *target32 = (uint32_t *)reloc_addr;
int64_t value = (int64_t)(load_base + sym_value + rela[j].r_addend) -
(int64_t)reloc_addr;
*target32 = (uint32_t)value;
break;
}
default:
return 0;
}
#elif defined(ARCH_AARCH64)
switch (type) {
case R_AARCH64_NONE:
break;
case R_AARCH64_RELATIVE: {
uint64_t *target = (uint64_t *)reloc_addr;
*target = load_base + rela[j].r_addend;
break;
}
case R_AARCH64_ABS64: {
uint64_t *target = (uint64_t *)reloc_addr;
*target = load_base + sym_value + rela[j].r_addend;
break;
}
default:
return 0;
}
#elif defined(ARCH_RISCV64)
switch (type) {
case R_RISCV_NONE:
break;
case R_RISCV_RELATIVE: {
uint64_t *target = (uint64_t *)reloc_addr;
*target = load_base + rela[j].r_addend;
break;
}
case R_RISCV_64: {
uint64_t *target = (uint64_t *)reloc_addr;
*target = load_base + sym_value + rela[j].r_addend;
break;
}
default:
return 0;
}
#endif
}
}
return 1;
}
/**
* @brief Load, relocate, and return the entry point of a StarKernel ELF64 binary.
*
* Top-level ELF loader called by @c efi_main() (in the split-build path)
* after @c ExitBootServices(). Performs three operations in sequence:
*
* 1. **Header validation** via @c elf_validate_header() — checks magic,
* class, data encoding, version, type, and machine-specific architecture.
* Returns 0 immediately on failure.
*
* 2. **Segment loading** via @c elf_load_segments() — maps all @c PT_LOAD
* segments to their target addresses, initialises BSS to zero.
* The @p load_base is:
* - 0 for @c ET_EXEC (absolute-address kernel; typical for StarKernel).
* - 0x400000 (4 MB) for @c ET_DYN (position-independent kernel).
*
* 3. **Relocation processing** via @c elf_apply_relocations() — applies
* @c SHT_RELA sections to patch in the correct runtime addresses.
* Returns 0 on any relocation error.
*
* On success, stores @c load_base + @c e_entry in @c *entry_out — the
* virtual address of the kernel's C entry function @c kernel_main() — and
* returns 1. The caller performs an indirect call to this address.
*
* @c elf_size is currently unused (reserved for future bounds-checking of
* program-header and section-header offsets against the file boundary).
*
* @param elf_data Pointer to the ELF64 file image in UEFI loader memory;
* must remain valid until after the call returns.
* @param elf_size Size of the ELF image in bytes (currently unchecked).
* @param entry_out Output: receives the kernel entry point address on success.
* @return 1 on success, 0 if validation, segment loading, or relocation fails.
*/
int elf_load_kernel(const uint8_t *elf_data, uint64_t elf_size,
Elf64_Addr *entry_out)
{
(void)elf_size; /* Reserved for future bounds checking */
const Elf64_Ehdr *ehdr = (const Elf64_Ehdr *)elf_data;
/* Validate ELF header */
if (!elf_validate_header(ehdr)) {
return 0;
}
/* Determine load base address */
Elf64_Addr load_base = 0;
if (ehdr->e_type == ET_DYN) {
/* Position-independent: use a fixed base for now */
load_base = 0x400000; /* 4MB */
} else if (ehdr->e_type == ET_EXEC) {
/* Absolute addresses: load at 0 (segments have absolute vaddr) */
load_base = 0;
}
/* Load segments */
if (!elf_load_segments(elf_data, load_base)) {
return 0;
}
/* Apply relocations */
if (!elf_apply_relocations(elf_data, load_base)) {
return 0;
}
/* Calculate entry point */
*entry_out = load_base + ehdr->e_entry;
return 1;
}
+30
View File
@@ -0,0 +1,30 @@
/**
* reloc.S - Minimal .reloc section for UEFI PE32+ loader
*
* UEFI firmware requires a .reloc section for base relocations.
* This provides a minimal placeholder that objcopy can recognize
* when converting ELF to PE32+ format.
*/
#if defined(__x86_64__) || defined(ARCH_AMD64)
.section .reloc, "a"
.balign 4
dummy_reloc:
.long 0
#elif defined(__aarch64__) || defined(ARCH_AARCH64)
.section .reloc, "a"
.balign 4
dummy_reloc:
.word 0
#elif defined(__riscv) || defined(ARCH_RISCV64)
.section .reloc, "a"
.balign 4
dummy_reloc:
.word 0
#endif
+98
View File
@@ -0,0 +1,98 @@
/*
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.
*/
/**
* reloc_stub.c - Provide a minimal .reloc section for PE/COFF EFI loader
*
* Some firmware refuses to load images without a Base Relocation Directory.
* This stub supplies an empty relocation block to satisfy the loader.
*/
#include <stdint.h>
/**
* @brief PE/COFF Base Relocation Block header.
*
* Mirrors the @c IMAGE_BASE_RELOCATION layout from the PE specification:
* - @c page_rva — RVA of the 4 KB page that the following entries apply to.
* - @c block_size — total byte size of this block including the header and
* all entries (minimum 8 bytes for the header alone).
* - @c entry — one or more 16-bit relocation entries packed as
* TYPE (4 bits) | OFFSET (12 bits); TYPE=0 / OFFSET=0
* is a no-op padding entry used to 32-bit-align the block.
*
* Marked @c packed to suppress any padding the compiler might insert between
* fields; @c aligned(4) ensures the block sits on a 4-byte boundary as
* required by the PE loader.
*/
struct reloc_block {
uint32_t page_rva;
uint32_t block_size;
uint16_t entry; /* TYPE=0, OFFSET=0 */
} __attribute__((packed, aligned(4)));
/**
* @brief Minimal @c .reloc section stub to satisfy UEFI firmware loaders.
*
* Some UEFI implementations (notably certain hardware vendor firmware and
* strict OVMF builds) refuse to load a PE32+ image that lacks a Base
* Relocation Directory entry in the optional-header Data Directory. Even
* for monolithic position-independent images that require no run-time fix-ups,
* the presence of the @c .reloc section with a well-formed (but empty)
* relocation block is required to pass the firmware's image validation.
*
* This single statically-allocated @c reloc_block satisfies the requirement:
* - @c page_rva = 0 — applies to the first 4 KB page (convention for stubs).
* - @c block_size = sizeof(reloc_block) — minimal well-formed block (8 bytes
* + 2 bytes for the single no-op entry = 10 bytes).
* - @c entry = 0 — TYPE=0 (absolute, no-op) / OFFSET=0; ignored by the loader.
*
* The @c used attribute prevents the compiler/linker from discarding the
* symbol as unreferenced; the @c section(".reloc") attribute places it
* in the named PE section so the linker emits the correct Data Directory.
*/
__attribute__((section(".reloc"), used))
static const struct reloc_block reloc_stub = {
.page_rva = 0,
.block_size = sizeof(struct reloc_block),
.entry = 0
};
+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;
}