danos/docs/sysv.md

123 lines
6.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 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 `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 `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](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.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 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.