This is an advanced guide. Bringing up a new architecture (not a new board) is a multi-month effort that touches the kernel, the build system, the toolchain, libSystem, and userland. If your target is a new board within an existing architecture, the port to a new board guide is what you want instead.
“Architecture” here means a new CPU target like RISC-V 64, POWER9, or SPARC — something XNU doesn’t currently compile for. The upstream XNU already has a long and opinionated story about how to add architectures; darwinOS follows it with minor additions for our build system.
Scope
A successful architecture bring-up requires, at minimum:
- Toolchain support. clang/LLVM target works for both hosted compilation (the kernel itself) and freestanding targets (dyld, libsystem_kernel).
- XNU kernel port. New
osfmk/arch/<arch>/tree with assembly stubs, exception handling, trap vectors, syscall entry/exit, context switching. - Memory management. Page tables, TLB management, fault handlers for this architecture’s memory model.
- libSystem kernel interface. New
libsyscall/arch/<arch>/stubs that implement the syscall ABI. - dyld port. The dynamic linker needs to know how to relocate this architecture’s Mach-O files.
- Build system wiring. Configure script accepts the new arch; per-arch Makefile fragments land.
- At least one platform expert + one virtual machine so the port can run somewhere.
Expect this to be 6–12 months of full-time work for one experienced kernel engineer, or several people’s part-time effort over a year.
Step 1: Get the toolchain ready
Confirm upstream clang builds for your target:
clang --target=<your-triple> --print-targetsIf the target needs extra assembly support or a custom relocation, upstream the LLVM bits first. darwinOS will never carry a vendor-patched toolchain as a prerequisite for an architecture port.
Step 2: Start with the nop platform
Begin with a “do-nothing” platform: a kernel that boots into a spin loop with IRQs masked. That lets you verify:
- The image loads at the right physical address
- Early setup runs
- The first
b .instruction is reached
You’ll typically test this in QEMU or another emulator — your first silicon is much later.
Step 3: Exception and trap tables
This is the hardest individual piece. Every architecture has its own model: AArch64’s exception levels, RISC-V’s mtvec table, x86_64’s IDT. You need to:
- Build the trap entry assembly that saves register state to the exception frame XNU expects
- Dispatch based on exception class to the right BSD/Mach handler
- Implement the syscall trap so userspace can reach the kernel
- Handle page faults correctly enough to run the pager
Each of these has a working template in the existing architectures. Read osfmk/arch/arm64/ carefully before you write anything.
Step 4: Context switching
The scheduler calls into architecture-specific code to save outgoing thread state and restore incoming state. cswitch.s (or its equivalent in your new arch) is a tight piece of assembly that needs to be right or nothing works.
Once context switching works, thread_block() can run. Once that runs, launchd can fork. Once launchd can fork, the system runs.
Step 5: dyld and libSystem
The dynamic linker reads Mach-O relocations and fixes them up at load time. New architecture = new relocation types. Specifically:
ARM64_RELOC_BRANCH26,X86_64_RELOC_BRANCH, etc. — the native branch relocation- GOT and lazy-binding relocations for position-independent code
- Thread-local storage relocations if TLS is supported
darwinOS dyld lives in dyld/dyld/ — add <arch>/ subtrees alongside the existing ones.
libSystem’s syscall stubs are mostly assembly that marshals arguments into the syscall ABI of your new architecture. They live in libsystem/libsyscall/arch/<arch>/.
Step 6: Build system
./configure accepts a new --arch= value. The heavy lifting is a new fragment under glue/mk/arch/<arch>.mk that declares:
- Compiler triple
- Per-arch CFLAGS / LDFLAGS
- Assembler dialect
- The kernel linker script
Once that’s in, make world --arch=<arch> produces an image for the new target.
Step 7: Bring-up board
With everything above working in an emulator, find a hardware target — at minimum a single SBC or reference board — and port darwinOS to that board. Real silicon always surfaces things the emulator doesn’t.
What success looks like
A new architecture is “done” when:
make world --arch=<arch>builds a bootable image unattended in CI- The image boots to a shell on at least one hardware target
- The host-side and guest-side test suites pass
- The status matrix carries an honest line for the new arch
Until all of that holds, the arch stays in an experimental branch.
Open a tracking RFC
Before you start writing code, open an RFC describing the architecture, the toolchain status, the bring-up board, and the expected timeline. A multi-month port with nobody else aware of it tends to bit-rot alone; an RFC invites help and keeps the rest of the project from shifting under you.