Static vs Dynamic Framework Dispatching (Part 3 of 3): Real-world edge cases

Static vs Dynamic Framework Dispatching (Part 3 of 3): Real-world edge cases

Why Debug Lies to You: dyld, rpaths, and the .debug.dylib

This is Article 3 of 3 in the series Static vs Dynamic Framework Dispatching.

In Article 1, we built the mental model:

In Article 2, we proved it with real terminal commands:

  • confirmed that static framework symbols are merged directly into the app Mach-O »
  • confirmed that dynamic frameworks appear as separate images in otool -L »
  • found __DATA_CONST,__got entries in dynamic frameworks — and confirmed their absence in static ones »
  • observed symbol stubs in disassembly — the real lazy binding mechanism »
  • showed that SPM and Xcode frameworks follow identical Mach-O + dyld rules »
  • and discovered that DEBUG builds wrap everything in a .debug.dylib »
That last point left an open question:

“Why does the app still work in DEBUG when I remove Embed & Sign for a dynamic framework — but crashes when I run it outside Xcode?”

In this article, we answer that question — with evidence.

We will:

  1. Explain what the .debug.dylib really is — and why Xcode creates it
  2. Show what @rpath means and what Xcode actually does with it in DEBUG
  3. Prove why Embed & Sign seems “ignored” in DEBUG — but is critical in RELEASE
  4. Explain why DEBUG builds are not representative of real runtime behavior
  5. And show why this matters for CI, TestFlight, and App Store builds

This is not about edge cases. This is about understanding what Xcode is actually doing — and why DEBUG lies to you.


⚠️ All binary-level observations in this article were captured on Xcode 26.3 (17C529), iOS Simulator. Internal Xcode build mechanics — including .debug.dylib structure and framework placement — may differ in other versions.

Disclaimer

TL;DR

  • In DEBUG, Xcode injects DYLD_FRAMEWORK_PATH into the process at launch, pointing dyld to DerivedData/Debug-iphonesimulator/ — this is the proven reason dynamic frameworks load even without Embed & Sign.
  • .debug.dylib is an Xcode-generated overlay that bundles all debug symbols into one image — it disappears entirely in RELEASE.
  • In RELEASE, the injected rpath is gone — Embed & Sign is mandatory for any dynamic framework to load.
  • DEBUG builds are not representative of real runtime behavior — they can silently mask missing Embed & Sign settings.
  • Always verify framework linking in a RELEASE build before pushing to CI, TestFlight, or the App Store.

The Symptom

We already observed this in Article 2 — but let’s state it clearly before we explain it:

⚠️

A dynamic framework with Embed & SignDo Not Embed:

  • runs fine from Xcode in DEBUG
  • crashes immediately when run outside Xcode 💥

Console log:

Termination Reason: Namespace DYLD, Code 1, Library missing;
Library not loaded: @rpath/XcodeFrameworkDynamic.framework/XcodeFrameworkDynamic

This is not a bug. This is not Xcode ignoring your settings.

This is Xcode changing the runtime loader context — and RELEASE refusing to pretend.

To understand why, we need to look at two things:

  • what @rpath really is
  • what .debug.dylib really does

What Is @rpath?

The Mechanism

When dyld loads a dynamic framework, it needs to know where to find it on disk.

The framework path stored inside the binary looks like this:

@rpath/XcodeFrameworkDynamic.framework/XcodeFrameworkDynamic

The @rpath prefix means:

“Search for this binary in the list of runtime search paths — LC_RPATH entries — embedded in the loading binary.”

In other words — @rpath is not a fixed path. It is a placeholder that gets resolved at runtime by dyld.

The Real LC_RPATH Entries in DEBUG

You can inspect them yourself:

otool -l Library-linking-Test-Xcode-Framework.app/Library-linking-Test-Xcode-Framework | grep -A3 LC_RPATH

Output from a DEBUG simulator build (Xcode 26.3):

          cmd LC_RPATH
      cmdsize 32
         path @executable_path (offset 12)

          cmd LC_RPATH
      cmdsize 40
         path @executable_path/Frameworks (offset 12)

Two entries. No DerivedData paths. No injected build directories.

The Real File Layout

This is what Xcode 26.3 produces in DerivedData after a DEBUG simulator build:

Debug-iphonesimulator/
├── Library-linking-Test-Xcode-Framework.app
│   ├── Library-linking-Test-Xcode-Framework        ← thin launcher
│   ├── Library-linking-Test-Xcode-Framework.debug.dylib
│   ├── __preview.dylib
│   └── ...
├── XcodeFrameworkDynamic.framework
│   ├── XcodeFrameworkDynamic
│   └── ...
└── XcodeFrameworkStatic.framework
    └── ...

XcodeFrameworkDynamic.framework is not inside the .app bundle. It sits next to it in Debug-iphonesimulator/.

What We Observed

We tested two scenarios with a dynamic framework set to Do Not Embed:

Launched from Xcode (active debug session):

✅ app runs — framework found

Launched directly on the simulator (no Xcode, no debug session):

💥 crash at launch
Library not loaded: @rpath/XcodeFrameworkDynamic.framework/XcodeFrameworkDynamic
Reason: tried '@executable_path/Frameworks/...' (no such file)

The .app bundle and the LC_RPATH entries are identical in both cases. The only difference is whether Xcode is driving the launch.

What This Tells Us — The Proven Mechanism

The file layout alone does not explain why the app works when launched from Xcode. The answer is in the environment variables that Xcode injects into the process at launch.

While the app is running under Xcode, query the process environment directly from lldb:

(lldb) expression -l swift -- import Foundation; ProcessInfo.processInfo.environment
  .filter { $0.key.hasPrefix("DYLD") }
  .forEach { print($0.key, "=", $0.value) }

Or from Terminal — first get the PID, then inspect the process environment:

pgrep -x "Library-linking-Test-Xcode-Framework"
# → 604  (the number will differ on your machine)

ps ewwp 604 | tr ' ' '\n' | grep DYLD

Both commands reveal:

DYLD_FRAMEWORK_PATH = /Users/.../DerivedData/.../Debug-iphonesimulator
DYLD_LIBRARY_PATH   = /Users/.../DerivedData/.../Debug-iphonesimulator:...
__XPC_DYLD_FRAMEWORK_PATH = /Users/.../DerivedData/.../Debug-iphonesimulator
__XPC_DYLD_LIBRARY_PATH   = /Users/.../DerivedData/.../Debug-iphonesimulator:...
DYLD_INSERT_LIBRARIES = .../libViewDebuggerSupport.dylib
DYLD_ROOT_PATH        = .../iOS 17.5.simruntime/Contents/Resources/RuntimeRoot
DYLD_FALLBACK_FRAMEWORK_PATH = .../System/Library/Frameworks

The key entry is DYLD_FRAMEWORK_PATH. It points directly to Debug-iphonesimulator/ — the exact folder where XcodeFrameworkDynamic.framework lives, next to the .app.

dyld resolves @rpath-prefixed paths in this order:

  1. DYLD_FRAMEWORK_PATH / DYLD_LIBRARY_PATH — environment variables ← Xcode wins here
  2. LC_RPATH entries embedded in the binary
  3. DYLD_FALLBACK_FRAMEWORK_PATH / DYLD_FALLBACK_LIBRARY_PATH

Because DYLD_FRAMEWORK_PATH is set to DerivedData, dyld finds the framework before it even checks LC_RPATH. The framework’s absence inside the .app bundle is never noticed.

When the app is launched without Xcode — manually on the simulator, via CI, or on a real device — none of these environment variables are set. dyld falls back to LC_RPATH only, finds no matching path, and crashes.

⚠️
The “forgiveness” of DEBUG builds is not a property of the simulator or the binary. It is Xcode injecting DYLD_FRAMEWORK_PATH at launch time — pointing dyld to DerivedData. Remove Xcode from the equation and the crash is immediate.

Summary

Launched by Xcode Launched without Xcode
.debug.dylib in .app root
Framework next to .app in DerivedData
DYLD_FRAMEWORK_PATH points to DerivedData ✅ injected by Xcode ❌ not set
App runs with Do Not Embed ❌ crashes
Mechanism proven at process level

The binary is the same. The file layout is the same. What changes is DYLD_FRAMEWORK_PATH — injected by Xcode, absent everywhere else.


What Is .debug.dylib — Really?

We saw in Article 2 that a .debug.dylib appears in DEBUG builds. But what exactly is it — and why does Xcode create it?

What it is

Run:

otool -L Library-linking-Test-Xcode-Framework.app/Library-linking-Test-Xcode-Framework

Output:

Library-linking-Test-Xcode-Framework.app/Library-linking-Test-Xcode-Framework:
        @rpath/Library-linking-Test-Xcode-Framework.debug.dylib (compatibility version 0.0.0, current version 0.0.0)
        /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1356.0.0)

Your app’s Mach-O binary loads almost nothing directly. Its only real dependency is the .debug.dylib — and libSystem.

Now inspect what the .debug.dylib itself loads:

otool -L Library-linking-Test-Xcode-Framework.app/Library-linking-Test-Xcode-Framework.debug.dylib

Result in smth. like:

(click to see full output result)
Library-linking-Test-Xcode-Framework.app/Library-linking-Test-Xcode-Framework.debug.dylib:
        @rpath/Library-linking-Test-Xcode-Framework.debug.dylib (compatibility version 0.0.0, current version 0.0.0)
        @rpath/XcodeFrameworkDynamic.framework/XcodeFrameworkDynamic (compatibility version 1.0.0, current version 1.0.0)
        /System/Library/Frameworks/Foundation.framework/Foundation (compatibility version 300.0.0, current version 4201.0.0)
  ...
Library-linking-Test-Xcode-Framework.app/Library-linking-Test-Xcode-Framework.debug.dylib:
        @rpath/Library-linking-Test-Xcode-Framework.debug.dylib (compatibility version 0.0.0, current version 0.0.0)
        @rpath/XcodeFrameworkDynamic.framework/XcodeFrameworkDynamic (compatibility version 1.0.0, current version 1.0.0)
        /System/Library/Frameworks/Foundation.framework/Foundation (compatibility version 300.0.0, current version 4201.0.0)
        /usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
        /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1356.0.0)
        /System/Library/Frameworks/DeveloperToolsSupport.framework/DeveloperToolsSupport (compatibility version 1.0.0, current version 23.0.4)
        /System/Library/Frameworks/SwiftUI.framework/SwiftUI (compatibility version 1.0.0, current version 7.2.5)
        /System/Library/Frameworks/UIKit.framework/UIKit (compatibility version 1.0.0, current version 9126.2.4)
        /usr/lib/swift/libswiftCore.dylib (compatibility version 0.0.0, current version 0.0.0)
        /usr/lib/swift/libswiftCoreFoundation.dylib (compatibility version 1.0.0, current version 120.100.0, weak)
        /usr/lib/swift/libswiftCoreImage.dylib (compatibility version 1.0.0, current version 2.2.0, weak)
        /usr/lib/swift/libswiftDarwin.dylib (compatibility version 1.0.0, current version 347.40.2, weak)
        /usr/lib/swift/libswiftDispatch.dylib (compatibility version 1.0.0, current version 56.0.0, weak)
        /usr/lib/swift/libswiftMetal.dylib (compatibility version 1.0.0, current version 370.64.2, weak)
        /usr/lib/swift/libswiftOSLog.dylib (compatibility version 1.0.0, current version 10.0.0, weak)
        /usr/lib/swift/libswiftObjectiveC.dylib (compatibility version 1.0.0, current version 951.1.0, weak)
        /usr/lib/swift/libswiftQuartzCore.dylib (compatibility version 1.0.0, current version 5.0.0, weak)
        /usr/lib/swift/libswiftSpatial.dylib (compatibility version 1.0.0, current version 1.0.0, weak)
        /usr/lib/swift/libswiftUniformTypeIdentifiers.dylib (compatibility version 1.0.0, current version 879.2.4, weak)
        /usr/lib/swift/libswiftXPC.dylib (compatibility version 1.0.0, current version 105.0.14, weak)
        /usr/lib/swift/libswift_Concurrency.dylib (compatibility version 1.0.0, current version 0.0.0)
        /usr/lib/swift/libswiftos.dylib (compatibility version 1.0.0, current version 1076.40.2, weak)
        /usr/lib/swift/libswiftsimd.dylib (compatibility version 1.0.0, current version 23.0.0, weak)

Two things worth noting here:

1. The .debug.dylib lists itself as its first dependency — via @rpath. This is a standard Mach-O self-identification entry, not a circular dependency.

2. The real dependency chain in DEBUG is:

App Mach-O (thin launcher)
    └── .debug.dylib (your actual app code)
            ├── XcodeFrameworkDynamic (dynamic framework)
            ├── Foundation
            ├── SwiftUI
            ├── UIKit
            └── libswiftCore + Swift runtime weak dependencies

The app Mach-O is just a thin launcher. Everything — your app code, your static frameworks, your SwiftUI views — lives inside the .debug.dylib.

What it contains

Run nm to find your static framework symbols:

nm Library-linking-Test-Xcode-Framework.debug.dylib | grep MyMath

Output:

(click to see full output result)
0000000000004190 T _$s20XcodeFrameworkStatic06MyMathC0V3addyS2i_SitFZ
0000000000004298 T _$s20XcodeFrameworkStatic06MyMathC0VACycfC
0000000000005274 s _$s20XcodeFrameworkStatic06MyMathC0VMF
00000000000045bc T _$s20XcodeFrameworkStatic06MyMathC0VMa
00000000000084c8 s _$s20XcodeFrameworkStatic06MyMathC0VMf
0000000000005238 S _$s20XcodeFrameworkStatic06MyMathC0VMn
00000000000084d8 S _$s20XcodeFrameworkStatic06MyMathC0VN
0000000000008470 s _$s20XcodeFrameworkStatic06MyMathC0VWV
00000000000042a4 t _$s20XcodeFrameworkStatic06MyMathC0Vwet
00000000000043f4 t _$s20XcodeFrameworkStatic06MyMathC0Vwst
                 U _$s21XcodeFrameworkDynamic06MyMathC0V3addyS2i_SitFZ
0000000000004f54 s _symbolic _____ 20XcodeFrameworkStatic06MyMathC0V

Notice the last MyMath entry:

                 U _$s21XcodeFrameworkDynamic06MyMathC0V3addyS2i_SitFZ

The Uundefined — is the MyMath.add symbol from XcodeFrameworkDynamic. It is referenced but not present in the .debug.dylib. dyld is expected to resolve it at load time from the separate dynamic framework Mach-O.

Compare with the static symbols — they carry T, S, s, t: those are defined — merged directly into the .debug.dylib at link time.

The boundary between static and dynamic is visible right here in nm output:

Symbol type Meaning Source
T, S, s, t defined — present in this binary static framework — merged
U undefined — must be resolved by dyld dynamic framework — separate Mach-O

Why Xcode creates it

Xcode builds a .debug.dylib for three concrete reasons:

1. Fast iteration

When you change your code and hit CMD+B, Xcode only needs to rebuild the .debug.dylib — not the entire app Mach-O. The thin launcher stays the same. This is why incremental builds in DEBUG are fast.

2. LLDB friendliness

LLDB can:

  • rebind symbols at runtime
  • inject breakpoints into a live dylib
  • replace function implementations without relaunch

This is much harder to do with a fully static binary.

3. SwiftUI Previews

SwiftUI Previews need to reload your UI code without restarting the simulator. The Preview engine loads your .debug.dylib directly — rebuilds it on each change — and swaps it in. This only works because your code lives in a dynamic library, not a static binary.

And notably — Xcode also places a __preview.dylib alongside the .debug.dylib in the .app root, dedicated entirely to Preview rendering.

Visual model

DEBUG vs RELEASE binary architecture

Key takeaway

The .debug.dylib is not a side effect — it is a deliberate Xcode optimization. It makes builds faster, debugging richer, and SwiftUI Previews possible. But it also means: DEBUG architecture is fundamentally different from RELEASE.


Why RELEASE Does Not Forgive You

In DEBUG, Xcode helps you in ways you might not even notice:

  • it places .debug.dylib directly in the .app root
  • it builds everything into the same DerivedData output directory
  • the app works when launched by Xcode even without Embed & Sign
ℹ️
In RELEASEnone of that exists.

What changes in RELEASE

When you build for RELEASE (or archive for TestFlight / App Store):

  • no .debug.dylib is created
  • your app Mach-O contains your actual code directly
  • dyld only looks in LC_RPATH entries — inside the .app bundle

You can verify this yourself. Build in RELEASE and run:

otool -L Library-linking-Test-Xcode-Framework.app/Library-linking-Test-Xcode-Framework
(click to see full output result)
Library-linking-Test-Xcode-Framework.app/Library-linking-Test-Xcode-Framework:
        @rpath/XcodeFrameworkDynamic.framework/XcodeFrameworkDynamic (compatibility version 1.0.0, current version 1.0.0)
        /System/Library/Frameworks/Foundation.framework/Foundation (..., weak)
  ...
Library-linking-Test-Xcode-Framework.app/Library-linking-Test-Xcode-Framework:
        @rpath/XcodeFrameworkDynamic.framework/XcodeFrameworkDynamic (compatibility version 1.0.0, current version 1.0.0)
        /System/Library/Frameworks/Foundation.framework/Foundation (compatibility version 300.0.0, current version 4201.0.0, weak)
        /usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
        /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1356.0.0)
        /System/Library/Frameworks/SwiftUI.framework/SwiftUI (compatibility version 1.0.0, current version 7.2.5)
        /System/Library/Frameworks/UIKit.framework/UIKit (compatibility version 1.0.0, current version 9126.2.4, weak)
        /usr/lib/swift/libswiftCore.dylib (compatibility version 0.0.0, current version 0.0.0)
        /usr/lib/swift/libswiftCoreFoundation.dylib (compatibility version 1.0.0, current version 120.100.0, weak)
        /usr/lib/swift/libswiftCoreImage.dylib (compatibility version 1.0.0, current version 2.2.0, weak)
        /usr/lib/swift/libswiftDarwin.dylib (compatibility version 1.0.0, current version 347.40.2, weak)
        /usr/lib/swift/libswiftDispatch.dylib (compatibility version 1.0.0, current version 56.0.0, weak)
        /usr/lib/swift/libswiftMetal.dylib (compatibility version 1.0.0, current version 370.64.2, weak)
        /usr/lib/swift/libswiftOSLog.dylib (compatibility version 1.0.0, current version 10.0.0, weak)
        /usr/lib/swift/libswiftObjectiveC.dylib (compatibility version 1.0.0, current version 951.1.0, weak)
        /usr/lib/swift/libswiftQuartzCore.dylib (compatibility version 1.0.0, current version 5.0.0, weak)
        /usr/lib/swift/libswiftSpatial.dylib (compatibility version 1.0.0, current version 1.0.0, weak)
        /usr/lib/swift/libswiftUniformTypeIdentifiers.dylib (compatibility version 1.0.0, current version 879.2.4, weak)
        /usr/lib/swift/libswiftXPC.dylib (compatibility version 1.0.0, current version 105.0.14, weak)
        /usr/lib/swift/libswift_Concurrency.dylib (compatibility version 1.0.0, current version 0.0.0)
        /usr/lib/swift/libswiftos.dylib (compatibility version 1.0.0, current version 1076.40.2, weak)
        /usr/lib/swift/libswiftsimd.dylib (compatibility version 1.0.0, current version 23.0.0, weak)

No .debug.dylib. The app Mach-O loads the dynamic framework directly — as the first dependency.

Now check LC_RPATH:

otool -l Library-linking-Test-Xcode-Framework.app/Library-linking-Test-Xcode-Framework | grep -A3 LC_RPATH
          cmd LC_RPATH
      cmdsize 32
         path /usr/lib/swift (offset 12)
Load command 36
          cmd LC_RPATH
      cmdsize 40
         path @executable_path/Frameworks (offset 12)
Load command 37

Two entries:

  • /usr/lib/swift — Swift runtime system libraries
  • @executable_path/Frameworks — embedded frameworks inside the .app bundle

No DerivedData. No Xcode build directory. No .debug.dylib artifacts.

The file layout in RELEASE

Release-iphonesimulator/
├── Library-linking-Test-Xcode-Framework.app
│   ├── Frameworks
│   │   └── XcodeFrameworkDynamic.framework  ← embedded inside .app
│   │       ├── XcodeFrameworkDynamic
│   │       └── ...
│   ├── Library-linking-Test-Xcode-Framework  ← real app Mach-O (no .debug.dylib)
│   └── ...
├── Library-linking-Test-Xcode-Framework.app.dSYM
│   └── ...
├── XcodeFrameworkDynamic.framework            ← also next to .app (build artifact)
│   └── ...
└── XcodeFrameworkStatic.framework
    └── ...

Two key differences compared to DEBUG:

  1. XcodeFrameworkDynamic.framework is now inside .app/Frameworks/Embed & Sign did its job
  2. No .debug.dylib, no __preview.dylib in the .app root — only the real app Mach-O

What this means for Embed & Sign

In RELEASE:

Setting Result
Embed & Sign framework copied into Frameworks/dyld finds it ✅
Do Not Embed framework not in bundle → dyld fails 💥

This is why Embed & Sign matters. Not in DEBUG — where Xcode covers for you. But in RELEASE — where reality takes over.

The crash you will see

If a dynamic framework is not embedded in RELEASE:

Termination Reason: Namespace DYLD, Code 1, Library missing
Library not loaded: @rpath/XcodeFrameworkDynamic.framework/XcodeFrameworkDynamic
Referenced from: .../Library-linking-Test-Xcode-Framework.app/Library-linking-Test-Xcode-Framework
Reason: tried:
  '/usr/lib/swift/XcodeFrameworkDynamic.framework/XcodeFrameworkDynamic' (no such file)
  '.../Library-linking-Test-Xcode-Framework.app/Frameworks/XcodeFrameworkDynamic.framework/XcodeFrameworkDynamic' (no such file)
(terminated at launch; ignore backtrace)

dyld tried each LC_RPATH entry in order — and found the framework in neither.

This is:

  • correct behavior
  • expected behavior
  • App Store behavior

DEBUG was lying by helping you. RELEASE tells the truth.

Visual model

RELEASE: dyld can only look inside the app bundle

Key takeaway

RELEASE is the real world. If your app only works in DEBUG — it is already broken. Embed & Sign is not optional for dynamic frameworks — it is required.


Why This Matters in Real Life

The DEBUG vs RELEASE difference is not just a curiosity. It has direct consequences for every step of your real delivery pipeline.

CI (Continuous Integration)

Your CI server — whether it’s Bitrise, GitHub Actions, Fastlane, or anything else — does not run inside Xcode’s loader environment.

This means:

  • no .debug.dylib wrapper
  • no shared DerivedData output directory
  • the build behaves like RELEASE — even if you build with DEBUG configuration

So a misconfigured dynamic framework that works fine on your machine:

✅ runs from Xcode on your Mac
💥 crashes on CI

This is one of the most common sources of “works on my machine” bugs in iOS teams.

Always run a sanity check on CI:

otool -L YourApp.app/YourApp | grep debug

If you see .debug.dylib in a CI build — your CI is building in DEBUG mode. RELEASE builds should never contain .debug.dylib.

TestFlight

TestFlight distributes your .ipa — a real app bundle, signed and packaged.

There is:

  • no .debug.dylib
  • no shared DerivedData directory
  • no Xcode launch context

If a dynamic framework is missing from Frameworks/ inside the .ipa:

💥 crash at launch — for every tester

This is why crashes that “never happen locally” appear on TestFlight — the DEBUG environment was hiding a real linking problem.

App Store

The App Store is identical to TestFlight from a linking perspective.

Additionally:

  • App Store builds go through Apple’s processing pipeline
  • binaries are re-signed and repackaged
  • any path that relied on Xcode’s launch context will break

The rule is simple:

⚠️
If a dynamic framework must be available at runtime — it must be embedded with Embed & Sign. No exceptions. Not in TestFlight. Not in the App Store.

Summary

Environment .debug.dylib Xcode launch context Behaves like
Xcode DEBUG (Simulator) forgiving
Xcode RELEASE strict
CI strict
TestFlight strict
App Store strict

One environment is forgiving. Four are not. Always ALWAYS test against the strict ones.


Putting It All Together

By now you have seen three different runtime realities. The source code is identical. The dispatching rules never change. What changes is the container — and the container determines everything.

Scenario 1 — Dynamic Framework, DEBUG (Simulator)

When you build in DEBUG for the simulator, Xcode restructures the binary before dyld ever sees it.

Your app Mach-O becomes a thin launcher. All your code — including any merged static frameworks — moves into .debug.dylib, which is placed in the root of the .app bundle. The dynamic framework is built into DerivedData next to the .app — not inside it.

The call chain:

DEBUG (Simulator): call chain with .debug.dylib

dyld finds the framework only when the app is launched by Xcode. The same binary launched directly on the simulator — without Xcode — crashes immediately.

Scenario 2 — Dynamic Framework, RELEASE

In RELEASE, Xcode builds a real production binary.

No .debug.dylib. Your code lives directly in the app Mach-O. dyld only searches LC_RPATH entries — /usr/lib/swift and @executable_path/Frameworks.

The call chain:

RELEASE: call chain without .debug.dylib

If the framework is not inside Frameworks/dyld fails at launch. No fallback. No Xcode context. No second chance.

Scenario 3 — Static Framework

No dyld involvement for the framework symbols at all.

The static linker merges object files from the .a archive directly into the app Mach-O (or into .debug.dylib in DEBUG) at build time. At runtime, calls resolve to addresses already present in the binary — no GOT indirection, no loader lookup.

The call chain:

RELEASE: dyld can only look inside the app bundle

No embedding decision needed. No @rpath. No crash risk. The cost: larger binary, no sharing between processes.

Three Realities, One Table

DEBUG dynamic (Simulator) RELEASE dynamic static
.debug.dylib
GOT indirection
dyld resolves at load time
Framework in DerivedData next to .app
Requires Xcode launch to work without embedding
Embed & Sign required ❌ forgiving ✅ required ❌ n/a
Symbol exists in app Mach-O

Same source code. Same symbol name. Same call site. Three completely different paths through the runtime. Understanding which path is active — and why — is what separates guessing from knowing.


Key Takeaways

  • .debug.dylib is a real Mach-O dynamic library — not a debug symbol file, not a dSYM
  • In DEBUG simulator builds (Xcode 26.3), Xcode places .debug.dylib directly in the .app root and builds the dynamic framework next to the .app in DerivedData — there is no DerivedData path injected into LC_RPATH
  • The apparent “forgiveness” of DEBUG is not a property of the simulator — it is a property of being launched by Xcode; the same binary crashes on the same simulator without Xcode
  • The proven mechanism: Xcode injects DYLD_FRAMEWORK_PATH into the process environment at launch, pointing dyld to DerivedData/Debug-iphonesimulator/ — confirmed via lldb and ps ewwp on the live process
  • DEBUG builds are not representative of real runtime behavior — they are optimized for iteration speed, not production truth
  • In RELEASE, dyld searches only /usr/lib/swift and @executable_path/Frameworks — if a dynamic framework is not embedded, the app crashes at launch
  • Embed & Sign is not optional for dynamic frameworks — it is required for every environment outside an active Xcode debug session
  • Static frameworks are merged at link time — no dyld, no @rpath, no embedding decision needed
  • The GOT (__DATA_CONST,__got) is how dyld binds dynamic symbols at load time — this mechanism is identical in DEBUG and RELEASE
  • The dispatching rules never change — only the container changes

Series Summary

Article 1 — Mental Model How Apple’s binary format works before you write a single line of code:

  • Mach-O structure
  • dyld and the loader pipeline
  • stubs and trampolines
  • GOT (__DATA_CONST,__got) and symbol binding
  • name mangling in Swift and Objective-C

Article 2 — Binary Proof Proving it with real tools on real binaries:

  • otool and nm in practice
  • reading __DATA_CONST,__got entries
  • symbol stubs in __TEXT,__stubs
  • .debug.dylib — first discovery
  • SPM vs Xcode framework differences

Article 3 — Runtime Truth Why DEBUG lies and RELEASE does not:

  • .debug.dylib — what it really is and why Xcode creates it
  • @rpath and the real simulator path resolution mechanism
  • DEBUG vs RELEASE architecture differences — verified on Xcode 26.3
  • CI, TestFlight, App Store implications

If you understand these three articles: ➡️ you understand framework dispatching on Apple platforms. Not as a checklist. As a mental model you can reason from.


That’s all folks

End of Series.

Further Reading

Last updated on