5.0 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("src/arch/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
src/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 — src/arch/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.zig — kmain 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 →
src/arch/<cpu>/. - Boot protocol (UEFI vs Raspberry Pi firmware + device tree): handled
separately, because loaders are their own binaries.
src/efi.zigbuildsBOOTX64.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 neutralBootInfo.
Current x86_64 contents
src/arch/x86_64/cpu.zig— thearchmodule root. Exposeshalt()(see halting.md),init()(bring up the descriptor tables),enablePaging(),setFaultHandler,readCr2/readCr3, and theCpuStatetrap frame.src/arch/x86_64/gdt.zig/idt.zig/tss.zig— the GDT, IDT and TSS plus CPU-exception handling (see interrupts.md).src/arch/x86_64/paging.zig— the kernel's page tables (see paging.md).src/arch/x86_64/apic.zig— the Local APIC and its timer, the source of device interrupts (see device-interrupts.md).src/arch/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.src/arch/x86_64/isr.s— the exception stubs and thelgdt/lidt/ltrload helpers, in real assembly because Zig inline asm can't express them.src/arch/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 danos.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
archsurface 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.