# Kernel Command-Line Arguments (KernelArgs) **Branch:** `lithosananke` **Status:** Design complete — awaiting implementation ## Motivation Memory sizing and runtime behavior are currently compile-time constants: ```c #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 ```c /* 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= e.g. 2M 4M 8M */ uint64_t heap_size; /* --heap= e.g. 2G 4G */ /* Runtime behaviour */ int run_doe; /* --doe flag, default off */ int log_level; /* --log-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: ```c 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`) ```asm 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`) ```c void cmdline_parse(const CHAR16 *load_options, BootInfo *boot_info); ``` Steps: 1. Convert UCS-2 → ASCII into a local 512-byte buffer (truncate at 511). 2. Copy verbatim into `boot_info->args.raw`. 3. Fill defaults into `boot_info->args`. 4. Tokenise on spaces; for each token: - `--stack=[KMG]` → `boot_info->kernel_stack_size` + `args.stack_size` - `--heap=[KMG]` → `args.heap_size` - `--doe` → `args.run_doe = 1` - `--log-level=` → `args.log_level` (debug/info/warn/error → 0-3) - anything else → silently ignored (forward-compat) 5. Allocate stack via `BS->AllocatePages(AllocateAnyPages, EfiLoaderData, pages)`. Store result in `boot_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`: ```c 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 ```c /* 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: ```makefile # 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: ```forth 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. ```c 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: 1. Pops `addr` / `len` from the data stack. 2. Validates length (`0 < len < KERNEL_ARGS_CMDLINE_MAX`); throws `-11` on out-of-range. 3. Converts the string to UCS-2. 4. Reads `StarForthRebootTries` from NVRAM (default 0 if absent). 5. If `tries >= REBOOT_MAX_TRIES` (default **3**): - Clears both `StarForthBootArgs` and `StarForthRebootTries` variables. - Prints warning to serial: `REBOOT: max tries reached — dropping to REPL`. - Returns normally (does **not** reset); execution continues at the REPL. 6. Otherwise: writes `StarForthBootArgs` and increments `StarForthRebootTries`, then calls `RuntimeServices->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. `StarForthRebootTries` is 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 () — 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`: ```c #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`: ```c REGISTER_WORD("REBOOT", word_reboot, WORD_NORMAL); ``` --- ## Extension Points Adding a new switch requires: 1. A field in `KernelArgs` with a default macro. 2. One `else if` branch in `cmdline_parse()`. 3. One read site in the kernel (usually `kernel_main.c`). No changes to `BootInfo`, `kernel_entry.S`, or the loader calling code. ## Invariants - If `LoadOptions` is 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.raw` is always null-terminated; max 511 chars. - Unknown arguments never cause a boot failure.