9.1 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 outsidesrc/Arch/(the Limine adapter is the one exception:limine.hhas per-architecture SMP structures); - no generic file includes a port's private headers (
<Arch/x86_64/...>); - with clang installed, the kernel compiles for
aarch64and 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/<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:
Arch::EarlyInitialize()- descriptor tables, FPU/SIMD enable. No allocator.- Frame allocator and heap come up (generic).
Arch::InitializeTraps()- exception vectors.Paging::Init()switches to the kernel's own page tables, thenArch::InitializeMemoryAttributes()(x86: PAT for write-combining).- ACPI tables are parsed (generic).
Arch::InitializeInterrupts(xsdt)brings up interrupt controllers, callsSmp::InitBsp(), idle and power management; kmain then callsArch::EnableInterrupts(). - PCI, AML, drivers (generic);
Timekeeping::TimerInitialize()registersIRQ_TIMERand callsArch::Timer::InitializeBsp(1). Arch::InitializePlatformDevices()(x86: PS/2).Arch::PrepareUserMode(),montauk::abi::InitializeSyscalls()(callsArch::InitializeSyscallEntry()), scheduler, IPC.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 inEarlyInitialize). Every exception saves one 288-byte frame that is bothArch::PanicFrameandArch::SyscallFrame. EL0svcgoes toSyscallDispatch; other synchronous exceptions go toArchSyncException(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). -
GICv2 (
Interrupts.cpp, from the MADT): IRQ slots map onto INTIDs --IRQ_TIMERonto the generic timer's PPI, the MSI slots (24-45) onto the SPIs of a GICv2m MSI frame (ComposeMsi), the IPI slots onto SGIs 0/1. Interrupts are completed (EOI) before the handler runs, as on x86; a level-triggered source drops its line first in an ack hook. -
Generic timer tick (
Timer.cpp): the EL1 virtual timer atCNTFRQ_EL0, its PPI from the GTDT, re-armed from the ack hook. -
PSCI
SYSTEM_RESET/SYSTEM_OFF(Arch::Reboot/Arch::PowerOff) through the conduit the FADT's ARM_BOOT_ARCH names. Hardware-reduced ACPI has no PM1 block, soAcpiShutdownleaves power-off to the port. -
Arch::SyncInstructionCacheafter the ELF loader writes code: ARM instruction fetch does not snoop data writes (QEMU hides this; real cores do not). EL0 may maintain caches itself (SCTLR_EL1.UCI).
Not yet written (each is marked TODO(aarch64)):
- Secondary CPU start-up (
Smp::BootAPs). - PCI INTx: the PC interrupt lines (slots 1-23) have no ARM meaning, so
RouteLegacyIrqrefuses them. Routing a PCI pin needs its_PRTentry and a GSI-based routing call; until then PCI drivers use MSI (GICv2m) or polling.
The generic kernel is compiled -fsigned-char for AArch64: it was written
for x86's signed plain char.
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 the full image (desktop, apps,
USB keyboard and mouse) 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, and the userspace compiler
(./toolchain/build-montauk-toolchain.sh aarch64). KERNEL_ONLY=1 builds an
image without userspace.
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.
Userspace
The programs are architecture-neutral C/C++ built by <arch>-montauk-gcc
(see toolchain/README.md). programs/mk/arch.mk holds everything
architecture-specific in the build: the toolchain prefix, FP/SIMD flag sets
(ARCH_FP_FLAGS for GUI apps, ARCH_NOFP_FLAGS for system programs) and the
output layout. x86_64 keeps bin/, obj/ and libraries next to their
sources; other architectures build beside them into bin-<arch>/,
obj-<arch>/ and lib/<name>/build-<arch>/, so both can be built from one
tree. The few architecture-specific sources are the raw system-call
wrappers (#if defined(__x86_64__) / __aarch64__ blocks in
montauk/syscall.h, libc/montauk.h, lib/libc/libc.c and
lib/libc/crt/crt1.c), lib/libc/arch/setjmp-<arch>.S and the scanout
store fence in gui/framebuffer.hpp.
Not built for AArch64: tcc (it generates x86 code), the on-OS SDK and the
Rust programs (their x86_64-unknown-montauk target).
Adding another architecture
- Create
kernel/src/Arch/<arch>/with the six inline headers listed above and stubs for the out-of-line functions (copyaarch64/as a template). - Add the target to
Arch/Arch.hppand to the#ifchains at the end of the dispatching interface headers. - Add
KernelStartSymbol/KernelEndSymboltokernel/linker-<arch>.ld, and theld.lldemulation toLD_EMULATION_<arch>inkernel/GNUmakefile. - Teach
Boot/Protocols/LimineProtocol.cppthe architecture's MP fields. - Run
./scripts/check-arch-neutral.sh <arch>until it passes.