--- name: axiom-lldb description: Use when ANY runtime debugging is needed — setting breakpoints, inspecting variables, evaluating expressions, analyzing threads, or reproducing crashes interactively with LLDB license: MIT --- # LLDB Debugging Interactive debugging with LLDB. The debugger freezes time so you can interrogate your running app — inspect variables, evaluate expressions, navigate threads, and understand exactly why something went wrong. **Core insight:** "LLDB is useless" really means "I don't know which command to use for Swift types." This is a knowledge-gap problem, not a tool problem. ## Red Flags — Check This Skill When | Symptom | This Skill Applies | |---------|-------------------| | Need to inspect a variable at runtime | Yes — breakpoint + inspect | | Crash you can reproduce locally | Yes — breakpoint before crash site | | Wrong value at runtime but code looks correct | Yes — step through and inspect | | Need to understand thread state during hang | Yes — pause + thread backtrace | | `po` doesn't work / shows garbage | Yes — Playbook 3 has alternatives | | Crash log analyzed, need to reproduce | Yes — set breakpoints from crash context | | Need to test a fix without rebuilding | Yes — expression evaluation | | Want to break on all exceptions | Yes — exception breakpoints | | App feels slow but responsive | No — use axiom-performance-profiling | | Memory grows over time | No — use axiom-memory-debugging first | | App completely frozen | Maybe — use axiom-hang-diagnostics first, then LLDB for thread inspection | | Crash in production, no local repro | No — use axiom-testflight-triage first | ## LLDB vs Other Tools ```dot digraph tool_selection { "What do you need?" [shape=diamond]; "axiom-testflight-triage" [shape=box]; "axiom-hang-diagnostics" [shape=box]; "axiom-memory-debugging" [shape=box]; "axiom-performance-profiling" [shape=box]; "LLDB (this skill)" [shape=box, style=bold]; "What do you need?" -> "axiom-testflight-triage" [label="Crash log from field,\ncan't reproduce locally"]; "What do you need?" -> "axiom-hang-diagnostics" [label="App frozen,\nneed diagnosis approach"]; "What do you need?" -> "axiom-memory-debugging" [label="Memory growing,\nneed leak pattern"]; "What do you need?" -> "axiom-performance-profiling" [label="Need to measure\nCPU/memory over time"]; "What do you need?" -> "LLDB (this skill)" [label="Need to inspect state\nat a specific moment"]; } ``` **Rule of thumb:** Instruments *measures*. LLDB *inspects*. If you need to understand what's happening at a specific moment in time, use LLDB. If you need to understand trends over time, use Instruments. ## Response Format When helping with LLDB debugging, structure your output as: 1. **Immediate diagnosis** (1-3 bullets, confidence-tagged: HIGH/MEDIUM/LOW) 2. **Commands to run** (numbered, copy-paste ready, with `(lldb)` prefix) 3. **What to look for** (command → expected output → interpretation) 4. **Likely root causes** (ranked by probability) 5. **Next breakpoint plan** (catch it earlier next time) 6. **If no debugger attached** (crash-log-only fallback path) --- ## Playbook 1: Crash Triage **Goal:** Understand why the app crashed, starting from the stop point. ### Step 1: Read the Stop Reason When the debugger stops, the first thing to check: ``` (lldb) thread info ``` This shows the stop reason. Common stop reasons: | Stop Reason | Meaning | Next Step | |-------------|---------|-----------| | `EXC_BAD_ACCESS (SIGSEGV)` | Accessed invalid memory (null pointer, dangling reference) | Check the address — `0x0` to `0x10` = nil dereference | | `EXC_BAD_ACCESS (SIGBUS)` | Misaligned or invalid address | Usually C interop or unsafe pointer issue | | `EXC_BREAKPOINT (SIGTRAP)` | Hit a trap — Swift runtime check failed | Check for `fatalError()`, `preconditionFailure()`, force-unwrap of nil, array out of bounds | | `EXC_CRASH (SIGABRT)` | Deliberate abort — assertion or uncaught exception | Look at "Application Specific Information" for the message | | `breakpoint` | Your breakpoint was hit | Normal — inspect state | ### Step 2: Get the Backtrace ``` (lldb) bt ``` Read top-to-bottom. Find the first frame in YOUR code (not system frameworks). That's where to start investigating. ``` (lldb) bt 10 ``` Limit to 10 frames if the full trace is noisy. ### Step 3: Navigate to Your Frame ``` (lldb) frame select 3 ``` Jump to frame 3 (or whichever frame is in your code). ### Step 4: Inspect State ``` (lldb) v (lldb) v self.someProperty (lldb) v localVariable ``` Use `v` (not `po`) for reliable Swift value inspection. See Playbook 3 for details. ### Step 5: Classify and Fix | Exception Type | Typical Cause | Fix Pattern | |----------------|---------------|-------------| | `EXC_BAD_ACCESS` at low address | Force-unwrap nil optional | `guard let` / `if let` | | `EXC_BAD_ACCESS` at high address | Use-after-free / dangling pointer | Check object lifetime, `[weak self]` | | `EXC_BREAKPOINT` | Swift runtime trap (bounds, unwrap, precondition) | Fix the violated precondition | | `SIGABRT` | Uncaught ObjC exception or `fatalError()` | Read the exception message, fix the root cause | ### Step 6: Set a Conditional Breakpoint to Catch It Earlier ``` (lldb) breakpoint set -f MyFile.swift -l 42 -c "value == nil" ``` This breaks only when `value` is nil at line 42 — catches the problem before the crash. --- ## Playbook 2: Hang/Deadlock Diagnosis **Goal:** Understand why the app is frozen by inspecting all thread states. ### Step 1: Pause the App If the app is hung, press the pause button in Xcode (⌃⌘Y) or: ``` (lldb) process interrupt ``` ### Step 2: Get All Thread Backtraces ``` (lldb) thread backtrace all ``` Or the shorthand: ``` (lldb) bt all ``` ### Step 3: Classify Thread States Look at Thread 0 (main thread) — it processes all UI events. If it's blocked, the app is frozen. **Main thread blocked on synchronous wait:** ``` frame #0: libsystem_kernel.dylib`__psynch_mutexwait frame #1: libsystem_pthread.dylib`_pthread_mutex_firstfit_lock_wait ... frame #5: MyApp`ViewController.viewDidLoad() ``` Translation: Main thread is waiting for a mutex lock. Something else holds it. **Main thread blocked on dispatch_sync:** ``` frame #0: libdispatch.dylib`_dispatch_sync_f_slow ... frame #3: MyApp`DataManager.fetchData() ``` Translation: `DispatchQueue.main.sync` called from background → classic deadlock. **Main thread busy (CPU-bound):** ``` frame #0: MyApp`ImageProcessor.processAllImages() frame #1: MyApp`ViewController.viewDidLoad() ``` Translation: Expensive work on main thread. Move to background. ### Step 4: Check for Deadlocks If two threads are both waiting on something the other holds: ``` (lldb) thread list ``` Look for multiple threads with state `waiting` that reference each other's locks. ### Step 5: Inspect Specific Thread ``` (lldb) thread select 3 (lldb) bt (lldb) v ``` Switch to another thread to inspect its state. **Cross-reference:** For fix patterns once you've identified the hang cause → `/skill axiom-hang-diagnostics` --- ## Playbook 3: Swift Value Inspection **This is the core value of this skill.** Most developers abandon LLDB because `po` doesn't work reliably with Swift types. Here's what actually works. ### The Four Print Commands | Command | Full Form | What It Does | Best For | |---------|-----------|--------------|----------| | `v` | `frame variable` | Reads memory directly, no compilation | Swift structs, enums, locals — **your default** | | `p` | `expression` (with formatter) | Compiles expression, shows formatted result | Computed properties, function calls | | `po` | `expression --object-description` | Calls `debugDescription` | Classes with `CustomDebugStringConvertible` | | `expr` | `expression` | Evaluates arbitrary code | Calling methods, modifying state | ### When to Use Each **Start with `v`** — it's fastest and most reliable for stored properties: ``` (lldb) v self.userName (lldb) v self.items[0] (lldb) v localStruct ``` `v` works by reading memory directly. It doesn't compile anything, so it can't fail due to expression compilation errors. **`v` limitation:** It only reads stored properties — computed properties, `lazy var` (before first access), and property wrapper projected values (`$binding`) won't show meaningful values. If a field looks wrong or missing with `v`, try `p` instead. **Use `p` when `v` can't reach it:** ``` (lldb) p self.computedProperty (lldb) p self.items.count (lldb) p someFunction() ``` `p` compiles and executes the expression. Needed for computed properties and function calls. **Use `po` for class descriptions:** ``` (lldb) po myObject (lldb) po error (lldb) po notification ``` `po` calls `debugDescription` on the result. Best for objects that have meaningful descriptions (NSError, Notification, etc.). ### The "LLDB Is Broken" Moments | What You See | Why | Fix | |--------------|-----|-----| | `` | `po` failed; variable hasn't been populated by optimizer | Use `v` instead | | `expression failed to parse, unknown type name` | Swift expression parser can't resolve the type | Try `expr -l objc -- (id)0x12345` for ObjC objects, or use `v` | | `` | Compiler optimized it out (Release build) | Rebuild with Debug, per-file `-Onone`, or `register read` as last resort | | `error: Couldn't apply expression side effects` | Expression had side effects LLDB couldn't reverse | Try a simpler expression; avoid mutating state | | `po` shows memory address instead of value | Object doesn't conform to `CustomDebugStringConvertible` | Use `v` for raw value, or implement the protocol | | `cannot find 'self' in scope` | Breakpoint is in a context without `self` (static, closure) | Use `v` with the explicit variable name | | `p` shows `$R0 = ...` but `po` crashes | Different compilation paths | Use `p` when it works; `po` adds an extra description step that can fail | ### Inspecting Optionals ``` (lldb) v optionalValue ``` Shows: `(String?) some = "hello"` or `(String?) none` Don't use `po optionalValue` — it may show just `Optional("hello")` which is less useful. ### Inspecting Collections ``` (lldb) v myArray (lldb) v myArray[2] (lldb) v myDict ``` For large collections, limit output: ``` (lldb) p Array(myArray.prefix(5)) ``` ### Inspecting SwiftUI State SwiftUI `@State` is backed by stored properties with underscore prefix: ``` (lldb) v self._isPresented (lldb) v self._items ``` For `@Observable` models: ``` (lldb) v self.viewModel.propertyName ``` **Diagnosing "view doesn't update":** If a property changes (confirmed with `v`) but the SwiftUI view doesn't re-render, check which thread the mutation happens on with `bt`. `@Observable` mutations must happen on `@MainActor` for SwiftUI to observe them — mutations on a background actor won't trigger view updates. Use `Self._printChanges()` inside a view body to see which property triggered (or didn't trigger) a re-render: ``` (lldb) expr Self._printChanges() ``` For the full observation diagnostic tree → `/skill axiom-swiftui-debugging` ### Inspecting Actors Actor state is best inspected with `v`, which reads memory directly without isolation concerns: ``` (lldb) v actor ``` Shows all stored properties. This works because LLDB pauses the entire process — you can read any memory regardless of actor isolation (which is a compile-time concept). ### Modifying Values at Runtime ``` (lldb) expr self.debugFlag = true (lldb) expr myArray.append("test") (lldb) expr self.view.backgroundColor = UIColor.red ``` Modify values without rebuilding. Useful for testing theories. ### Referencing Previous Results LLDB assigns result variables (`$R0`, `$R1`, etc.): ``` (lldb) p someValue $R0 = 42 (lldb) p $R0 + 10 $R1 = 52 ``` --- ## Playbook 4: Breakpoint Strategies ### Source Breakpoints (Basic) ``` (lldb) breakpoint set -f ViewController.swift -l 42 (lldb) b ViewController.swift:42 ``` Short form `b` works for simple cases. ### Conditional Breakpoints Break only when a condition is true: ``` (lldb) breakpoint set -f MyFile.swift -l 42 -c "index > 100" (lldb) breakpoint set -f MyFile.swift -l 42 -c "name == \"test\"" ``` **Iteration-based:** Break after N hits: ``` (lldb) breakpoint set -f MyFile.swift -l 42 -i 50 ``` Ignores the first 50 hits, then breaks. ### Logpoints (Action + Auto-Continue) Log without stopping — like a print statement but no rebuild needed: ``` (lldb) breakpoint set -f MyFile.swift -l 42 (lldb) breakpoint command add 1 > v self.value > continue > DONE ``` Or in Xcode: Edit breakpoint → Add Action → "Log Message" → use `@self.value@` token syntax → Check "Automatically continue" ### Symbolic Breakpoints Break on ANY call to a method by name: ``` (lldb) breakpoint set -n viewDidLoad (lldb) breakpoint set -n "MyClass.myMethod" ``` Break on all ObjC messages to a selector: ``` (lldb) breakpoint set -S "layoutSubviews" ``` ### Exception Breakpoints **Swift errors (break on throw):** ``` (lldb) breakpoint set -E swift ``` **Objective-C exceptions (break on throw):** ``` (lldb) breakpoint set -E objc ``` **In Xcode:** Breakpoint Navigator → + → Swift Error Breakpoint / Exception Breakpoint This is the single most useful breakpoint for crash debugging. It stops at the throw site instead of the catch/crash site. ### Watchpoints Break when a variable's value changes: ``` (lldb) watchpoint set variable self.count (lldb) watchpoint set variable -w read_write myGlobal ``` Watchpoints are hardware-backed — limited to ~4 per process but very fast. ### One-Shot Breakpoints Break once, then auto-delete: ``` (lldb) breakpoint set -f MyFile.swift -l 42 -o ``` ### Managing Breakpoints ``` (lldb) breakpoint list (lldb) breakpoint disable 3 (lldb) breakpoint enable 3 (lldb) breakpoint delete 3 (lldb) breakpoint delete ``` --- ## Playbook 5: Async/Concurrency Debugging ### Identifying Async Frames Swift concurrency backtraces are noisy — expect `swift_task_switch`, `_dispatch_call_block_and_release`, and executor internals mixed in with your code. Don't be discouraged by 40+ frames of runtime noise. Focus on frames from YOUR module. In Swift concurrency backtraces, look for `swift-task` frames: ``` Thread 3: frame #0: MyApp`MyActor.doWork() frame #1: swift_task_switch frame #2: MyApp`closure #1 in ViewController.loadData() ``` The `swift_task_switch` frame indicates an async suspension point. Your code frames are the ones prefixed with your module name (`MyApp` above). ### Inspecting Task State ``` (lldb) thread backtrace all ``` Look for threads with `swift_task` in their frames. Each represents an active Swift task. ### Actor-Isolated Code When stopped inside an actor: ``` (lldb) v self ``` Shows all actor state. This works because LLDB pauses the entire process — actor isolation is a compile-time concept, not a runtime lock (for default actors). ### Task Group Inspection When debugging task groups, break inside the group closure and inspect: ``` (lldb) v (lldb) bt ``` Each child task runs on its own thread. Use `bt all` to see them. **Cross-reference:** For Swift concurrency patterns and fix strategies → `/skill axiom-swift-concurrency`. For profiling async performance → `/skill axiom-concurrency-profiling` --- ## Pressure Scenarios ### Scenario 1: "Release-Only Crash — LLDB Is Useless in Release" **Situation:** Crash happens in Release builds but not Debug. Team says "we can't debug it." **Why this fails:** Release optimizations change timing, memory layout, and can eliminate variables — making the crash non-reproducible in Debug. **Correct approach:** 1. Build with Debug configuration but Release-like settings: - Optimization Level: `-O` (not `-Onone`) - Still include debug symbols (`DEBUG_INFORMATION_FORMAT = dwarf-with-dsym`) 2. Enable Address Sanitizer (`-fsanitize=address`) — catches memory errors with 2-3x overhead 3. Use the crash report to set breakpoints at the crash site 4. Set exception breakpoints to catch the error before the crash: ``` (lldb) breakpoint set -E swift (lldb) breakpoint set -E objc ``` 5. If variable shows ``, reduce optimization for that one file: - Build Settings → Per-file flags → `-Onone` for the specific file 6. Last resort — read register values directly (variables live in registers before being optimized out): ``` (lldb) register read (lldb) register read x0 x1 x2 ``` On ARM64: `x0` = self, `x1`-`x7` = first 7 arguments. Check `/skill axiom-lldb-ref` Part 1 for details. ### Scenario 2: "Just Add Print Statements" **Situation:** Developer adds `print()` calls to debug, rebuilds, runs, reads console. Repeat. **Why this fails:** Each print-debug cycle costs 3-5 minutes (edit → build → run → navigate to state → read output). An LLDB breakpoint costs 30 seconds. **Correct approach:** 1. Set a breakpoint at the line you'd add a `print()`: ``` (lldb) b MyFile.swift:42 ``` 2. Add a logpoint for "print-like" behavior without rebuilding: - Edit breakpoint → Add Action → Log Message → Check "Auto continue" 3. Inspect variables directly: `v self.someValue` 4. Modify variables at runtime to test theories: `expr self.debugMode = true` 5. **One breakpoint session replaces 5-10 print-debug cycles.** **Time comparison** (typical control-flow debugging): | Approach | Per investigation | 5 variables | |----------|-------------------|-------------| | print() statements | 3-5 min (build + run) | 15-25 min | | LLDB breakpoint | 30 sec (set + inspect) | 2.5 min | **Exception:** In tight loops (thousands of hits/sec), logpoints add per-hit overhead. Use `-i` to skip to the iteration you care about, or use a temporary `print()` for that specific loop. ### Scenario 3: "po Doesn't Work So LLDB Is Broken" **Situation:** Developer types `po myStruct` and gets garbage. Concludes LLDB is broken for Swift. Goes back to print debugging. **This is the #1 reason developers abandon LLDB.** **Why `po` fails with Swift structs:** `po` calls `debugDescription` which requires compiling an expression in the debugger context. For Swift structs, this compilation often fails due to missing type metadata, generics, or module resolution issues. **Correct approach:** 1. Use `v` instead of `po` — reads memory directly, no compilation: ``` (lldb) v myStruct (lldb) v myStruct.propertyName ``` 2. Use `p` for computed properties: ``` (lldb) p myStruct.computedValue ``` 3. Use `po` only for classes with `CustomDebugStringConvertible` 4. If `p` also fails, try specifying the language: ``` (lldb) expr -l objc -- (id)0x12345 ``` 5. If everything fails, `v self` always works inside a method. --- ## Anti-Patterns | Anti-Pattern | Why It's Wrong | Better Alternative | |---|---|---| | `po` everything | Fails for Swift structs, enums, optionals | `v` for values, `po` only for classes | | Print-debug cycles | 3-5 min per cycle vs 30 sec breakpoint | Breakpoints with logpoint actions | | "LLDB doesn't work with Swift" | It does — wrong command choice | `v` is designed for Swift values | | Ignoring backtraces | Jumping to guesses instead of reading the trace | `bt` first, then navigate frames | | Conditional breakpoints on every hit | Slows execution if condition is expensive | Use `-i` (ignore count) when possible | | Debugging optimized (Release) builds | Variables missing, code reordered | Debug configuration, or per-file `-Onone` | | Force-continuing past exceptions | Hides the real error | Fix the exception, don't suppress it | | No exception breakpoints set | Crashes land in system code, not throw site | Always add Swift Error + ObjC Exception breakpoints | ## Debugging Checklist Before starting a debug session: - [ ] Debug build configuration (not Release) - [ ] Exception breakpoints enabled (Swift Error + ObjC Exception) - [ ] Breakpoint set before suspected problem area - [ ] Know which command to use: `v` for values, `p` for computed, `po` for descriptions During debug session: - [ ] Read stop reason (`thread info`) before anything else - [ ] Get backtrace (`bt`) — find your frame - [ ] Navigate to your frame (`frame select N`) - [ ] Inspect relevant state (`v self`, `v localVar`) - [ ] Understand the cause before writing any fix After finding the issue: - [ ] Set conditional breakpoint to catch recurrence - [ ] Consider adding assertion/precondition for this case - [ ] Remove temporary breakpoints ## Resources **WWDC**: 2019-429, 2018-412, 2022-110370 **Docs**: /xcode/stepping-through-code-and-inspecting-variables-to-isolate-bugs, /xcode/setting-breakpoints-to-pause-your-running-app, /xcode/diagnosing-memory-thread-and-crash-issues-early **Skills**: axiom-lldb-ref, axiom-testflight-triage, axiom-hang-diagnostics, axiom-memory-debugging, axiom-swift-concurrency, axiom-concurrency-profiling