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:
Mach-Ofilesdyldstubs,trampolines, andGOTmangled symbols
In Article 2, we proved it with real terminal commands:
- confirmed that
staticframework symbols are merged directly into the appMach-O» - confirmed that
dynamicframeworks appear as separate images inotool -L» - found
__DATA_CONST,__gotentries indynamicframeworks — and confirmed their absence instaticones » - observed
symbol stubsin disassembly — the real lazy binding mechanism » - showed that
SPMandXcodeframeworks follow identicalMach-O+dyldrules » - and discovered that
DEBUGbuilds wrap everything in a.debug.dylib»
“Why does the app still work in
DEBUGwhen I removeEmbed & Signfor adynamicframework — but crashes when I run it outsideXcode?”
In this article, we answer that question — with evidence.
We will:
- Explain what the
.debug.dylibreally is — and whyXcodecreates it - Show what
@rpathmeans and whatXcodeactually does with it inDEBUG - Prove why
Embed & Signseems “ignored” inDEBUG— but is critical inRELEASE - Explain why
DEBUGbuilds are not representative of real runtime behavior - And show why this matters for
CI,TestFlight, andApp Storebuilds
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.dylibstructure and framework placement — may differ in other versions.Disclaimer
TL;DR
- In
DEBUG,XcodeinjectsDYLD_FRAMEWORK_PATHinto the process at launch, pointingdyldtoDerivedData/Debug-iphonesimulator/— this is the proven reason dynamic frameworks load even withoutEmbed & Sign. .debug.dylibis an Xcode-generated overlay that bundles all debug symbols into one image — it disappears entirely inRELEASE.- In
RELEASE, the injectedrpathis gone —Embed & Signis mandatory for any dynamic framework to load. DEBUGbuilds are not representative of real runtime behavior — they can silently mask missingEmbed & Signsettings.- Always verify framework linking in a
RELEASEbuild before pushing to CI,TestFlight, or theApp 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 & Sign → Do Not Embed:
- runs fine from
XcodeinDEBUG✅ - crashes immediately when run outside
Xcode💥
Console log:
Termination Reason: Namespace DYLD, Code 1, Library missing;
Library not loaded: @rpath/XcodeFrameworkDynamic.framework/XcodeFrameworkDynamicThis 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
@rpathreally is - what
.debug.dylibreally 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/XcodeFrameworkDynamicThe @rpath prefix means:
“Search for this binary in the list of runtime search paths —
LC_RPATHentries — 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_RPATHOutput 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 foundLaunched 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 DYLDBoth 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/FrameworksThe 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:
DYLD_FRAMEWORK_PATH/DYLD_LIBRARY_PATH— environment variables ← Xcode wins hereLC_RPATHentries embedded in the binaryDYLD_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.
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 byXcode, 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-FrameworkOutput:
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.dylibResult 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)
...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 dependenciesThe 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 MyMathOutput:
(click to see full output result)
0000000000004190 T _$s20XcodeFrameworkStatic06MyMathC0V3addyS2i_SitFZ
0000000000004190 T _$s20XcodeFrameworkStatic06MyMathC0V3addyS2i_SitFZ0000000000004298 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 _____ 20XcodeFrameworkStatic06MyMathC0VNotice the last MyMath entry:
U _$s21XcodeFrameworkDynamic06MyMathC0V3addyS2i_SitFZThe U — undefined — 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
Key takeaway
The
.debug.dylibis not a side effect — it is a deliberateXcodeoptimization. It makes builds faster, debugging richer, andSwiftUIPreviews possible. But it also means:DEBUGarchitecture is fundamentally different fromRELEASE.
Why RELEASE Does Not Forgive You
In DEBUG, Xcode helps you in ways you might not even notice:
- it places
.debug.dylibdirectly in the.approot - it builds everything into the same
DerivedDataoutput directory - the app works when launched by
Xcodeeven withoutEmbed & Sign
RELEASE — none of that exists.What changes in RELEASE
When you build for RELEASE (or archive for TestFlight / App Store):
- no
.debug.dylibis created - your app
Mach-Ocontains your actual code directly dyldonly looks inLC_RPATHentries — inside the.appbundle
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 (..., 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 37Two entries:
/usr/lib/swift— Swift runtime system libraries@executable_path/Frameworks— embedded frameworks inside the.appbundle
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:
XcodeFrameworkDynamic.frameworkis now inside.app/Frameworks/—Embed & Signdid its job- No
.debug.dylib, no__preview.dylibin the.approot — only the real appMach-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 Storebehavior
DEBUG was lying by helping you.
RELEASE tells the truth.
Visual model
Key takeaway
RELEASEis the real world. If your app only works inDEBUG— it is already broken.Embed & Signis not optional fordynamicframeworks — 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.dylibwrapper - no shared
DerivedDataoutput directory - the build behaves like
RELEASE— even if you build withDEBUGconfiguration
So a misconfigured dynamic framework that works fine on your machine:
✅ runs from Xcode on your Mac
💥 crashes on CIThis 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 debugIf 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
DerivedDatadirectory - no
Xcodelaunch context
If a dynamic framework is missing from Frameworks/ inside the .ipa:
💥 crash at launch — for every testerThis 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 Storebuilds go throughApple’s processing pipeline- binaries are re-signed and repackaged
- any path that relied on
Xcode’s launch context will break
The rule is simple:
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:
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:
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:
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.dylibis a realMach-Odynamic library — not a debug symbol file, not adSYM- In
DEBUGsimulator builds (Xcode 26.3),Xcodeplaces.debug.dylibdirectly in the.approot and builds the dynamic framework next to the.appinDerivedData— there is noDerivedDatapath injected intoLC_RPATH - The apparent “forgiveness” of
DEBUGis not a property of the simulator — it is a property of being launched byXcode; the same binary crashes on the same simulator withoutXcode - The proven mechanism:
XcodeinjectsDYLD_FRAMEWORK_PATHinto the process environment at launch, pointingdyldtoDerivedData/Debug-iphonesimulator/— confirmed vialldbandps ewwpon the live process DEBUGbuilds are not representative of real runtime behavior — they are optimized for iteration speed, not production truth- In
RELEASE,dyldsearches only/usr/lib/swiftand@executable_path/Frameworks— if a dynamic framework is not embedded, the app crashes at launch Embed & Signis not optional for dynamic frameworks — it is required for every environment outside an activeXcodedebug session- Static frameworks are merged at link time — no
dyld, no@rpath, no embedding decision needed - The
GOT(__DATA_CONST,__got) is howdyldbinds dynamic symbols at load time — this mechanism is identical inDEBUGandRELEASE - 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-Ostructuredyldand 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:
otoolandnmin practice- reading
__DATA_CONST,__gotentries - symbol stubs in
__TEXT,__stubs .debug.dylib— first discoverySPMvsXcodeframework differences
Article 3 — Runtime Truth
Why DEBUG lies and RELEASE does not:
.debug.dylib— what it really is and whyXcodecreates it@rpathand the real simulator path resolution mechanismDEBUGvsRELEASEarchitecture differences — verified onXcode 26.3CI,TestFlight,App Storeimplications
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.
End of Series.
Further Reading
-
Apple: Dynamic Libraries Overview
https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/DynamicLibraries/100-Articles/OverviewOfDynamicLibraries.html -
Apple: Mach-O Programming Topics
https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/MachOTopics -
dyld source code
https://opensource.apple.com -
Static vs Dynamic Libraries (Poplauschi)
https://bpoplauschi.github.io/2021/10/25/Advanced-static-vs-dynamic-libraries-and-frameworks.html