# 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//`, 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 (``); - 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/ the port in progress (see below) ``` 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//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 port 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, and: - Exception vectors (`Vectors.S`, installed in `EarlyInitialize`). Every exception saves one 288-byte frame that is both `Arch::PanicFrame` and `Arch::SyscallFrame`. EL0 `svc` goes to `SyscallDispatch`; other synchronous exceptions go to `ArchSyncException` (`Exceptions.cpp`), which grows user stacks and lazy anonymous memory, kills faulting processes with a crash report, and panics on kernel faults. - Translation tables (`Paging.cpp`): 4 KiB granule, 48-bit VAs. The kernel half lives in its own TTBR1 table; `KernelRoot()` is the TTBR0 table of kernel contexts (EFI identity mappings only, so null pointers fault). The HHDM maps only the memory map's RAM and firmware regions, never the holes between them (device space on ARM). MAIR: 0 Normal WB, 1 Normal NC (`MapWC`), 2 Device-nGnRE (`MapMMIO`). Not yet written (each is marked `TODO(aarch64)`): - GIC driver: SPI routing, SGIs for IPIs, MSIs via ITS/GICv2m (`ComposeMsi` returns false, so drivers fall back to INTx/polling). IRQs reach `ArchIrqException`, which only logs them. - Generic timer tick (`Arch::Timer::InitializeBsp` returns false). Boot currently stops in the first driver that waits on `GetMilliseconds()` (xHCI port power-up). - Secondary CPU start-up, PSCI reset and suspend. Building it needs `clang` and `ld.lld`; without a system `ld.lld` the kernel makefile uses the one rustup ships (`~/.rustup/toolchains/*/lib/rustlib/*/bin/gcc-ld/ld.lld`), and `KERNEL_LD=` overrides both. `make run ARCH=aarch64` boots a kernel-only image (userspace is x86_64-only so far) on QEMU `virt` with a GICv2 and Cortex-A72 cores, the Raspberry Pi 4/400's configuration; it needs `qemu-system-aarch64` and `qemu-efi-aarch64`. The kernel console is mirrored to the serial port the ACPI SPCR table names (PL011, SBSA UART, 16550 or the BCM2835 mini UART; `Drivers/Serial/Uart.cpp`), including the log written before the port was found. On QEMU that is `-serial stdio`. ## Adding another architecture 1. Create `kernel/src/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-.ld`, and the `ld.lld` emulation to `LD_EMULATION_` in `kernel/GNUmakefile`. 4. Teach `Boot/Protocols/LimineProtocol.cpp` the architecture's MP fields. 5. Run `./scripts/check-arch-neutral.sh ` until it passes.