Symbolicating a crash report from the App Store
A crash report from a user or from App Store Connect often arrives as a list of hexadecimal addresses:
0 MyApp 0x0000000102a4c8d0 0x102a40000 + 51408
1 MyApp 0x0000000102a4b120 0x102a40000 + 45344
2 UIKitCore 0x00000001a2c31f44 ...
That is unreadable and completely recoverable. The addresses map to function names and line numbers, and the thing that does the mapping is the dSYM.
The dSYM is the whole story
When Xcode builds with optimisations, it strips symbol names from the binary and writes them to a
separate .dSYM bundle. Without it, the crash report is hex forever.
The dSYM must match the exact build. Not the same version — the same build. Every compile produces a new UUID, and a dSYM from a rebuild of identical source will not work.
Check the UUID of a crash report against a dSYM:
# UUID of the dSYM
dwarfdump --uuid MyApp.app.dSYM
# it must match the "Binary Images" section of the crash report
If those do not match, stop — nothing else will work, and symbolicating with a mismatched dSYM produces plausible, wrong answers.
Where to find it
If you uploaded to App Store Connect, Xcode has it. Window → Organizer → Archives, select the build, right-click → Download Debug Symbols.
If you use bitcode — increasingly rare — Apple recompiled your app, so your local dSYM is not the one that shipped. You must download Apple’s.
If it is a local archive, it is inside the .xcarchive:
MyApp.xcarchive/dSYMs/MyApp.app.dSYM
Archive every release build and keep the dSYM. This is the one that catches people out — a crash report from a version whose dSYM you did not keep is unsolvable, permanently.
Symbolicating
For a whole .crash or .ips file, Xcode does it automatically when you drag it into the Organizer’s
Crashes tab, provided the dSYM is in Spotlight’s index.
For one address, atos is the direct tool:
atos -o MyApp.app.dSYM/Contents/Resources/DWARF/MyApp \
-arch arm64 \
-l 0x102a40000 \
0x0000000102a4c8d0
-o— the binary inside the dSYM bundle, not the bundle itself-arch—arm64for devices-l— the load address, the second hex number on the crash line- the final argument — the address to resolve
Output:
ProfileViewModel.loadAvatar(for:) (in MyApp) (ProfileViewModel.swift:84)
Tip
The load address (-l) is the one people get wrong. It is the module’s base address, listed in
the “Binary Images” section of the report and repeated on each frame as the number before the
+. Using the frame address for both produces a wrong answer rather than an error.
Reading the report before symbolicating
Some of it is readable immediately, and two fields decide what you are even looking at.
Exception type:
| Type | Usually means |
|---|---|
EXC_BAD_ACCESS (SIGSEGV) |
Dereferenced bad memory — a dangling unowned, a use-after-free |
EXC_CRASH (SIGABRT) |
A deliberate abort — force unwrap of nil, array out of bounds, fatalError |
EXC_BAD_INSTRUCTION (SIGILL) |
A Swift runtime trap — also force unwrap and bounds |
EXC_RESOURCE |
Memory or CPU limit exceeded — the app was jettisoned |
0x8badf00d |
Watchdog: the main thread was blocked too long at launch or on suspend |
Those last two are not code bugs in the usual sense, and symbolicating them is less useful than looking at what the main thread was doing.
Termination reason often names the exact problem — Fatal error: Unexpectedly found nil while unwrapping an Optional value tells you the class of bug before any address is resolved.
The thread that matters
The crashed thread is marked. Read it from the top down and find the first frame that is your code — frames above it are usually system internals reacting to your bug rather than the bug itself.
For EXC_RESOURCE and watchdog terminations, look at thread 0 regardless of which thread is
marked, because the question is what was blocking the main thread.
What actually prevents this being painful
Archive and keep every shipped dSYM, in version control or object storage. They are a few megabytes and they are the difference between fixing a crash and guessing.
Use a crash reporter that symbolicates for you — Xcode Organizer covers App Store builds, and a service handles TestFlight and enterprise distribution. Uploading dSYMs on every release build should be part of your release script, because the day you need one is the day you discover the manual step was skipped.
Do not disable dSYM generation for Release. It is on by default and I have seen it turned off to speed up builds. It saves seconds and costs you every crash report you will ever receive.