danos/docs/arch.md

5.2 KiB

Architecture split

danos targets x86_64 today, but is meant to grow onto other systems later — a Raspberry Pi, say, which is AArch64 and has no UEFI. To keep that possible without a rewrite, CPU-specific kernel code lives behind a boundary: the generic kernel never names an architecture, and each architecture plugs in behind it.

The seam is a build-time module named arch

The mechanism is deliberately boring — no vtables, no function-pointer tables, no runtime dispatch. build.zig exposes one architecture's code as a module called arch:

const arch_mod = b.addModule("arch", .{
    .root_source_file = b.path("system/kernel/architecture/x86_64/cpu.zig"),
});

and the generic kernel imports it by that name:

const arch = @import("arch");
// ...
arch.halt();          // never says "x86_64"

Adding a second architecture is then a build-time choice: create system/kernel/arch/aarch64/, and point the arch module at it when the target CPU is AArch64. main.zig and console.zig don't change. That compiler-checked module boundary is the architecture interface — when a new arch is missing a function the generic kernel calls, the build fails and names exactly what's missing.

What's arch-specific vs generic

The split follows a simple test: does it name a CPU instruction, a hardware register, or a memory-management structure? If so, it's arch-specific.

Arch-specific — system/kernel/architecture/x86_64/ Generic — kernel core
cpu.zig: halt() (hlt), later GDT/IDT/paging console.zig — pure pixel math, works anywhere
linker.ld — link layout, load address main.zigkmain orchestration, panic handler
(future) interrupt controller, MMU setup root.zig — the neutral handoff contract

Notice the framebuffer console is generic: it just writes pixels into whatever framebuffer it's handed, so it needs no per-arch version. Most of the kernel should end up on the generic side; the arch module stays small.

Two axes, kept separate

There are really two independent questions, and it's worth not conflating them:

  • CPU architecture (x86_64 vs AArch64): instructions, MMU, interrupts → system/kernel/arch/<cpu>/.
  • Boot protocol (UEFI vs Raspberry Pi firmware + device tree): handled separately, because loaders are their own binaries. boot/efi.zig builds BOOTX64.efi, a distinct executable from the kernel ELF. On a Pi there is no separate loader at all — the firmware jumps straight into the kernel with a device-tree pointer, so that entry work would live in the AArch64 arch code. Either path converges on the same neutral BootInfo.

Current x86_64 contents

  • system/kernel/architecture/x86_64/cpu.zig — the arch module root. Exposes halt() (see halting.md), init() (bring up the descriptor tables), enablePaging(), setFaultHandler, readCr2/readCr3, and the CpuState trap frame.
  • system/kernel/architecture/x86_64/gdt.zig / idt.zig / tss.zig — the GDT, IDT and TSS plus CPU-exception handling (see interrupts.md).
  • system/kernel/architecture/x86_64/paging.zig — the kernel's page tables (see paging.md).
  • system/kernel/architecture/x86_64/apic.zig — the Local APIC and its timer, the source of device interrupts (see device-interrupts.md).
  • system/kernel/architecture/x86_64/serial.zig / io.zig — the COM1 UART (the kernel's machine-readable log channel, see testing.md) and the shared port-I/O + MSR primitives.
  • system/kernel/architecture/x86_64/isr.s — the exception stubs, the lgdt/lidt/ltr load helpers, and the context switch (switch_context / task_trampoline, see scheduling.md) — real assembly, since Zig inline asm can't express them.
  • system/kernel/architecture/x86_64/linker.ld — the kernel link layout (fixed low load address, one PT_LOAD per permission set).

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, 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 the arch modules.

The discipline

The thing that makes this help rather than hurt: only extract what's provably architecture-specific, and let the interface emerge with the second implementation. With a single architecture you're guessing at the seam, and a wrong guess encoded as elaborate abstraction is expensive to undo. So:

  • Move code into arch/ only when it genuinely names CPU-specific machinery.
  • Grow the arch surface one function at a time, as steps need it.
  • Don't pre-design the interrupt or paging interfaces before writing them.

Directory hygiene is cheap and reversible; premature abstraction is neither. When arch #2 lands and something doesn't fit, reshaping a few hundred lines is nothing — unwinding an abstraction empire is not.