Technical deep-dives on build systems, toolchains, and cross-platform development
by
Understanding Apple’s Debug Symbol Architecture, VSCode Integration, and Bazel Workflows
by Srikanth Arunachalam
Debugging iOS applications presents unique challenges compared to Linux or Windows. Apple’s “lazy” DWARF scheme, the distinction between dSYM bundles and N_OSO debug maps, and the lack of --fission support create a learning curve for developers coming from other platforms. This guide distills practical knowledge for debugging iOS simulator and device applications, with emphasis on Bazel-based build systems and VSCode integration.
--fission and .dwo Files Don’t Work on macOSOne of the most common misconceptions when moving from Linux to macOS development:
--fissionand.dwofiles are LINUX ONLY, not supported on macOS
On Linux, the -gsplit-dwarf flag (exposed via Bazel’s --fission) produces separate .dwo files containing debug information, which can later be combined into .dwp (DWARF package) files. This reduces link times and enables distributed debug info.
macOS uses a fundamentally different approach:
| Feature | Linux | macOS/iOS |
|---|---|---|
| Split debug format | .dwo files |
Not supported |
| Package format | .dwp files |
.dSYM bundles |
| Debug info location | Separate files | Inside .o files OR dSYM |
| Linker behavior | Combines debug sections | Creates debug map entries |
| Debug tool | dwp utility |
dsymutil |
As documented in MaskRay’s analysis, on macOS -gsplit-dwarf has different behavior and will not produce .dwo files.
Apple developed a unique approach to debug information that prioritizes lazy loading. From the DWARF Standards Wiki:
┌─────────────────────────────────────────────────────────────────┐
│ Apple Debug Symbol Flow │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Source Files Object Files Final Binary │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ a.c │──compile──│ a.o │ │ │ │
│ │ b.c │──compile──│ b.o │──link────│ app │ │
│ │ c.c │──compile──│ c.o │ │ │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │ │ │
│ │ N_OSO entries │ │
│ │◄────────────────────┤ │
│ │ (debug map) │ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────┐ │
│ │ dsymutil │ │
│ │ (creates .dSYM bundle) │ │
│ └─────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ app.dSYM │ │
│ │ (standalone) │ │
│ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Two modes of operation:
N_OSO Debug Map Mode: The linker creates N_OSO stab entries in __LINKEDIT pointing to original .o file paths. DWARF debug symbols live INSIDE the .o files. LLDB needs those .o files locally to resolve symbols at debug time.
dSYM Bundle Mode: dsymutil processes the debug map and .o files to create a standalone .dSYM bundle with all addresses remapped to final locations.
You can inspect N_OSO entries using:
dsymutil -s MyApp | grep N_OSO
Every Apple binary is stamped with a 128-bit unique identifier (LC_UUID load command). This UUID is copied into the dSYM during creation, ensuring reliable matching:
# Check binary UUID
dwarfdump --uuid MyApp.app/MyApp
# Check dSYM UUID
dwarfdump --uuid MyApp.app.dSYM/Contents/Resources/DWARF/MyApp
# They must match for symbolication to work
Unlike Android (which has excellent VSCode support), iOS debugging has traditionally required Xcode. However, several projects now enable VSCode-based iOS debugging.
The vscode-ios-debug extension provides:
Installation:
code --install-extension nisargjhaveri.ios-debug
Sample launch.json:
{
"version": "0.2.0",
"configurations": [
{
"type": "ios",
"request": "launch",
"name": "iOS: Launch App",
"appPath": "${workspaceFolder}/bazel-bin/MyGame_sim.app",
"bundleId": "com.example.mygame",
"iosTarget": "simulator"
}
]
}
SweetPad provides lightweight CodeLLDB integration for iOS:
Setup steps:
settings.json:
{
"lldb.library": "/Applications/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Versions/A/LLDB"
}
For Bazel-built iOS apps, configure CodeLLDB directly:
{
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "attach",
"name": "Attach to iOS Simulator",
"pid": "${command:pickProcess}",
"initCommands": [
"platform select ios-simulator",
"settings set target.source-map ./ ${workspaceFolder}/"
],
"sourceMap": {
".": "${workspaceFolder}"
}
}
]
}
Per Yrom’s debugging guide, this usually means debug symbol object paths don’t match VSCode source paths. Solutions:
lldb.library pathPer rules_apple documentation, enable dSYM generation:
# Generate dSYM for top-level target
bazel build //ios:MyGame_sim \
--apple_generate_dsym \
--ios_multi_cpus=arm64
# Generate dSYMs for all dependencies
bazel build //ios:MyGame_sim \
--apple_generate_dsym \
--output_groups=+dsyms
# Recommended debug configuration
build:debug --spawn_strategy=local
build:debug --compilation_mode=dbg
build:debug --strip=never
build:debug --features=oso_prefix_is_pwd
build:debug -c dbg
# iOS-specific debug settings
build:ios-debug --config=debug
build:ios-debug --apple_generate_dsym
build:ios-debug --define=apple.add_debugger_entitlement=yes
oso_prefix_is_pwd FeatureThis critical feature addresses Bazel sandbox path issues. From Keith Smiley’s analysis:
The -oso_prefix linker argument strips sandbox paths from N_OSO entries, replacing them with . (relative to current directory). Without this, LLDB cannot find .o files because paths point to deleted sandbox directories.
Based on rules_ios LLDB settings:
# ~/.lldbinit for Bazel projects
settings set target.source-map ./ /path/to/workspace/
platform settings -w /path/to/workspace/
settings set target.sdk-path /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk
atosWhen you have a crash address and need to symbolicate manually:
# Basic usage
atos -arch arm64 -o MyApp.app.dSYM/Contents/Resources/DWARF/MyApp -l 0x1003d8000 0x1003e2abc
# With inlined function support (-i flag)
atos -arch arm64 -i -o MyApp.app/MyApp -l 0x100000000 0x100001234
Per Nutrient’s advanced symbolication guide:
-l): The base address where the binary was loaded (from crash log)-arch): Must match the crashed device (arm64, x86_64).crash extensionThe codebase includes a Python symbolicator at tools/scripts/Symbolicator/Symbolicator.py:
# Usage pattern
python Symbolicator.py \
--dsym MyGame.app.dSYM.tar.gz \
--crash crash_report.txt \
--output symbolicated_crash.txt
This tool:
| Symptom | Cause | Solution |
|---|---|---|
| Addresses not resolved | UUID mismatch | Verify UUIDs with dwarfdump --uuid |
| Partial symbolication | Missing framework dSYMs | Build with --output_groups=+dsyms |
| “No matching arch” | Wrong architecture | Check file command output, use correct -arch |
| dSYM not found | Spotlight not indexed | Run mdimport MyApp.app.dSYM |
The ios-deploy tool enables command-line debugging:
# Install
brew install ios-deploy
# Install and debug on device
ios-deploy --debug --bundle MyApp.app
# Launch with LLDB attached
ios-deploy -d -b MyApp.app
# Boot simulator
xcrun simctl boot "iPhone 15 Pro"
# Install app
xcrun simctl install booted ./bazel-bin/MyGame_sim.app
# Launch app
xcrun simctl launch booted com.example.mygame
# Attach LLDB
lldb
(lldb) platform select ios-simulator
(lldb) process attach --name MyGame --waitfor
For debugging from another machine or advanced scenarios:
# On device/simulator (via debugserver)
debugserver *:4445 --attach=PID
# On development machine
lldb
(lldb) platform select remote-ios
(lldb) process connect connect://localhost:4445
# 1. Build with debug symbols
bazel build //:MyGame_sim \
--config=ios-debug \
--apple_generate_dsym
# 2. Install to simulator
bazel run //:install_ios_sim
# 3. Launch simulator app
xcrun simctl launch booted com.example.mygame
# 4. Attach with LLDB
lldb
(lldb) process attach --name MyGame
(lldb) settings set target.source-map ./ /path/to/MyGame/
(lldb) breakpoint set --file GameScene.cpp --line 100
(lldb) continue
# 1. Extract dSYM from build artifacts
tar -xzf MyGame.app.dSYM.tar.gz
# 2. Verify UUID match
dwarfdump --uuid MyGame.app.dSYM
# 3. Symbolicate crash address
atos -arch arm64 -i \
-o MyGame.app.dSYM/Contents/Resources/DWARF/MyGame \
-l 0x104a00000 \
0x104a12345
settings.json:
{
"lldb.library": "/Applications/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Versions/A/LLDB"
}
.vscode/launch.json:
{
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "attach",
"name": "Attach iOS Simulator",
"program": "${workspaceFolder}/bazel-bin/MyGame_sim.app/MyGame",
"pid": "${command:pickProcess}",
"sourceMap": {
".": "${workspaceFolder}"
},
"initCommands": [
"settings set target.source-map ./ ${workspaceFolder}/"
]
}
]
}
Cause: Bazel sandbox paths in N_OSO entries point to deleted directories.
Solution:
# Enable OSO prefix rewriting
bazel build //target --features=oso_prefix_is_pwd
Cause: dSYM not in LLDB search path.
Solution:
(lldb) settings append target.debug-file-search-paths /path/to/dsyms/
(lldb) add-dsym /path/to/MyApp.app.dSYM
<unavailable>Cause: Optimization level too high, variables optimized out.
Solution:
# Build with -O0
bazel build //target -c dbg --copt=-O0
Cause: Compilation happened in different directory than debugging.
Solution:
(lldb) settings set target.source-map /original/compile/path /current/source/path
Debugging iOS applications requires understanding Apple’s unique debug symbol architecture:
.dwo support: macOS uses N_OSO debug maps OR dSYM bundles, not split DWARF--features=oso_prefix_is_pwd and --apple_generate_dsymios-deploy, simctl, and direct LLDB attachment work without Xcode UIFor iOS debugging in Bazel projects, focus on proper dSYM generation and LLDB source mapping.