Skip to content

What happens when you launch an app on macOS

From double-click to first window: LaunchServices, launchd, posix_spawn, AMFI, dyld, the shared cache, sandbox profile installation, the runloop. Six subsystems in three seconds.

Published 5 min read
App launch flow on macOSEnd-to-end app launch flow: Finder double-click triggers LaunchServices, which asks launchd to posix_spawn, the kernel validates the signature and installs the sandbox, dyld loads dylibs and runs initializers, then main runs.SOURCE PROCESSDAEMONS + KERNELNEW PROCESSuser double-clicksFinder · t = 0LSOpenLaunchServices (lsd)resolve bundle · t + 5 mslaunchd (PID 1)prepare posix_spawn · t + 10 mskernel: posix_spawnbsd/kern/kern_exec.c · t + 15 msAMFI + code signingvalidate · load entitlements · t + 20 mssandbox + map shared cacheapply profile · map VM · t + 25 msjumpdyldresolve symbols · run initializers · t + 30–50 msmain / NSApplicationMainapp code starts · t + 60 msFirst window typically appears around t + 200 ms depending on app complexity. Kernel-side work (validation + map + sandbox) is roughly fixed; dyld scales with dylib count, main scales with app init.

You double-click an app. Three seconds later it's running. In between, six different subsystems coordinate to: find the binary, verify its signature, set up its sandbox, fork a process, replace its address space, load every dylib, run static initializers, and finally call main. This article walks the whole thing.

The actors, in order of appearance:

  • Finder (the app you double-clicked from)
  • LaunchServices (the framework that resolves "which app handles this") — owned by lsd daemon
  • launchd (PID 1)
  • AMFI (in the kernel)
  • the sandbox kext
  • dyld (in the new process's userspace)

Step 1: Finder asks LaunchServices

Finder doesn't exec apps directly. It calls LSOpenApplicationAtURL — a LaunchServices framework call.

LaunchServices is the framework that owns the "what app handles this" decision. It maintains a database (/private/var/folders/.../com.apple.LaunchServices*) mapping bundle IDs to app bundle locations, URL schemes to handler apps, file UTIs to handler apps, etc.

For your double-clicked app:

  1. Finder hands the app's .app URL to LaunchServices.
  2. LaunchServices figures out: is this app already running? (If yes, just NSApplication.activate and skip the whole launch.)
  3. If not running: LaunchServices asks launchd to spawn it.

The cross-process communication here is XPC — LaunchServices in your Finder process talks to the lsd daemon, which talks to launchd.

Step 2: launchd posix_spawns the app

launchd receives the spawn request. It:

  1. Looks up the app's bundle info from its plist cache (executable path, info plist contents).
  2. Constructs a posix_spawn argument set:
    • Executable: Foo.app/Contents/MacOS/Foo
    • argv: standard for app launch
    • envp: DYLD_* variables filtered out (they're stripped for hardened-runtime apps); user defaults injected; Apple-specific variables added
    • Spawn attributes: jetsam priority, sandbox profile name, QoS defaults
  3. Calls posix_spawn(2).

See the fork/exec article for what posix_spawn does internally. The short version: it atomically forks and execs in one syscall, with no broken intermediate state.

Step 3: kernel does exec validation

Now we're in the kernel's exec_mach_imgact. Before the new process runs a single instruction, the kernel:

  1. Opens the binary, validates the Mach-O headers.
  2. Validates the code signature: parses the LC_CODE_SIGNATURE, walks the cert chain to Apple's root, verifies CodeDirectory hashes are intact. See the code signing chain article.
  3. Asks AMFI (AppleMobileFileIntegrity.kext): is this binary allowed to load? AMFI checks team identifier, library validation status, runtime flags. See the AMFI glossary entry.
  4. Loads entitlements from the signature blob into the new process's task struct.
  5. Builds the initial VM map: maps __TEXT, __DATA, __LINKEDIT segments per the Mach-O load commands.
  6. Maps the shared cache/System/Library/dyld/dyld_shared_cache_* — at its predetermined address. This is a single large file containing every system dylib, pre-linked. Mapping it once gives the process access to UIKit, AppKit, Foundation, Metal, every CoreFoundation type — all without separate dylib opens.

That last point is one of the most important performance details on macOS: every dylib your app uses is already mapped at process start, courtesy of the shared cache.

Step 4: sandbox is applied

If the app has com.apple.security.app-sandbox = true in its Info.plist (every App Store app does), the kernel:

  1. Constructs the effective sandbox profile from the app's entitlements.
  2. Compiles the profile bytecode via libsandbox (this work is mostly done at build time; only fine-tuning happens now).
  3. Hands the bytecode to Sandbox.kext via sandbox_apply.

From this point on, every syscall the process makes is gated by the sandbox bytecode. See the sandbox profiles article.

Step 5: dyld takes over

The kernel never directly executes a user binary. It executes dyld. The kernel maps dyld into the new process's address space (it lives at a well-known location in the shared cache), sets up auxiliary vector data (argv, envp, the shared cache slide), and starts the process executing at dyld's entry point.

dyld's job:

  1. Map the main executable's dependencies if not already in the shared cache. (For modern macOS apps, almost everything is in the shared cache; only third-party frameworks need separate mapping.)
  2. Resolve every symbol the main executable references against the loaded dylibs. The "chained fixups" format in modern Mach-O makes this fast — fixups are pre-computed at link time.
  3. Run static initializers in dependency order. Every + initialize Objective-C method, every C++ static constructor, every Swift static let runs here.
  4. Call main (or NSApplicationMain).
apple-oss-distributions/dylddyld/main.cppdyld main — what actually starts running when the kernel hands off.View on GitHub(line )

For a typical app, the dyld stage takes 10-50 ms.

Step 6: app's main runs

Finally, the app's own code starts. For an AppKit app, NSApplicationMain runs:

  1. Creates the shared NSApplication.
  2. Loads the main nib/storyboard.
  3. Sets up the main run loop.
  4. Sends applicationDidFinishLaunching: to the app's delegate.
  5. Enters the run loop, processing events from the WindowServer.

The first window draws when the WindowServer's CoreAnimation pipeline produces the initial frame and the display engine pushes it to the panel. From the user's perspective, the app "appeared" — but the heavy lifting (shared cache mapping, sandbox install, signature validation) was all kernel work that finished before main even started.

What surprises newcomers

  • The kernel never executes a user binary directly. Every app starts in dyld. Even /usr/bin/true.
  • The shared cache is the reason apps launch fast. Mapping every system dylib via one mmap is dramatically cheaper than dlopen-ing each.
  • Signature validation is lazy at the page level, eager at the directory level. The CodeDirectory's signature is verified at exec; individual code pages are verified as they fault in.
  • LaunchServices is itself a process. Your Finder calls LaunchServices over XPC to ask lsd; nothing about the launch happens in Finder's address space.

A complete timeline

t = 0     double-click in Finder
t + 5ms   LaunchServices resolves bundle, decides to spawn
t + 10ms  launchd receives spawn request
t + 15ms  launchd builds posix_spawn args, calls into kernel
t + 20ms  kernel: validate Mach-O, signature, entitlements
t + 25ms  kernel: install sandbox, map shared cache, map binary
t + 30ms  kernel: jumps to dyld
t + 50ms  dyld: resolves symbols, runs static initializers
t + 60ms  app's main runs
t + 200ms first window drawn (depending on app complexity)

The kernel-side work is bounded — maybe 20 ms regardless of how big the app is. dyld scales with how many dylibs you have outside the shared cache. Static initializers and first-paint scale with how much the app itself does.

The articles this synthesizes from:

And source:

apple-oss-distributions/xnubsd/kern/kern_exec.cexec_mach_imgact — the kernel side of every app launch.View on GitHub(line ) apple-oss-distributions/launchdlaunchd/src/launchd.cThe historical launchd open-source release.View on GitHub(line )

Related

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.
From the classic 4.4 BSD TCP/IP stack to Apple's modern Skywalk replacement — how packets traverse XNU's networking code, and why Apple is moving the data plane out of the BSD layer.
Every Mach-O on a Mac is signed; every signature chains to an Apple root cert. Here's what the kernel actually verifies, what notarization adds, and how the SEP roots it all.