110 lines
6.2 KiB
Markdown
110 lines
6.2 KiB
Markdown
# 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 `architecture`
|
|
|
|
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
|
|
`architecture`:
|
|
|
|
```zig
|
|
const architecture_module = b.addModule("architecture", .{
|
|
.root_source_file = b.path("system/kernel/architecture/x86_64/cpu.zig"),
|
|
});
|
|
```
|
|
|
|
and the generic kernel imports it by that name:
|
|
|
|
```zig
|
|
const architecture = @import("architecture");
|
|
// ...
|
|
architecture.halt(); // never says "x86_64"
|
|
```
|
|
|
|
Adding a second architecture is then a build-time choice: create
|
|
`system/kernel/architecture/aarch64/`, and point the `architecture` module at it when the target CPU is
|
|
AArch64. `kernel.zig` and `console.zig` don't change. **That compiler-checked module
|
|
boundary _is_ the architecture interface** — when a new architecture 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`: CPU state, trap-frame accessors, paging, SMP | `console.zig` — pure pixel math, framebuffer drawing |
|
|
| `gdt.zig`, `idt.zig`, `tss.zig` — descriptor tables | `kernel.zig` — kernel orchestration, scheduler, IPC |
|
|
| `paging.zig` — page-table setup and management | `process.zig` — process lifecycle, address spaces |
|
|
| `apic.zig`, `ioapic.zig` — interrupt controllers | `scheduler.zig` — task scheduling and context switch |
|
|
| `serial.zig`, `io.zig` — UART, I/O primitives | `vfs.zig` — filesystem abstraction |
|
|
| `isr.s`, `smp.zig` — exceptions, AP bring-up, context switch | `irq.zig`, `ipc*.zig` — interrupt dispatch, messaging |
|
|
| `linker.ld` — kernel link layout, load address | |
|
|
|
|
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 architecture 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/architecture/<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 architecture code.
|
|
Either path converges on the same neutral [`BootInformation`](memory-map.md).
|
|
|
|
## Current x86_64 contents
|
|
|
|
- **`system/kernel/architecture/x86_64/cpu.zig`** — the `architecture` module root. Exposes the trap-frame
|
|
`CpuState` and accessors, `init()` (bring up the descriptor tables), `enablePaging()`,
|
|
`enterUser()`/`userExit()` for ring-0 ↔ ring-3 transitions, address-space management, and SMP
|
|
entry points (see [halting.md](halting.md), [interrupts.md](interrupts.md), [paging.md](paging.md),
|
|
[scheduling.md](scheduling.md)).
|
|
- **`system/kernel/architecture/x86_64/gdt.zig`** / **`idt.zig`** / **`tss.zig`** — the GDT, IDT and
|
|
TSS plus CPU-exception handling (see [interrupts.md](interrupts.md)).
|
|
- **`system/kernel/architecture/x86_64/paging.zig`** — the kernel's page tables and address-space
|
|
management (see [paging.md](paging.md)).
|
|
- **`system/kernel/architecture/x86_64/apic.zig`** / **`ioapic.zig`** — the Local APIC, its timer,
|
|
and the I/O APIC for device interrupts (see [device-interrupts.md](../device-driver-development/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](../testing.md)) and the shared port-I/O + MSR primitives.
|
|
- **`system/kernel/architecture/x86_64/smp.zig`** / **`per-cpu.zig`** — application-processor bring-up
|
|
and per-CPU state (GS base, system-call entry point, see [scheduling.md](scheduling.md)).
|
|
- **`system/kernel/architecture/x86_64/isr.s`** — the exception stubs, the `lgdt`/`lidt`/`ltr` load
|
|
helpers, ring-0 ↔ ring-3 transitions, and the context switch — real assembly, since Zig inline asm can't
|
|
express them (see [scheduling.md](scheduling.md)).
|
|
- **`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` lives in the architecture-specific `isr.s` (x86_64 here).
|
|
On x86_64 it sets up the kernel stack in BSS and jumps to `kmain()` in `kernel.zig`.
|
|
This is already per-architecture — an AArch64 port would have its own `isr.s` entry
|
|
that parses the device-tree pointer from a register and jumps to the same `kmain()`.
|
|
The entry interface is minimal and emerges naturally from the [boot-handoff](memory-map.md)
|
|
contract both share.
|
|
|
|
## 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 `architecture/` only when it genuinely names CPU-specific machinery.
|
|
- Grow the `architecture` 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
|
|
architecture #2 lands and something doesn't fit, reshaping a few hundred lines is nothing —
|
|
unwinding an abstraction empire is not.
|