Porting

Bring up a new architecture

Teach XNU and the darwinOS build system about a new CPU architecture — not a new board within an existing family.

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:

  1. Toolchain support. clang/LLVM target works for both hosted compilation (the kernel itself) and freestanding targets (dyld, libsystem_kernel).
  2. XNU kernel port. New osfmk/arch/<arch>/ tree with assembly stubs, exception handling, trap vectors, syscall entry/exit, context switching.
  3. Memory management. Page tables, TLB management, fault handlers for this architecture’s memory model.
  4. libSystem kernel interface. New libsyscall/arch/<arch>/ stubs that implement the syscall ABI.
  5. dyld port. The dynamic linker needs to know how to relocate this architecture’s Mach-O files.
  6. Build system wiring. Configure script accepts the new arch; per-arch Makefile fragments land.
  7. 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-targets

If 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.