Srikantharun's Engineering Blog

Technical deep-dives on build systems, toolchains, and cross-platform development

7 March 2026

iOS Debugging Deep Dive: Debug Symbols, IDE Setup, and Crash Troubleshooting

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.


The Apple Debug Symbol Constraint

Why --fission and .dwo Files Don’t Work on macOS

One of the most common misconceptions when moving from Linux to macOS development:

--fission and .dwo files 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’s “Lazy” DWARF Scheme

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:

  1. 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.

  2. 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

The UUID Safeguard

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

IDE Support for iOS Debugging

The Challenge: No Native VSCode iOS Debugging

Unlike Android (which has excellent VSCode support), iOS debugging has traditionally required Xcode. However, several projects now enable VSCode-based iOS debugging.

Option 1: vscode-ios-debug Extension

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"
    }
  ]
}

Option 2: SweetPad Extension

SweetPad provides lightweight CodeLLDB integration for iOS:

Setup steps:

  1. Install CodeLLDB extension
  2. Configure LLDB backend in settings.json:
    {
      "lldb.library": "/Applications/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Versions/A/LLDB"
    }
    
  3. Press F5 to build, launch simulator, and attach debugger

Option 3: CodeLLDB Direct Configuration

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}"
      }
    }
  ]
}

Troubleshooting: Breakpoints Show “Resolved locations: 0”

Per Yrom’s debugging guide, this usually means debug symbol object paths don’t match VSCode source paths. Solutions:

  1. Set correct lldb.library path
  2. Configure source mapping in launch.json
  3. Ensure dSYM is generated and accessible

Bazel iOS Debugging Configuration

Generating dSYM Files with rules_apple

Per 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

Debug Configuration in .bazelrc

# 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

The oso_prefix_is_pwd Feature

This 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.

LLDB Settings for Bazel Builds

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

Crash Symbolication Workflow

Manual Symbolication with atos

When 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:

Xcode Organizer Workflow

  1. Ensure crash files have .crash extension
  2. Verify dSYM UUID matches binary UUID
  3. Import to Xcode Organizer
  4. Xcode auto-symbolicates if dSYM is in Spotlight index

Symbolicator Script (Project-Specific)

The 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:

Troubleshooting Symbolication Failures

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

Command-Line iOS Debugging (Without Xcode UI)

ios-deploy

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

simctl + LLDB Direct Attach

# 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

Remote Debugging Setup

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

Practical Debugging Recipes

Recipe 1: Debug Bazel-Built iOS Simulator App

# 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

Recipe 2: Symbolicate Production Crash

# 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

Recipe 3: VSCode iOS Simulator Debugging

  1. Install extensions:
  2. Configure settings.json:
    {
      "lldb.library": "/Applications/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Versions/A/LLDB"
    }
    
  3. Create .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}/"
       ]
     }
      ]
    }
    
  4. Build, launch simulator, then F5 in VSCode to attach

Common Pitfalls and Solutions

Pitfall 1: “No debug info” After Bazel Build

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

Pitfall 2: dSYM Generated But LLDB Can’t Use It

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

Pitfall 3: Breakpoints Work But Variables Show <unavailable>

Cause: Optimization level too high, variables optimized out.

Solution:

# Build with -O0
bazel build //target -c dbg --copt=-O0

Pitfall 4: Source Files Not Found

Cause: Compilation happened in different directory than debugging.

Solution:

(lldb) settings set target.source-map /original/compile/path /current/source/path

Summary

Debugging iOS applications requires understanding Apple’s unique debug symbol architecture:

  1. No .dwo support: macOS uses N_OSO debug maps OR dSYM bundles, not split DWARF
  2. UUID matching: Binaries and dSYMs are tied by 128-bit UUIDs
  3. Bazel considerations: Use --features=oso_prefix_is_pwd and --apple_generate_dsym
  4. VSCode is possible: Extensions like vscode-ios-debug and SweetPad enable non-Xcode debugging
  5. Command-line tools: ios-deploy, simctl, and direct LLDB attachment work without Xcode UI

For iOS debugging in Bazel projects, focus on proper dSYM generation and LLDB source mapping.


References

Apple Debug Architecture

VSCode iOS Debugging

Crash Symbolication

Bazel Integration

Command-Line Tools

×