Split the system contract into boot-handoff / abi / device-abi

The `system` module (formerly `danos`) had become a grab-bag: it held the
loader<->kernel handoff *and* the kernel<->user ABI *and* the device wire types, in
one module three different audiences imported. Usage proved the seam — the
bootloader never touched the syscall/device ABI, and user space never touched the
boot handoff — so split it by audience, one module per contract:

  system/boot-handoff.zig       loader <-> kernel: BootInformation, Framebuffer,
                                MemoryMap, the VM layout + physicalToVirtual, kernel_abi
  system/abi.zig                kernel <-> user, core: SystemCall, mmap prot flags,
                                page_size, notify_badge_bit, ServiceId
  system/devices/device-abi.zig kernel <-> user, devices: DeviceDescriptor,
                                DeviceClass, ResourceDescriptor, ResourceKind, ...

device-abi is the devices sub-project's public interface, exposed as its own module
the way vfs exposes vfs-protocol — importable by user space, unlike the
kernel-internal device model it also feeds. That collapses a real duplication:
DeviceClass and ResourceKind were defined twice (device-model.zig and the contract,
kept "in sync by hand"); device-model now re-exports them from device-abi, so the
enum a driver matches on and the one the kernel classifies with are one type.

Each import now declares which contract it speaks: the bootloader imports only
boot-handoff; a driver only abi + device-abi (via the runtime); the kernel all
three. This also retires the `system` / `runtime.system` name overlap. page_size
lands in abi (it's part of the mmap contract user space aligns to); the bootloader
keeps its own local 4 KiB constant so it depends on nothing but the handoff.

All 21 importers rewired, docs updated to keep /system mapping to source. Build,
host tests, and the QEMU suite (36/36) all green.
This commit is contained in:
Daniel Samson
2026-07-10 18:08:51 +01:00
parent 47610e8ee2
commit be81394be3
37 changed files with 395 additions and 337 deletions
+6 -2
View File
@@ -146,11 +146,13 @@ A sub-project's extra files are reached through the module, never as separate pa
```
system/ → /system danos's own internals (the self-representation)
system.zig the kernel↔user ABI contract (the `system` module)
boot-handoff.zig the loader↔kernel contract (the `boot-handoff` module)
abi.zig the core kernel↔user ABI (the `abi` module)
parameters.zig initial-ramdisk.zig shared contracts
kernel/ IPC, memory, scheduling, the private syscall dispatch
architecture/x86_64/ the `architecture` module (never named by generic code)
devices/ the device model /system/devices reflects (+ aml/)
device-abi.zig the device wire types (the `device-abi` module)
drivers/ hpet/ bus/ one sub-project per driver → /system/drivers
services/ init/ vfs/ device-manager/ system servers → /system/services (vfs/ holds
vfs.zig, vfs-test.zig, protocol.zig)
@@ -177,7 +179,9 @@ appears in the private-ABI path.
|------|------|
| Boot methods (one per way of booting the kernel) | `boot/` — `efi.zig` (UEFI) → `BOOTX64.efi` |
| Kernel entry, panic, bring-up | `system/kernel/kernel.zig` |
| Shared loader↔kernel contract (`BootInfo`, `Framebuffer`, `MemoryMap`, `Syscall`, ABI) | `system/system.zig` |
| Loader↔kernel handoff (`BootInfo`, `Framebuffer`, `MemoryMap`, VM layout) | `system/boot-handoff.zig` |
| Core kernel↔user ABI (`SystemCall`, mmap prot flags, `page_size`) | `system/abi.zig` |
| Device wire types (`DeviceDescriptor`, `DeviceClass`, …) | `system/devices/device-abi.zig` |
| Physical frame allocator | `system/kernel/pmm.zig` |
| Kernel heap (`std.mem.Allocator`) | `system/kernel/heap.zig` |
| Scheduler (fixed-priority preemptive; blocking, wait queues) | `system/kernel/scheduler.zig` |
+2 -2
View File
@@ -16,7 +16,7 @@ the RSDT's address is a field *inside* the RSDP. The platform follows that point
UEFI configuration table
│ the loader reads the RSDP's physical address
▼
BootInfo.acpi_rsdp (u64, in the shared `system` module) system/system.zig
BootInfo.acpi_rsdp (u64, in the loader↔kernel handoff) system/boot-handoff.zig
│ the kernel forwards the whole BootInfo
▼
platform.discover(boot_info, …) system/devices/platform.zig
@@ -44,7 +44,7 @@ the [memory map](memory-map.md).
The loader can't just call the device module: the bootloader binary and the kernel
binary are compiled separately, and **the loader isn't linked against the `platform`
module at all** (it imports only the shared `system` module). So instead of a call, it
module at all** (it imports only the `boot-handoff` contract). So instead of a call, it
deposits a value in the handoff struct:
```zig
+2 -1
View File
@@ -22,7 +22,8 @@ say.*
## The capability: claim before touch
The five driver syscalls (`system/system.zig`, dispatched in `system/kernel/process.zig`):
The driver syscall numbers (`system/abi.zig`) with the device types they carry
(`system/devices/device-abi.zig`), dispatched in `system/kernel/process.zig`:
| # | Call | Meaning |
|---|------|---------|
+8 -6
View File
@@ -124,7 +124,7 @@ entirely ours.
### 4. Jump to the kernel
```zig
const kernel: *const fn (*const BootInfo) callconv(system.kernel_abi) noreturn =
const kernel: *const fn (*const BootInfo) callconv(boot_handoff.kernel_abi) noreturn =
@ptrFromInt(entry);
kernel(&boot_info);
```
@@ -142,17 +142,19 @@ kernel is freestanding and uses the **SysV AMD64** convention (first argument in
read garbage.
So both sides pin the convention explicitly to SysV via the shared
`system.kernel_abi` (defined in `system/system.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
`system` module: it's a contract both binaries must agree on. See
`boot_handoff.kernel_abi` (defined in `system/boot-handoff.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 `boot-handoff` 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 loader and kernel are two *separate* binaries built for two different targets,
so everything they exchange must have an identically-defined memory layout. That's
what `system/system.zig` provides — imported by both as the `system` module:
what `system/boot-handoff.zig` provides — imported by both as the `boot-handoff` module.
It is *only* the handoff: the kernel↔user ABI (`system/abi.zig`) and the device types
(`system/devices/device-abi.zig`) are separate contracts the bootloader never sees.
- `BootInfo` — the top-level struct passed to the kernel (currently just the
framebuffer; this is where future handoff data like the memory map will go).
+1 -1
View File
@@ -12,7 +12,7 @@ exactly what `Console.pixel` does:
self.rowPtr(y)[x] = color; // system/kernel/console.zig
```
Our `Framebuffer` struct (`system/system.zig`) is the four facts you need to
Our `Framebuffer` struct (`system/boot-handoff.zig`) is the four facts you need to
address it:
| Field | Meaning |
+1 -1
View File
@@ -58,7 +58,7 @@ screen. `console.write` is a no-op when the firmware gave us no framebuffer.
A framebuffer is not guaranteed — a headless server exposes no UEFI Graphics Output
Protocol. That used to be *fatal* (the loader failed the boot). Now the loader hands
over a "no framebuffer" descriptor (`base == 0`) rather than failing, and
`Framebuffer.present()` (in `system/system.zig`) gates every on-screen path. A headless,
`Framebuffer.present()` (in `system/boot-handoff.zig`) gates every on-screen path. A headless,
serial-less machine boots and runs correctly — it just goes quiet.
## Last-resort channels (no text output at all)
+1 -1
View File
@@ -27,7 +27,7 @@ danos's own neutral format, and the kernel only ever sees that.**
## The neutral format
Defined in `system/system.zig`, the shared loader↔kernel contract:
Defined in `system/boot-handoff.zig`, the shared loader↔kernel contract:
```zig
pub const MemoryKind = enum(u32) {
+1 -1
View File
@@ -27,7 +27,7 @@ address to the low load address in its bootstrap tables and jumps in). The entir
alongside a **physmap** — a straight window onto all of physical memory at
`physmap_base + phys`. Wherever the kernel needs to touch a physical address (a
page-table frame, an ACPI table, a device register), it adds that constant:
`system.physToVirt(phys)`. The layout constants live in `system/system.zig`:
`system.physToVirt(phys)`. The layout constants live in `system/boot-handoff.zig`:
| region | virtual base | PML4 slot |
|--------|--------------|-----------|
+1 -1
View File
@@ -1,6 +1,6 @@
# SysV: the kernel's calling convention
Several places in danos say "the kernel is SysV" — most visibly `system/system.zig`:
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 = .{} };
+3 -2
View File
@@ -8,8 +8,9 @@ without a human staring at the screen.
There are two layers:
- **Host unit tests** (`zig build test`) — for pure, platform-independent logic in
the shared `system` module (the handoff layout in `system/system.zig`). These compile
for the host and run natively.
the shared contracts (`system/boot-handoff.zig`, `system/abi.zig`,
`system/devices/device-abi.zig`), which also compile-checks the three-way split
stays self-consistent. These compile for the host and run natively.
- **QEMU integration tests** (`python3 test/qemu_test.py`) — boot the real kernel
and check its behaviour. This is the interesting part.