# 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`: ```zig 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: ```zig 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//`. - **Boot protocol** (UEFI vs Raspberry Pi firmware + device tree): handled *separately*, because loaders are their own binaries. `src/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`](memory-map.md). ## Current x86_64 contents - **`src/arch/x86_64/cpu.zig`** — the `arch` module root. Exposes `halt()` (see [halting.md](halting.md)), `init()` (bring up the descriptor tables), `enablePaging()`, `setFaultHandler`, `readCr2`/`readCr3`, and the `CpuState` trap frame. - **`src/arch/x86_64/gdt.zig`** / **`idt.zig`** / **`tss.zig`** — the GDT, IDT and TSS plus CPU-exception handling (see [interrupts.md](interrupts.md)). - **`src/arch/x86_64/paging.zig`** — the kernel's page tables (see [paging.md](paging.md)). - **`src/arch/x86_64/apic.zig`** — the Local APIC and its timer, the source of device interrupts (see [device-interrupts.md](device-interrupts.md)). - **`src/arch/x86_64/serial.zig`** / **`io.zig`** — the COM1 UART (the kernel's machine-readable log channel, see [testing.md](testing.md)) and the shared port-I/O + MSR primitives. - **`src/arch/x86_64/isr.s`** — the exception stubs, the `lgdt`/`lidt`/`ltr` load helpers, and the context switch (`switch_context` / `task_trampoline`, see [scheduling.md](scheduling.md)) — real assembly, since 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](sysv.md), 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 `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.