Pass argv to processes on a SysV entry stack; grow the user stack to 32 KiB

Processes now start with C-compatible arguments: the kernel builds the
System V AMD64 entry block (argc, argv, empty envp, auxiliary vector)
at the top of the stack, argv[0] is the path or initial-ramdisk name
the process was spawned as, and system_spawn carries an optional
NUL-separated blob that becomes argv[1..]. The runtime parses the block
(runtime.argumentCount/argument) and its spawn wrappers pass arguments
through. The name is also recorded on the task, so a fault report says
which binary died, not just its id.

The user stack grows from one page to eight (32 KiB,
parameters.user_stack_pages), with the page below left unmapped as a
guard so an overflow faults into a clean process kill rather than
corrupting the image. Task.name_buffer is zero-initialised, not
undefined: an undefined default is materialised as a 0xAA fill that
moved the static task pool out of .bss and made the whole kernel ~7x
slower under QEMU TCG (caught by the affinity test).

Proven end to end by the new args test: args-echo respawns itself with
arguments via the syscall blob, burns more stack than one page could
hold, and echoes its argv intact. Full suite: 44/44.
This commit is contained in:
Daniel Samson
2026-07-11 08:33:12 +01:00
parent 6b3ae0c997
commit a5fe63c1dd
14 changed files with 385 additions and 50 deletions
+34 -7
View File
@@ -5,25 +5,52 @@
const std = @import("std");
const system = @import("system.zig");
/// The kernel enters at `_start` with rsp 16-aligned, but a SystemV function expects
/// rsp ≡ 8 (mod 16) on entry (as if reached by `call`). The `call` below pushes
/// the 8-byte return address, satisfying the ABI before any Zig frame runs; the
/// `ud2` is a safety net if `rt_start` ever returns.
/// The kernel enters at `_start` with rsp 16-aligned, pointing at the System V
/// process-entry block it built: argc, argv pointers, NULL, envp terminator, the
/// auxiliary vector, then the strings (see system/kernel/process.zig,
/// `buildEntryStack`). Capture that address in rdi — the first SysV argument —
/// before `call` disturbs the stack; the call's pushed return address also puts
/// rsp ≡ 8 (mod 16), satisfying the ABI before any Zig frame runs. The `ud2` is a
/// safety net if `rt_start` ever returns.
pub export fn _start() callconv(.naked) noreturn {
asm volatile (
\\mov %%rsp, %%rdi
\\call rt_start
\\ud2
);
}
/// The first Zig frame. The heap is lazy (first alloc grows it), so there is no
/// runtime init to order here — just hand control to the program's `main`.
export fn rt_start() callconv(.c) noreturn {
// The process-entry block, recorded by `rt_start` for the accessors below.
var argument_count: usize = 0;
var argument_vector: [*]const u64 = undefined;
/// The first Zig frame, entered with `stack` pointing at the kernel-built entry
/// block. Record argc/argv for the accessors, then hand control to the program's
/// `main`. The heap is lazy (first alloc grows it), so there is no other runtime
/// init to order here.
export fn rt_start(stack: [*]const u64) callconv(.c) noreturn {
argument_count = stack[0];
argument_vector = stack + 1;
const root = @import("root"); // the user binary's root source file
root.main();
system.exit(0);
}
/// Number of process arguments (argc). At least 1: argument 0 is the path or
/// name this binary was spawned as.
pub fn argumentCount() usize {
return argument_count;
}
/// Process argument `index` (0 = the program's own path/name), or an empty slice
/// if out of range. The bytes live in the entry block at the top of the stack
/// page, NUL-terminated, valid for the process's lifetime.
pub fn argument(index: usize) []const u8 {
if (index >= argument_count) return "";
const string: [*:0]const u8 = @ptrFromInt(argument_vector[index]);
return std.mem.span(string);
}
/// No runtime to unwind into — report a panic as a nonzero exit code.
pub const panic = std.debug.FullPanic(struct {
fn panic(_: []const u8, _: ?usize) noreturn {