Documenting sysv
This commit is contained in:
@@ -36,6 +36,8 @@ Cutting across all of these:
|
|||||||
- **[arch.md](arch.md) — the architecture split.** How CPU-specific code is kept
|
- **[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,
|
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.
|
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
|
- **[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
|
QEMU and asserting on its serial output — reproducibly, and structured so the
|
||||||
same tests run across architectures.
|
same tests run across architectures.
|
||||||
|
|||||||
+2
-1
@@ -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
|
`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
|
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
|
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
|
## The handoff contract
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
Reference in New Issue
Block a user