From 56219b40500e6d9ea1a7549d1f4903ae17b3dbba Mon Sep 17 00:00:00 2001 From: Daniel Samson Date: Fri, 3 Jul 2026 13:06:27 +0100 Subject: [PATCH] Documenting sysv --- docs/README.md | 2 ++ docs/efi.md | 3 +- docs/sysv.md | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 docs/sysv.md diff --git a/docs/README.md b/docs/README.md index b488e15..e1bb39c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -36,6 +36,8 @@ Cutting across all of these: - **[arch.md](arch.md) — the architecture split.** How CPU-specific code is kept behind a build-time `arch` module so the generic kernel never names x86_64, leaving room for other systems (e.g. an AArch64 Raspberry Pi) later. +- **[sysv.md](sysv.md) — the calling convention.** What "the kernel is SysV" means, + and why the loader→kernel boundary has to pin it (the RDI-vs-RCX handoff). - **[testing.md](testing.md) — testing.** How the kernel is tested by booting it in QEMU and asserting on its serial output — reproducibly, and structured so the same tests run across architectures. diff --git a/docs/efi.md b/docs/efi.md index e3b74b1..4aafdf1 100644 --- a/docs/efi.md +++ b/docs/efi.md @@ -142,7 +142,8 @@ So both sides pin the convention explicitly to SysV via the shared `danos.kernel_abi` (defined in `src/root.zig`). The loader's function-pointer type and the kernel's `_start` both reference it, so the pointer lands in the register the kernel expects. This is the whole reason `kernel_abi` lives in the shared -`danos` module: it's a contract both binaries must agree on. +`danos` module: it's a contract both binaries must agree on. See +[sysv.md](sysv.md) for what "SysV" means and where else it shows up. ## The handoff contract diff --git a/docs/sysv.md b/docs/sysv.md new file mode 100644 index 0000000..5422d94 --- /dev/null +++ b/docs/sysv.md @@ -0,0 +1,90 @@ +# SysV: the kernel's calling convention + +Several places in danos say "the kernel is SysV" — most visibly `src/root.zig`: + +```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.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 `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](efi.md) for +the handoff it governs. + +## 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](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 `call`ing 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](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.