6.1 KiB
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 R12–R15 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.zigis 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 system.kernel_abi (SysV): the loader's
function-pointer type and the kernel's _start both carry
callconv(system.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/runtime/start.zig) hands the block to rt_start, which builds a
runtime.process.Init from it and passes that to the program's main
(pub fn main(init: runtime.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.zigdisables 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 declaredcallconv(.c), which resolves to SysV on this target. That's why the assembly stub inisr.smoves theCpuStatepointer into RDI beforecalling 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 X0–X7, 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.