Files

324 lines
11 KiB
Markdown

# 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=<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:
```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=<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)
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 (<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`:
```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.