danos/docs/os-development/sysv.md

6.1 KiB
Raw Permalink Blame History

SysV: the kernel's calling convention

Several places in danos say "the kernel is SysV" — most visibly system/boot-handoff.zig:

pub const kernel_abi: std.builtin.CallingConvention = .{ .x86_64_sysv = .{} };

SysV is short for the System V AMD64 ABI, the calling convention that Unix-like systems (Linux, the BSDs, macOS) use on x86-64. This page explains what that means and why danos has to pin it explicitly.

What a calling convention is

At the machine level there's no language keeping two functions honest when one calls the other — just registers and a stack. So there has to be a shared agreement on the mechanics:

  • which registers carry the arguments, and in what order,
  • where the return value goes,
  • which registers the callee must preserve versus may freely clobber,
  • the stack alignment required at a call,
  • how larger things (structs, floats, varargs) are passed.

That agreement is the calling convention. Both sides of a call must be compiled to the same one, or they read arguments out of the wrong registers and get garbage. ("ABI" — Application Binary Interface — is the broader term, also covering type sizes and object-file format; here we mean the calling-convention part.)

What SysV specifies (the parts that matter here)

Integer and pointer arguments go in this register sequence:

arg 1 2 3 4 5 6
reg RDI RSI RDX RCX R8 R9

The return value comes back in RAX. RBX, RBP and R12R15 are callee-saved (a function must restore them before returning); the rest are caller-saved. The stack must be 16-byte aligned at a call. And there's a red zone — 128 bytes below RSP that a function may use as scratch without adjusting RSP.

The name is historical: it descends from AT&T's System V Unix, whose ABI documents this lineage comes from. The modern spec is the "System V Application Binary Interface, AMD64 Architecture Processor Supplement."

Why danos pins it: RDI vs RCX

The reason this is called out explicitly is a clash with the other common x86-64 convention, Microsoft x64 — used by Windows and UEFI — where the first argument arrives in RCX, not RDI.

danos's two binaries default to different conventions:

  • boot/efi.zig is built for the UEFI target, so its default C convention is Microsoft x64 (first argument → RCX).
  • The kernel is freestanding, so its convention is SysV (first argument → RDI).

When the loader jumps to the kernel passing the BootInformation pointer, both sides have to agree which register that pointer lands in. Left to their defaults, the loader would place it in RCX while the kernel looked in RDI — and the kernel would read garbage. So both sides reference the same boot_handoff.kernel_abi (SysV): the loader's function-pointer type and the kernel's _start both carry callconv(boot_handoff.kernel_abi), and the pointer reliably arrives in RDI. That is the whole reason kernel_abi lives in the shared contract — see efi.md for the handoff it governs.

The process-entry stack (argc/argv)

The SysV ABI also fixes what a fresh process finds on its stack — and danos follows it, so its own runtime and any future C libc read arguments the same way. At the first user instruction, rsp is 16-byte aligned and points at (addresses growing upward):

rsp →  argc                       u64
       argv[0] … argv[argc-1]     pointers into the strings area below
       NULL                       argv terminator
       NULL                       envp terminator (no environment yet)
       {AT_PAGESZ, page size}     auxiliary vector
       {AT_NULL, 0}               auxiliary-vector terminator
       argv string bytes          NUL-terminated
       ─────────────────────────  stack top (stack_top_virtual)

The kernel builds this block at the top of the process's stack — 8 pages (32 KiB, parameters.user_stack_pages) mapped RW+NX below a fixed top, with the page below them left unmapped as a guard, so a stack overflow faults (killing only that process) instead of silently corrupting the image (buildEntryStack in system/kernel/process.zig); argv[0] is always the path or initial-ramdisk name the process was spawned as, and system_spawn's optional argument blob becomes argv[1..]. The runtime's _start (library/kernel/start.zig) hands the block to rt_start, which builds a process.Init from it and passes that to the program's main (pub fn main(init: process.Init); a parameterless main() is also accepted). A C runtime's crt0 would walk the identical layout unmodified — that's the compatibility being bought. The args test proves the round trip.

Where else it surfaces

  • The red zone → red_zone = false. build.zig disables the red zone for the kernel. Interrupts push their frame onto the current stack; if the interrupted code was using its 128-byte red zone, that push would stomp it. Turning the red zone off is the standard fix for kernel code — a direct consequence of SysV having a red zone. (See interrupts.md.)
  • callconv(.c) == SysV here. The exception/interrupt dispatcher (interruptDispatch) is declared callconv(.c), which resolves to SysV on this target. That's why the assembly stub in isr.s moves the CpuState pointer into RDI before calling it — the same first-argument rule.

So "the kernel is SysV" means: its functions pass arguments in RDI/RSI/RDX/…, return in RAX, preserve the SysV callee-saved registers, and assume a red zone — and every boundary that calls into the kernel (the loader, the interrupt stubs) has to speak that same convention at the point of the call.

A note on other architectures

This is x86-64-specific. An AArch64 port (architecture.md) has its own calling convention (arguments in X0X7, and so on) — a different ABI entirely. kernel_abi would be set per-architecture, but the principle is the same: the loader/entry boundary and the kernel must agree on how arguments are passed.