re-org docs

This commit is contained in:
Daniel Samson
2026-07-23 00:25:34 +01:00
parent 52d6e372fd
commit 757c6f14c3
52 changed files with 156 additions and 156 deletions
+122
View File
@@ -0,0 +1,122 @@
# SysV: the kernel's calling convention
Several places in danos say "the kernel is SysV" — most visibly `system/boot-handoff.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:
- `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](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](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 ([architecture.md](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.