Rename the shared contract module danos -> system; QEMU logs to /var/log/system

The shared kernel<->user ABI contract (BootInformation, the SystemCall numbers,
DeviceDescriptor, page_size, ...) is now the `system` module at
system/system.zig, following the convention that a directory's root file takes
the directory's name.

One overlap to note: the runtime's syscall wrappers are already `runtime.system`,
so the single file that uses both the contract and those wrappers
(library/runtime/heap.zig) aliases the wrappers locally as `system_calls`. The
two are distinct (top-level `system` vs `runtime.system`); everywhere else the
contract is just `system`.

Also: the QEMU run's serial capture now lands in the FHS log location,
zig-out/var/log/system/serial0-<timestamp>.log — a stand-in for the kernel's own
logging system, which will eventually write there itself.

Suite 35/35 plus host tests green.
This commit is contained in:
Daniel Samson
2026-07-10 14:09:38 +01:00
parent 3d1de37d0e
commit d19a0ae38d
38 changed files with 202 additions and 198 deletions
+2 -2
View File
@@ -146,7 +146,7 @@ A sub-project's extra files are reached through the module, never as separate pa
```
system/ → /system danos's own internals (the self-representation)
danos.zig the kernel↔user ABI contract (the `danos` module)
system.zig the kernel↔user ABI contract (the `system` 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)
@@ -177,7 +177,7 @@ 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/danos.zig` |
| Shared loader↔kernel contract (`BootInfo`, `Framebuffer`, `MemoryMap`, `Syscall`, ABI) | `system/system.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 `danos` module) system/danos.zig
BootInfo.acpi_rsdp (u64, in the shared `system` module) system/system.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 `danos` module). So instead of a call, it
module at all** (it imports only the shared `system` module). So instead of a call, it
deposits a value in the handoff struct:
```zig
+1 -1
View File
@@ -83,7 +83,7 @@ There are really two independent questions, and it's worth not conflating them:
The kernel entry point `_start` currently still lives in the generic `main.zig` as
a thin trampoline into `kmain`. It's arch-adjacent (its calling convention is
x86_64 [SysV](sysv.md), via the shared `danos.kernel_abi`), but it's three lines
x86_64 [SysV](sysv.md), via the shared `system.kernel_abi`), but it's three lines
and mostly generic, so it stays put for now. When AArch64 arrives — where entry means setting
up a stack and reading a device-tree pointer from a register — the entry work will
be substantial and per-arch, and *that* is when we extract an entry interface into
+1 -1
View File
@@ -22,7 +22,7 @@ say.*
## The capability: claim before touch
The five driver syscalls (`system/danos.zig`, dispatched in `system/kernel/process.zig`):
The five driver syscalls (`system/system.zig`, dispatched in `system/kernel/process.zig`):
| # | Call | Meaning |
|---|------|---------|
+7 -7
View File
@@ -86,9 +86,9 @@ All of this *must* happen now, because after exit there's no GOP to ask. (See
- Use the **LoadedImage** protocol to discover which device we booted from, then
**SimpleFileSystem** to open that volume.
- Open the file named `danos`, seek to the end to learn its size, rewind, and read
the whole ELF into a firmware-allocated pool buffer. (`read` may return short, so
we loop.)
- Open the kernel ELF at its FHS path (`system\kernel`), seek to the end to learn its
size, rewind, and read the whole ELF into a firmware-allocated pool buffer. (`read`
may return short, so we loop.)
- Parse the ELF: validate the `\x7fELF` magic and the `x86_64` machine type, then
walk the program headers. For every `PT_LOAD` segment we:
- reserve the exact physical pages it's linked at (`p_paddr`) via
@@ -124,7 +124,7 @@ entirely ours.
### 4. Jump to the kernel
```zig
const kernel: *const fn (*const BootInfo) callconv(danos.kernel_abi) noreturn =
const kernel: *const fn (*const BootInfo) callconv(system.kernel_abi) noreturn =
@ptrFromInt(entry);
kernel(&boot_info);
```
@@ -142,17 +142,17 @@ 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
`danos.kernel_abi` (defined in `system/danos.zig`). The loader's function-pointer type
`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
`danos` module: it's a contract both binaries must agree on. See
`system` 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/danos.zig` provides — imported by both as the `danos` module:
what `system/system.zig` provides — imported by both as the `system` module:
- `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
@@ -8,7 +8,7 @@ natural unit because that's the granularity the CPU's paging hardware maps — a
it is the primitive everything above it stands on: page tables, the kernel heap,
per-process memory all ultimately ask the frame allocator for pages.
It's **generic kernel code**: it operates on the neutral `danos.MemoryRegion`
It's **generic kernel code**: it operates on the neutral `system.MemoryRegion`
array, so there's no UEFI in it and nothing architecture-specific beyond the 4 KiB
page. (Contrast [arch.md](arch.md), which is where CPU-specific code lives.)
+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/danos.zig`) is the four facts you need to
Our `Framebuffer` struct (`system/system.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/danos.zig`) gates every on-screen path. A headless,
`Framebuffer.present()` (in `system/system.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)
+2 -2
View File
@@ -27,7 +27,7 @@ danos's own neutral format, and the kernel only ever sees that.**
## The neutral format
Defined in `system/danos.zig`, the shared loader↔kernel contract:
Defined in `system/system.zig`, the shared loader↔kernel contract:
```zig
pub const MemoryKind = enum(u32) {
@@ -121,7 +121,7 @@ The kernel receives a plain array and reads it with zero UEFI knowledge:
```zig
const mm = boot_info.memory_map;
const regions = @as([*]const danos.MemoryRegion, @ptrFromInt(mm.regions))[0..mm.len];
const regions = @as([*]const system.MemoryRegion, @ptrFromInt(mm.regions))[0..mm.len];
for (regions) |r| {
if (r.kind == .usable) usable_pages += r.pages;
}
+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:
`danos.physToVirt(phys)`. The layout constants live in `system/danos.zig`:
`system.physToVirt(phys)`. The layout constants live in `system/system.zig`:
| region | virtual base | PML4 slot |
|--------|--------------|-----------|
+1 -1
View File
@@ -203,7 +203,7 @@ next lands.
[scheduling.md](scheduling.md#affinity-pinning-a-task-to-a-core)). The `affinity`
test confirms a pinned task never migrates. This is the mechanism the fault-on-AP
test rides on, and the *explicit-affinity* real-time-predictable model.
- **Right-sized footprint** — the per-CPU ceiling (`danos.max_cpus`, one constant
- **Right-sized footprint** — the per-CPU ceiling (`system.max_cpus`, one constant
shared by discovery, the scheduler, and the per-core GDT/TSS) is generous (128), but
the *large* per-core resources — the kernel and IST (double-fault) stacks — are
**heap-allocated at bring-up**, only for cores that actually come online. Only the
+3 -3
View File
@@ -1,6 +1,6 @@
# SysV: the kernel's calling convention
Several places in danos say "the kernel is SysV" — most visibly `system/danos.zig`:
Several places in danos say "the kernel is SysV" — most visibly `system/system.zig`:
```zig
pub const kernel_abi: std.builtin.CallingConvention = .{ .x86_64_sysv = .{} };
@@ -59,9 +59,9 @@ danos's two binaries default to different conventions:
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
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(danos.kernel_abi)`, and the pointer reliably arrives in RDI. That is the
`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.
+1 -1
View File
@@ -8,7 +8,7 @@ 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 `danos` module (the handoff layout in `system/danos.zig`). These compile
the shared `system` module (the handoff layout in `system/system.zig`). 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.