11 KiB
Kernel Command-Line Arguments (KernelArgs)
Branch: lithosananke
Status: Design complete — awaiting implementation
Motivation
Memory sizing and runtime behavior are currently compile-time constants:
#define KERNEL_HEAP_SIZE (2ULL * 1024 * 1024 * 1024) /* kernel_main.c */
.space 0x200000 /* kernel_entry.S */
This means tuning stack depth or heap size for a given machine requires a
full rebuild. The JVM model (-Xmx, -Xss) is the right analogy: bake in
safe defaults, allow runtime overrides via boot-time command line. The same
mechanism also lets the kernel self-start the DoE and control log verbosity
without relying on external socat injection from the Makefile scripts.
UEFI Mechanism
EFI_LOADED_IMAGE_PROTOCOL->LoadOptions is a UCS-2 string passed to the
EFI application by the firmware or boot manager. It is available to the
loader before ExitBootServices() at no extra cost — the loader already
holds loaded_image (see uefi_loader.c:160). We convert it to ASCII
(simple 8-bit truncation; all valid switches are ASCII) and parse it into
a KernelArgs struct that travels to the kernel via BootInfo.
New Files
| File | Purpose |
|---|---|
include/starkernel/kernel_args.h |
KernelArgs struct + default macros |
src/starkernel/boot/cmdline.c |
UCS-2→ASCII conversion + tokeniser + parser |
include/starkernel/cmdline.h |
Parser API (cmdline_parse) |
Changed Files
| File | Change |
|---|---|
include/starkernel/uefi.h |
Add KernelArgs args + void *kernel_stack_base + uint64_t kernel_stack_size to BootInfo |
src/starkernel/boot/uefi_loader.c |
Call cmdline_parse(loaded_image->LoadOptions, ...), allocate dynamic stack, fill BootInfo |
src/starkernel/arch/amd64/kernel_entry.S |
Read boot_info->kernel_stack_base; fall back to BSS stack if zero |
src/starkernel/kernel_main.c |
Use boot_info->args.heap_size; honour run_doe and log_level |
Makefile.starkernel |
Pass --doe via OVMF boot entry args; remove socat EXEC-DOE injection |
KernelArgs Struct
/* include/starkernel/kernel_args.h */
#define KARGS_DEFAULT_STACK_SIZE (2ULL * 1024 * 1024) /* 2 MB */
#define KARGS_DEFAULT_HEAP_SIZE (2ULL * 1024 * 1024 * 1024) /* 2 GB */
#define KARGS_DEFAULT_LOG_LEVEL 1 /* info */
#define KARGS_DEFAULT_RUN_DOE 0
typedef struct {
/* Memory */
uint64_t stack_size; /* --stack=<size> e.g. 2M 4M 8M */
uint64_t heap_size; /* --heap=<size> e.g. 2G 4G */
/* Runtime behaviour */
int run_doe; /* --doe flag, default off */
int log_level; /* --log-level=<level> debug=0 info=1 warn=2 error=3 */
/* Extension: raw unparsed cmdline preserved verbatim (null-terminated) */
char raw[512];
} KernelArgs;
All fields have explicit defaults. Unknown switches are ignored and
appended to raw so future kernels can act on them while old kernels boot
safely.
BootInfo Changes
Two fields are added directly to BootInfo (not nested inside
KernelArgs) so kernel_entry.S can read them at fixed, known offsets
without any C header dependency:
typedef struct {
EFI_MEMORY_DESCRIPTOR *memory_map;
UINTN memory_map_size;
UINTN memory_map_descriptor_size;
EFI_RUNTIME_SERVICES *runtime_services;
void *acpi_table;
FramebufferInfo framebuffer;
UINT8 uefi_boot_services_exited;
/* NEW — kernel stack (loader-allocated; zero = use BSS fallback) */
void *kernel_stack_base; /* low address of allocation */
uint64_t kernel_stack_size;
/* NEW — all other parsed arguments */
KernelArgs args;
} BootInfo;
Keeping kernel_stack_base / kernel_stack_size at the top-level avoids
having to compute a nested struct offset inside assembly.
Assembly Trampoline (kernel_entry.S)
kernel_main:
/* rdi = BootInfo* */
mov rax, [rdi + BOOT_INFO_STACK_BASE_OFFSET]
test rax, rax
jnz .dynamic_stack
lea rax, [rip + g_kernel_stack_top] /* BSS fallback */
jmp .stack_ready
.dynamic_stack:
mov rcx, [rdi + BOOT_INFO_STACK_SIZE_OFFSET]
add rax, rcx /* top = base + size */
.stack_ready:
and rax, -16
sub rax, 8
mov rsp, rax
xor rbp, rbp
jmp kernel_main_impl
BOOT_INFO_STACK_BASE_OFFSET and BOOT_INFO_STACK_SIZE_OFFSET are defined
as integer constants in a small boot_info_offsets.h that is included by
both C (to _Static_assert correctness) and the assembly file.
Parser (cmdline.c)
void cmdline_parse(const CHAR16 *load_options, BootInfo *boot_info);
Steps:
- Convert UCS-2 → ASCII into a local 512-byte buffer (truncate at 511).
- Copy verbatim into
boot_info->args.raw. - Fill defaults into
boot_info->args. - Tokenise on spaces; for each token:
--stack=<N>[KMG]→boot_info->kernel_stack_size+args.stack_size--heap=<N>[KMG]→args.heap_size--doe→args.run_doe = 1--log-level=<s>→args.log_level(debug/info/warn/error → 0-3)- anything else → silently ignored (forward-compat)
- Allocate stack via
BS->AllocatePages(AllocateAnyPages, EfiLoaderData, pages). Store result inboot_info->kernel_stack_base.
Size suffix parser handles K/M/G (case-insensitive); bare numbers are
treated as bytes.
Loader Integration
In uefi_loader.c, after the existing HandleProtocol call for
loaded_image and before ExitBootServices:
cmdline_parse(loaded_image->LoadOptions, &boot_info);
boot_info is already on the loader stack and passed to the kernel entry;
no structural change needed.
kernel_main.c Integration
/* Heap — use runtime size if provided */
uint64_t heap_sz = boot_info->args.heap_size
? boot_info->args.heap_size
: KARGS_DEFAULT_HEAP_SIZE;
kmalloc_init(heap_sz);
/* DoE — self-start if requested */
#ifdef STARFORTH_ENABLE_VM
if (boot_info->args.run_doe) {
vm_interpret(mama, "EXEC-DOE");
}
#endif
Log level feeds into the existing LOG-DEBUG / LOG-INFO etc. word
infrastructure via a new sk_set_log_level(int) call.
Makefile Integration
The qemu target currently injects EXEC-DOE via socat after detecting
ok>. With --doe supported natively:
# Pass --doe via OVMF boot entry OptionalData
QEMU_KERNEL_ARGS ?= --doe --log-level=info --stack=2M --heap=2G
The ISO build embeds these in the UEFI boot entry so the kernel receives
them from firmware. The socat injection dance is removed. The SK_CMD
compile-time flag remains available for one-off testing.
Passing Args in QEMU
OVMF reads boot entry OptionalData from the UEFI variable store. The
cleanest approach for the Makefile: use a small Python/shell script to
write the boot entry with the desired args into OVMF_VARS.fd before
launching QEMU. Alternatively, efibootmgr syntax baked into the ISO's
startup.nsh script works with the OVMF shell.
REBOOT FORTH Word
Purpose
Allows a running kernel to set new boot flags and immediately cold-reset. The canonical use case:
S" --doe --log-level=debug" REBOOT
On the next boot the loader finds the one-shot NVRAM variable, uses it
instead of LoadOptions, and clears it so subsequent boots revert to
normal.
Stack effect
REBOOT ( addr len -- )
Takes a counted string (same convention as S") containing the new
argument line.
Interpret-only guard
REBOOT is a normal (non-immediate) word that checks compilation state at
the top of its body. Attempting to include it in a : definition produces
a -14 throw (interpretation semantics only) at definition time, not
at run time — which is the correct FORTH-standard behaviour.
static void word_reboot(VM *vm) {
if (vm->compiling) {
vm_throw(vm, -14); /* interpretation semantics only */
return;
}
...
}
NVRAM one-shot mechanism
When REBOOT executes it:
- Pops
addr/lenfrom the data stack. - Validates length (
0 < len < KERNEL_ARGS_CMDLINE_MAX); throws-11on out-of-range. - Converts the string to UCS-2.
- Reads
StarForthRebootTriesfrom NVRAM (default 0 if absent). - If
tries >= REBOOT_MAX_TRIES(default 3):- Clears both
StarForthBootArgsandStarForthRebootTriesvariables. - Prints warning to serial:
REBOOT: max tries reached — dropping to REPL. - Returns normally (does not reset); execution continues at the REPL.
- Clears both
- Otherwise: writes
StarForthBootArgsand incrementsStarForthRebootTries, then callsRuntimeServices->ResetSystem(EfiResetCold, EFI_SUCCESS, 0, NULL). This call does not return.
On the next boot, the loader checks for StarForthBootArgs:
- If found: copies value, clears
StarForthBootArgs(one-shot), uses it for parsing.StarForthRebootTriesis left untouched by the loader — it is only cleared by a successful REPL session (see below) or by the max-tries guard above.
On reaching the REPL successfully, kernel_main clears StarForthRebootTries
to zero, resetting the counter for the next REBOOT invocation. This means
a clean interactive boot always resets the escape-hatch state.
SetVariable failure path
If any SetVariable call returns a non-EFI_SUCCESS status:
- Print warning:
REBOOT: NVRAM write failed (<status>) — dropping to REPL. - Do not attempt
ResetSystem. - Return normally; execution continues at the REPL.
No infinite retry loop. One attempt, one warning, one escape to REPL.
Hosted-VM stub
On the Linux-hosted build RuntimeServices is unavailable. The stub logs
the would-be args and sets vm->halted = 1:
#ifndef STARFORTH_KERNEL
hal_console_puts("[REBOOT] hosted stub — would reboot with: ");
hal_console_write((const char *)VM_ADDR(vm, addr), (size_t)len);
hal_console_puts("\n");
vm->halted = 1;
#endif
Registration
Registered alongside BYE / WARM / COLD in system_words.c:
REGISTER_WORD("REBOOT", word_reboot, WORD_NORMAL);
Extension Points
Adding a new switch requires:
- A field in
KernelArgswith a default macro. - One
else ifbranch incmdline_parse(). - One read site in the kernel (usually
kernel_main.c).
No changes to BootInfo, kernel_entry.S, or the loader calling code.
Invariants
- If
LoadOptionsis NULL or empty, all defaults apply — identical to current behaviour. Existing QEMU launch commands continue to work. - The BSS stack (
g_kernel_stack) remains in the binary as a fallback. It can be removed in a future cleanup once dynamic allocation is proven. KernelArgs.rawis always null-terminated; max 511 chars.- Unknown arguments never cause a boot failure.