4.4 KiB
SysV: the kernel's calling convention
Several places in danos say "the kernel is SysV" — most visibly src/root.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:
src/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 BootInfo 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 danos.kernel_abi (SysV): the loader's
function-pointer type and the kernel's _start both carry
callconv(danos.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.
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 (arch.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.