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
+5
View File
@@ -26,5 +26,10 @@ pub const dma = @import("dma.zig");
/// Re-exported so a user binary can `pub const panic = runtime.panic;`.
pub const panic = start.panic;
/// Process arguments (argc/argv, parsed from the kernel-built entry stack):
/// `argument(0)` is the path or name this binary was spawned as.
pub const argumentCount = start.argumentCount;
pub const argument = start.argument;
/// The heap as a `std.mem.Allocator`, for Zig `std` containers in user code.
pub const allocator = heap.allocator;
+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 {
+24 -4
View File
@@ -44,11 +44,31 @@ pub fn exit(code: usize) noreturn {
}
/// Start the binary bundled in the initial-ramdisk under `name` as a new ring-3
/// process, returning true on success. This is how a supervisor (the device manager)
/// launches a driver it matched — danos-native, not POSIX (a spawn/exec family comes
/// with the process work later).
/// process, returning true on success. The child's argv[0] is `name`. This is how
/// a supervisor (the device manager) launches a driver it matched — danos-native,
/// not POSIX (a spawn/exec family comes with the process work later).
pub fn spawn(name: []const u8) bool {
return sc.systemCall2(.system_spawn, @intFromPtr(name.ptr), name.len) == 0;
return sc.systemCall4(.system_spawn, @intFromPtr(name.ptr), name.len, 0, 0) == 0;
}
/// Like `spawn`, but hands the child command-line arguments: they arrive as
/// argv[1..] on its System V entry stack (argv[0] is still `name`). Marshalled to
/// the kernel as one NUL-separated blob; the combined arguments must fit
/// `blob` (the kernel caps the blob at 256 bytes and argc at 8 anyway).
pub fn spawnWithArguments(name: []const u8, arguments: []const []const u8) bool {
var blob: [256]u8 = undefined;
var len: usize = 0;
for (arguments, 0..) |argument, i| {
if (i != 0) {
if (len >= blob.len) return false;
blob[len] = 0;
len += 1;
}
if (len + argument.len > blob.len) return false;
@memcpy(blob[len..][0..argument.len], argument);
len += argument.len;
}
return sc.systemCall4(.system_spawn, @intFromPtr(name.ptr), name.len, @intFromPtr(&blob), len) == 0;
}
/// Grant `len` bytes (rounded up to whole pages) of fresh, zeroed, writable