Files
MontaukOS/docs/porting.md
T

5.8 KiB

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 #ifs 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.