feat: make kernel architecture-agnostic; primitive work on arm port

This commit is contained in:
daniel committed 2026-10-24 21:25:18 +02:00
1 parent dab7d5427b
commit 1bae77ec84
164 files changed
+3664 -1641

No files matched your search

+10 -4
View File
@@ -70,10 +70,16 @@ sudo apt install build-essential nasm git curl xorriso mtools gdisk qemu-system-
### Cross-Architecture Builds
To build for a different architecture, pass `ARCH=`:
The kernel is split into architecture-neutral code and per-architecture
ports under `kernel/src/Arch/` (see [porting.md](porting.md)). x86_64 is the
only complete port. `aarch64` is a skeleton: the whole kernel compiles and
links for it, but paging, exception vectors, the GIC and the timer are not
written yet, so it does not boot. Userspace is x86_64-only.
Non-x86 kernels are built with LLVM (`clang`, plus `ld.lld` for the link):
```bash
make ARCH=aarch64
make ARCH=riscv64
make ARCH=loongarch64
make kernel ARCH=aarch64 # kernel/bin-aarch64/kernel
make kernel ARCH=aarch64 KERNEL_LD=/path/to/ld.lld
./scripts/check-arch-neutral.sh # boundary check + aarch64 compile
```
+117
View File
@@ -0,0 +1,117 @@
# Porting the Kernel to a New Architecture
The kernel is written so that everything outside `kernel/src/Arch/` is
architecture-neutral. A port is one directory, `kernel/src/Arch/<arch>/`,
implementing the interfaces declared in `kernel/src/Arch/*.hpp` (plus a few
interface headers elsewhere, listed below). The build compiles the generic
tree plus `Arch/$(ARCH)/` only.
`scripts/check-arch-neutral.sh` enforces the boundary and should pass before
every commit that touches the kernel:
- no inline assembly, assembly sources or architecture `#if`s outside
`src/Arch/` (the Limine adapter is the one exception: `limine.h` has
per-architecture SMP structures);
- no generic file includes a port's private headers (`<Arch/x86_64/...>`);
- with clang installed, the kernel compiles for `aarch64` and every symbol
the generic code references is defined by the port.
## Layout
```
kernel/src/Arch/
Arch.hpp target selection (MONTAUK_ARCH_*), MONTAUK_ARCH_NAME
Cpu.hpp inline CPU primitives (interrupt state, halt, barriers,
timestamp counter, TLB, page-table root)
Interrupts.hpp IRQ slots, handler registration, legacy line routing,
MSI message composition, IPIs
Smp.hpp Smp::CpuData (generic fields + arch sub-structs),
per-CPU base access, InitBsp / BootAPs
Context.hpp thread context: FPU area, initial kernel stack,
SwitchContext, ActivateThread, EnterUserMode
Abi.hpp user ABI: ELF machine, relocation kinds, TLS variant,
exit stub, initial user stack, EFI calling convention
Syscall.hpp SyscallFrame contract and entry-point installation
Timer.hpp per-CPU tick timer hardware
Platform.hpp boot-sequence hooks, platform device list, reboot
CpuPower.hpp frequency scaling / thermal governor (may be no-ops)
x86_64/ the complete port
aarch64/ skeleton port
```
Other interfaces a port implements:
| Header | What the port provides |
|---|---|
| `Memory/Paging.hpp` | the whole `Memory::VMM::Paging` class: page-table format, kernel and user mappings, address-space creation and teardown |
| `Io/IoPort.hpp` | port I/O (`Io::In8`...), or the absent-device fallback with `HasPortIo = false` |
| `ACPI/AcpiSleep.hpp` | `Initialize` / `IsS3Available` / `Suspend` (S3 via the FACS waking vector is an x86 mechanism) |
| `Common/Panic.hpp` | `Arch::PanicFrame` and `Arch::PrintPanicFrame` |
Interface headers that need inline definitions include the port's header of
the same name at their end (`Arch/Cpu.hpp` -> `Arch/<arch>/Cpu.hpp`); those
are `Cpu.hpp`, `IoPort.hpp`, `PerCpu.hpp`, `Context.hpp`, `Abi.hpp` and
`SyscallFrame.hpp`. Everything else is an ordinary out-of-line function.
## Boot sequence
`kmain` (`Boot/Main.cpp`) owns the order and calls the port at fixed points:
1. `Arch::EarlyInitialize()` - descriptor tables, FPU/SIMD enable. No allocator.
2. Frame allocator and heap come up (generic).
3. `Arch::InitializeTraps()` - exception vectors.
4. `Paging::Init()` switches to the kernel's own page tables, then
`Arch::InitializeMemoryAttributes()` (x86: PAT for write-combining).
5. ACPI tables are parsed (generic). `Arch::InitializeInterrupts(xsdt)` brings
up interrupt controllers, calls `Smp::InitBsp()`, idle and power
management; kmain then calls `Arch::EnableInterrupts()`.
6. PCI, AML, drivers (generic); `Timekeeping::TimerInitialize()` registers
`IRQ_TIMER` and calls `Arch::Timer::InitializeBsp(1)`.
7. `Arch::InitializePlatformDevices()` (x86: PS/2).
8. `Arch::PrepareUserMode()`, `montauk::abi::InitializeSyscalls()` (calls
`Arch::InitializeSyscallEntry()`), scheduler, IPC.
9. `Smp::BootAPs()`, `Arch::LateInitialize()`, first process, idle loop.
## Assumptions the generic kernel makes
- 64-bit, little-endian, 4 KiB pages.
- A higher-half direct map of physical memory (`Memory::HHDM`), as Limine
provides on every architecture.
- User space in the lower half of each address space, kernel in the upper
half; an address space is identified by the physical address of its root
table (on ARM, the TTBR0 table; the kernel half lives in TTBR1).
- Firmware tables via ACPI (`Hal::ACPI`); device-tree-only platforms would
need a discovery path first.
- Interrupts are numbered IRQ slots (`Arch::IRQ_COUNT`); slot numbers for
MSI-capable drivers are fixed in the drivers.
## The aarch64 skeleton
Real: CPU primitives (DAIF, WFI, `CNTVCT_EL0`, TLBI, TTBR0), per-CPU data via
`TPIDR_EL1`, the context switch and EL0 entry (`Context.S`), TLS through
`TPIDR_EL0`, the ELF/TLS/exit-stub ABI (syscall number in x8), the IRQ slot
dispatch table (`ArchIrqDispatch`), panic-frame printing.
Not yet written (each is marked `TODO(aarch64)`):
- `Paging.cpp`: translation tables and MAIR (every method panics).
- Exception vectors (`VBAR_EL1`): synchronous faults, `svc`, IRQs.
- GIC driver: SPI routing, SGIs for IPIs, MSIs via ITS/GICv2m
(`ComposeMsi` returns false, so drivers fall back to INTx/polling).
- Generic timer tick (`Arch::Timer::InitializeBsp` returns false).
- Secondary CPU start-up, PSCI reset and suspend.
Building it needs `clang` and `ld.lld` (`make kernel ARCH=aarch64`;
`KERNEL_LD=` points at a different `ld.lld`, e.g. rustup's
`lib/rustlib/x86_64-unknown-linux-gnu/bin/gcc-ld/ld.lld`).
## Adding another architecture
1. Create `kernel/src/Arch/<arch>/` with the six inline headers listed above
and stubs for the out-of-line functions (copy `aarch64/` as a template).
2. Add the target to `Arch/Arch.hpp` and to the `#if` chains at the end of
the dispatching interface headers.
3. Add `KernelStartSymbol` / `KernelEndSymbol` to `kernel/linker-<arch>.ld`,
and the `ld.lld` emulation to `LD_EMULATION_<arch>` in `kernel/GNUmakefile`.
4. Teach `Boot/Protocols/LimineProtocol.cpp` the architecture's MP fields.
5. Run `./scripts/check-arch-neutral.sh <arch>` until it passes.