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:
@@ -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.
|
||||
@@ -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) */
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user