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.
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.
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:
- Configures the MMU to map the kernel into a virtual address space.
- Sets up exception vectors so the CPU knows where to trap.
- Configures APRR/SPRR base banks for the kernel itself.
- Jumps to
kernel_startup_threadin 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.
It does, in roughly this order:
- Initialize BSD subsystems that haven't yet been touched: the file system layer, the network stack, kqueue, the VFS namecache.
- 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.
- Set up the initial file descriptors (stdin/stdout/stderr connected to the kernel log device).
- Construct a userspace exec environment for
/sbin/launchd. - Call
load_init_programto exec/sbin/launchd. - From now on, the kernel thread
bsdinit_taskislaunchdin 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:
- Parse every plist under
/System/Library/LaunchDaemons/and/Library/LaunchDaemons/. - Decide which daemons should be started now (Boot or System session).
- Spawn them via
posix_spawn(see fork/exec article). - Start the user session managers (
launchditself acts as a per-user PID-1-equivalent for each logged-in user). - Bring up the login window via
loginwindow(which is itself a launched daemon).
Daemons launchd brings up at boot — in rough order:
kernel_taskrelated 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/bputilin recovery mode. - Safe Mode: launchd's plists honor a
RunAtBootexclusion flag for Safe Mode; many third-party daemons skip starting.
What to read next
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.