Skip to content

How macOS boots: iBoot to launchd

The full chain from power-on to your login window. Boot ROM, iBoot, kernelcache, kernel_init, bsdinit_task, launchd — what each stage does and how control transfers.

Published 6 min read
macOS boot sequenceLinear boot pipeline from Boot ROM through iBoot, kernelcache, kernel_init, bsdinit_task, and finally launchd as PID 1. The chain of trust runs from Boot ROM to every subsequent stage.CHAIN OF TRUST — each stage verifies the next before executingBoot ROMburned at manufacturejobverify LLBroot of trusthardwareiBootOS loaderjobload + verify kernelcacheset boot policysecurekernelcacheXNU + required kextsjobprelinked imagesealed by Appleimagekernel_initstart.s → C startupjobMMUschedulerIPCkernelbsdinit_taskBSD-side bring-upjobmount rootbecome launchdkernellaunchdPID 1jobspawn every userspace daemonuserSECURE / KERNEL / USER BOUNDARIESThe key transitionsBoot ROM → iBoot: signature check. Failure = no boot.iBoot → kernelcache: load image into memory, verify Apple signature, jump in.kernel_init → bsdinit_task: a kernel thread executes BSD-side bring-up.bsdinit_task → launchd: the kernel thread becomes launchd by replacing its address space.launchd → userspace: posix_spawn every daemon from the system / library plist directories.No fork-from-init pattern. No rc.d scripts. Service definitions are declarative plists launchd brings up in dependency order.

Press the power button. Within a few seconds you're at a login window. In between, control passes through five distinct stages, each in a different part of the system, with different trust assumptions and different jobs to do. This article walks them in order.

The chain (Apple Silicon flavor — Intel is slightly different at the early stages but converges by the kernel):

Boot ROM → iBoot → kernelcache → kernel_init → bsdinit_task → launchd
   ↑                                                                ↓
hardware                                                        user-mode

Stage 1: Boot ROM

The very first code that runs is in Boot ROM — a small immutable program burned into the SoC at manufacture. Its job:

  • Verify the chip is in a known-good state.
  • Load and verify LLB (Low Level Bootloader) from the flash.
  • Hand off to LLB.

Boot ROM is the root of the boot chain of trust. Its public key (Apple's) is what every subsequent signature ultimately chains back to. There is no way to update or modify Boot ROM in the field — only Apple, at manufacture, can change it.

The chain is measured: each stage verifies the next stage's signature before executing it. A modified iBoot would fail Boot ROM's verification and the machine won't boot.

Stage 2: iBoot (the OS loader)

iBoot is Apple's bootloader. On Apple Silicon it has the role that BIOS+GRUB plays on a Linux PC, compressed into one signed binary. iBoot's job:

  • Initialize enough hardware to read storage (NAND controller, memory training).
  • Discover the boot policy from nvram / Preboot volume — which OS to boot, in which mode (full security / reduced / permissive), what kernelcache to use.
  • Load the kernelcache from the Preboot volume.
  • Validate the kernelcache's signature against the boot policy.
  • Set up the initial Device Tree describing what hardware is present.
  • Jump into the kernelcache.

iBoot is also what handles recoveryOS (boot into recovery) and DFU (Device Firmware Update) modes. From the user's perspective, the difference between a normal boot and a recovery boot is just which kernelcache iBoot decides to load.

Stage 3: kernelcache loading

A kernelcache is a prelinked kernel image — the XNU kernel + every required kext, link-edited together at install time so iBoot only has to load one file. Building the kernelcache is the job of kmutil, which runs at OS install / kext approval.

apple-oss-distributions/xnuosfmk/arm64/start.sThe very first ARM instructions XNU executes — set up exception vectors, enable the MMU, jump to C code.View on GitHub(line )

The kernelcache contains:

  • XNU itself.
  • Every kext marked as required for boot — KDP debugger, the file-system kext (APFS), HID drivers needed for early init.
  • Any DriverKit dexts that opt in to early load (rare; most dexts come up later via launchd).
  • The sealed system volume hash root the kernel will verify the read-only system volume against once it can read disk.

iBoot maps the kernelcache into physical memory at a known address and jumps in.

Stage 4: kernel_init

The first kernel code runs in start.s — assembly that:

  1. Configures the MMU to map the kernel into a virtual address space.
  2. Sets up exception vectors so the CPU knows where to trap.
  3. Configures APRR/SPRR base banks for the kernel itself.
  4. Jumps to kernel_startup_thread in C.

From there the kernel sets up everything: pmap, VM compressor, slab allocator, scheduler, the IPC subsystem, the Mach ports for the host, the IOKit registry root, the platform expert, IO completion routines, the kernel debugger.

apple-oss-distributions/xnuosfmk/kern/startup.ckernel_startup_thread / kernel_bootstrap — the C-side kernel init, runs in a Mach kernel thread.View on GitHub(line ) apple-oss-distributions/xnuiokit/Kernel/IOStartIOKit.cppIOKit startup — registers the platform expert, starts probing the device tree.View on GitHub(line )

The kernel doesn't exec userspace directly. It creates the first BSD proc (PID 0, the "kernel proc"), and from inside that proc spawns the first real BSD task: bsdinit_task.

Stage 5: bsdinit_task

bsdinit_task is the bridge from kernel-only land to userspace. It runs entirely in kernel mode, in a Mach thread, but its job is to set up enough state that a userspace process can be launched.

apple-oss-distributions/xnubsd/kern/init_main.c:1bsdinit_task — the first thing on the BSD side. Reads top to bottom for the most important hour of XNU's life.View on GitHub(line 1)

It does, in roughly this order:

  1. Initialize BSD subsystems that haven't yet been touched: the file system layer, the network stack, kqueue, the VFS namecache.
  2. Mount the root file system. On modern Macs this is the sealed system volume; the kernel verifies the seal as it mounts. Failure to verify panics.
  3. Set up the initial file descriptors (stdin/stdout/stderr connected to the kernel log device).
  4. Construct a userspace exec environment for /sbin/launchd.
  5. Call load_init_program to exec /sbin/launchd.
  6. From now on, the kernel thread bsdinit_task is launchd in userspace.

That last point is worth dwelling on. There's no fork. The kernel takes its own kernel thread, replaces its address space with /sbin/launchd's image, and "becomes" PID 1. Every userspace process on the system descends from this thread.

Stage 6: launchd

launchd runs as PID 1. It is the init system, the service manager, and the socket activator all in one. Its first job on boot is to:

  1. Parse every plist under /System/Library/LaunchDaemons/ and /Library/LaunchDaemons/.
  2. Decide which daemons should be started now (Boot or System session).
  3. Spawn them via posix_spawn (see fork/exec article).
  4. Start the user session managers (launchd itself acts as a per-user PID-1-equivalent for each logged-in user).
  5. Bring up the login window via loginwindow (which is itself a launched daemon).

Daemons launchd brings up at boot — in rough order:

  • kernel_task related helpers — the kernel exposes a few user-mode helpers via launchd.
  • syslogd — to capture log output from everything that follows.
  • opendirectoryd — auth and identity.
  • launchservicesd — the daemon behind the LaunchServices framework.
  • UserEventAgent — the system-event broker.
  • mDNSResponder — Bonjour / .local discovery.
  • coreaudiod, bluetoothd, WindowServer — the device-facing daemons.
  • loginwindow — finally, the login UI.

By the time you see the login window, launchd has spawned and configured a few hundred processes. The system is fully alive.

What the boot path tells you about XNU

A few things become much clearer once you've walked the boot:

  • The boot chain of trust is unbroken. Every stage verifies the next. Modifying a single byte anywhere in the chain breaks boot.
  • launchd is special. It's not just a process that happens to be PID 1 — it's a kernel thread that got its address space replaced. There is no init script, no rc.d, no fork-from-init pattern. Service definitions are declarative plists.
  • The kernelcache architecture is why kext approval has the friction it does. A new kext requires rebuilding the kernelcache, which requires SIP-relaxed booting, user approval, and a reboot. The complexity is intentional — the cost is the security model.
  • bsdinit_task is the single most important function in XNU. Read it once. It's compact and shows you the seam between every layer.

Boot variations

A few flavors of boot worth knowing:

  • Recovery boot: hold the power button on Apple Silicon, or ⌘R on Intel. iBoot loads a recovery kernelcache that includes the recovery utilities and an alternate root file system.
  • DFU mode: iBoot fails to start; the Boot ROM serves a USB endpoint that lets a connected Mac restore the device. The "nothing works" rescue path.
  • Reduced Security boot: iBoot accepts kernelcaches that include user-approved third-party kexts. Done via csrutil/bputil in recovery mode.
  • Safe Mode: launchd's plists honor a RunAtBoot exclusion flag for Safe Mode; many third-party daemons skip starting.

For launchd's open-source roots:

apple-oss-distributions/launchdlaunchd/src/launchd.cThe historical launchd source — Apple hasn't synced this in a while, but the design holds.View on GitHub(line )

And read bsd/kern/init_main.c end-to-end at least once. It's the seam between every layer of the kernel — Mach below, BSD personality above, IOKit alongside, userspace ahead.

Related

The Unix-est of Unix calls, implemented on a Mach kernel. Why fork is awkward on macOS, what exec actually replaces, and why posix_spawn is now the preferred way to start a process.
POSIX says signals are per-process. Mach says everything is a thread. Here's how XNU bridges the two — pending masks, delivery threads, the AST mechanism, and exception ports.
Processes, file descriptors, signals, sockets — the FreeBSD-derived layer that sits on top of Mach and makes macOS pass POSIX.