LLDB Debugging: Commanding the Debugger Behind Xcode's UI

Learn to drive LLDB directly from the debug console — breakpoints, conditional breakpoints, watchpoints, stepping, expression evaluation, and the core inspection commands (po, p, bt, frame, thread) — so that debugging a running Objective-C program becomes a matter of deliberate commands rather than guesswork.

Every time a breakpoint stops a program in Xcode, LLDB is the thing that actually stopped it. Xcode's clickable gutter breakpoints, its variables pane, and its step buttons are a graphical skin over a full command-line debugger, and that debugger's console at the bottom of the Xcode window accepts LLDB commands directly. Learning to use LLDB from that console — rather than relying solely on the mouse — turns debugging from a slow, click-driven activity into something fast, precise, and scriptable: conditional breakpoints that only fire after the tenth iteration of a loop, watchpoints that catch the exact line that corrupts a value, and expression evaluation that lets you test a fix without recompiling. This lesson works through LLDB's vocabulary in the order you're likely to reach for it: stopping the program, inspecting where it stopped, and moving it forward again.

Breakpoints: stopping execution at a specific line

A breakpoint is an instruction to LLDB: when execution reaches this line, pause the program and hand control back to the developer. Clicking in Xcode's editor gutter next to a line number sets exactly this kind of breakpoint, and under the hood it is implemented as an LLDB breakpoint attached to that source location. Once execution reaches it, the entire program freezes mid-instruction — not just the current function, but every thread — and the debug console becomes live, ready to accept commands.

Breakpoints can also be set directly from the LLDB command line with breakpoint set --file ViewController.m --line 42, or more commonly by its shorthand b ViewController.m:42. This matters once you're comfortable enough with LLDB to stop reaching for the mouse: setting a breakpoint by typing its location is often faster than scrolling to find the right line in the editor, especially in a large file or when working across several files in one debugging session.

A stopped breakpoint is not a crash and not the end of the program — it is a pause. Everything about the program's state at that instant is still there to be examined: local variables, the call stack, the values of instance variables on self, the contents of any object reachable from those variables. The rest of this lesson is essentially about what you can do while paused at a breakpoint, and how to control which lines actually trigger that pause in the first place.

Conditional breakpoints: pausing only when it matters

An ordinary breakpoint pauses every single time execution reaches its line, which is fine for a line that runs once but useless for a line inside a loop that runs a thousand times when the bug only shows up on iteration eight hundred. A conditional breakpoint solves this by attaching a boolean expression to the breakpoint: LLDB still notices every time execution reaches the line, but only actually pauses the program when that expression evaluates to true.

In Xcode, right-clicking an existing breakpoint and choosing 'Edit Breakpoint' exposes a Condition field where you type an ordinary Objective-C or C boolean expression — something like index == 799 or [user.name isEqualToString:@"corrupted"]. From the LLDB console the equivalent is breakpoint modify --condition 'index == 799' 1 (where 1 is the breakpoint's number, shown when it was created or listed with breakpoint list), or the condition can be supplied at creation time.

This is one of the single most valuable techniques in this lesson, precisely because of the class of bug it targets: intermittent, hard-to-reproduce failures that only manifest after many iterations, or only for a specific input buried deep in a loop or a large data set. Stepping through eight hundred iterations by hand to reach the interesting one is not a realistic option; a conditional breakpoint reaches it in one run, automatically, every time.

Watchpoints: pausing when a memory location changes

A breakpoint pauses when execution reaches a line. A watchpoint pauses when a specific memory location's value changes — regardless of which line, or which function, or even which file does the changing. This is a fundamentally different way of asking a question of the running program: not 'stop when this code runs' but 'stop the instant this particular piece of data is written to.'

Watchpoints are set on an address or an expression that resolves to one, typically from an existing stopped state: watchpoint set variable someObject->_count or watchpoint set expression -- &someVariable. Once set, the program runs freely — no line-by-line slowdown — until something, anywhere in the program, writes a new value into that exact memory location, at which point LLDB pauses immediately, at the exact instruction that performed the write.

This is the right tool for a specific and often maddening category of bug: a value that ends up wrong, but where the code that's supposed to set it looks correct, and the actual culprit is some other, unexpected piece of code writing to the same memory — an over-aggressive reset function, a mistaken index into an array that lands on the wrong element, a use-after-free scribbling over memory that's been reused for something else. A breakpoint on the 'obvious' line that sets the value won't catch this, because the obvious line isn't the one doing the damage; a watchpoint on the value itself catches it no matter where the write comes from.

Stepping: over, into, and out

Once paused, three related commands move execution forward by small, controlled amounts, and choosing the right one is about deciding how much detail you want to see. Step over (next or n, or Xcode's step-over button) executes the current line as a single unit — if that line calls a function, the function runs to completion in the background and control returns to the next line in the current function, without ever stopping inside the callee.

Step into (step or s) does the opposite: if the current line calls a function, execution pauses at the very first line inside that function, letting you follow the call down into its implementation. This is what you want when the bug might be inside the function being called, and step over is what you want when you already trust that function and just want to keep moving through the current one.

Step out (finish) runs the remainder of the current function to completion and pauses immediately after it returns, back in the calling function, with the return value (if any) available for inspection. This is the natural move once you've stepped into a function, confirmed it isn't where the bug is, and want to get back up to the caller without stepping through every remaining line one at a time.

The practical discipline is to default to step over for code you trust — most of Foundation, most of your own already-verified utility methods — and reach for step into only at the specific call you suspect, then step out as soon as you've learned what you needed from inside it. Stepping into everything indiscriminately turns a five-minute debugging session into an hour spent wandering through unrelated framework internals.

po and p: the two ways to print a value

po — print object — is the single most commonly typed LLDB command in day-to-day Objective-C debugging. Given an expression that evaluates to an object, po someArray invokes that object's -description method and prints the readable result: an NSArray prints its elements, a custom model object prints whatever its own -description override produces (or a generic, less useful default if it hasn't overridden it), and NSDictionary prints its key-value pairs formatted for reading.

p — print — is the more general command: it evaluates an expression and prints its raw value, without invoking -description. For a primitive — an int, a float, a BOOL — p is what you want, since there's no object and no -description to call in the first place. For an object, p prints something closer to the raw pointer value and static type rather than a human-readable summary, which is occasionally useful (confirming two variables point at the identical object, for instance) but far less useful than po for actually understanding what's inside the object.

The rule of thumb worth internalizing: reach for po first for anything object-typed, since it's almost always what you actually want to see, and reach for p for primitives or for the rarer cases where you specifically want the raw value rather than a description. Getting this backwards — typing po on a plain int — produces an error or nonsense output, since there's no object and no -description to invoke.

bt, frame, and thread: navigating the call stack

bt — backtrace — prints the full call stack at the current pause point: every function call that led to where execution is right now, from the innermost frame (where you're stopped) outward to wherever the chain started, often all the way back to the operating system's own entry point into your app. This is usually the very first command worth typing after any unexpected stop, especially a crash, because it answers the most basic question — how did execution even get here.

Each entry in that backtrace is a frame, and frame lets you move between them and inspect one specifically. frame select 2 (or its shorthand f 2) switches LLDB's current context to the third frame up the stack, and once selected, commands like po and p operate against that frame's local variables rather than the innermost one — letting you inspect what the calling function's own local variables looked like at the moment it made the call that led down to where you're actually stopped.

thread addresses the multi-threaded case: when a paused app has more than one thread running, thread list shows all of them, and thread select 3 switches which thread's stack bt and frame operate against. This matters constantly in real Objective-C apps, where background work on a dispatch queue and UI work on the main thread are both live at once, and a crash or a breakpoint on a background thread needs its own backtrace and its own frame inspection, entirely separate from whatever the main thread happens to be doing at that same instant.

Expression evaluation and memory inspection

Beyond simply printing existing values, LLDB can evaluate arbitrary expressions live, using the expression command (usually abbreviated further, since po and p are themselves shorthand forms of expression with different formatting flags). This means you can call a method, mutate a variable, or even construct a brand-new object, all without recompiling: expression self.count = 5 changes a live property's value mid-debug, and expression [array addObject:@"test"] mutates a live array to see how the rest of the code reacts.

This capability turns LLDB from a passive observation tool into an interactive one. Suspecting that a particular value is the cause of a downstream bug can be tested directly — pause at the point where the value is used, override it with expression, and continue, watching whether the suspected bug disappears — without the slower cycle of editing source, rebuilding, and reproducing the failure from scratch.

Memory inspection goes a layer lower still: memory read (or x) dumps raw bytes at a given address, useful when you need to see exactly what's stored at a location rather than how Objective-C's type system chooses to interpret it — confirming a buffer's actual contents, checking whether memory has been overwritten with an unexpected pattern, or examining an object's raw layout when something about its higher-level behavior doesn't add up.

Symbolic debugging and why symbols matter

Everything in this lesson — readable function names in a backtrace, being able to set a breakpoint by typing ViewController.m:42 rather than a raw address, LLDB resolving self.count to an actual memory offset — depends on debug symbols: the mapping, produced by the compiler and linker and covered in the earlier Build System lesson, between raw machine addresses and the human-meaningful names and source locations the original code used.

This is what symbolic debugging means: LLDB resolving addresses back into names you recognize, rather than leaving you staring at a bare hexadecimal address with no indication of what function it's actually inside. A build configured to strip symbols — common, and correct, for a release build shipped to users, exactly as the Build System lesson described — removes exactly this mapping, and debugging that build looks dramatically different: a backtrace shows raw addresses and, at best, the name of the nearest exported symbol, with no line numbers and no local variable names for LLDB to display.

The practical consequence is straightforward: debug builds keep full symbol information specifically so that a session like the one this lesson describes works cleanly, and any time a backtrace looks unreadable — addresses instead of function names, no source lines, po failing to resolve a variable — the very first thing worth checking is whether you're actually debugging a build that has symbols at all, rather than assuming LLDB itself is broken.

Code Examples

This sets a breakpoint at line 42 and then attaches a condition so it only actually pauses execution once index equals 799, rather than pausing on every one of potentially hundreds of earlier loop iterations. continue resumes the program, which will now run freely and stop only when the condition is met.

bt shows the call stack: processItem: was called from viewDidLoad. frame select 1 moves up to that calling frame, and po self.items prints a readable description of the items array as it looked at that point. p index prints the raw integer value of index, since it's a primitive and po would be the wrong command for it.

Suppose count keeps ending up wrong, but every place in the code that's supposed to change it looks correct. A watchpoint on the underlying instance variable pauses execution the moment any write touches that memory, wherever in the program it happens, which is exactly how you'd catch an unexpected writer that a breakpoint on the 'obvious' lines would never trigger.

(lldb) breakpoint set --file ViewController.m --line 42
Breakpoint 1: where = MyApp`-[ViewController processItem:] + 88 at ViewController.m:42
(lldb) breakpoint modify --condition 'index == 799' 1
(lldb) continue
(lldb) bt
* frame #0: 0x0001023a MyApp`-[ViewController processItem:] at ViewController.m:42
  frame #1: 0x00010188 MyApp`-[ViewController viewDidLoad] at ViewController.m:20
  frame #2: 0x00010040 UIKitCore`...
(lldb) frame select 1
(lldb) po self.items
<__NSArrayI 0x600002a1c0a0>(
  item1,
  item2,
  item3
)
(lldb) p index
(int) 799
@interface Counter : NSObject
@property (nonatomic) NSInteger count;
@end

// In the debugger, paused with a Counter instance in scope as 'counter':
// (lldb) watchpoint set variable counter->_count
// (lldb) continue
//
// Some time later, LLDB stops automatically the instant _count changes,
// anywhere in the program -- even in code far from where you expected it.

The LLDB debugging workflow

The core LLDB loop: a breakpoint pauses execution, and from that pause you inspect the frame, the variables in it, and the objects those variables refer to, before continuing execution — repeating as many times as needed.

Debugging a release build with stripped symbols and concluding LLDB is broken because the backtrace shows only hexadecimal addresses with no function names.

Recognize that symbolic debugging depends entirely on debug symbols; a stripped release build has none, so debug against a Debug configuration build (or a symbolicated build with its matching dSYM available) when you need readable backtraces.

Symbol stripping is correct and intentional for shipped builds, but it removes exactly the information LLDB needs to show function names, line numbers, and variable names — an unreadable backtrace is expected behavior for that build, not a debugger failure.

Typing po on a plain int variable and being confused by the resulting error or garbage output.

Use p for primitives (int, float, BOOL, and similar non-object values) and reserve po for expressions that evaluate to an actual Objective-C object.

po works by invoking -description on the result, which only makes sense for objects; a primitive has no -description to call, so po either errors or misinterprets the raw bits as a pointer.

Stepping into every single function call while chasing a bug, including deep into Foundation and UIKit internals that have nothing to do with the actual problem.

Default to step over for trusted code, and reserve step into specifically for the call you actually suspect is where the bug lives, stepping back out as soon as you've learned what you needed.

Indiscriminate step into wastes enormous time wandering through framework internals that were never the source of the bug, and can leave you lost several layers deep with no clear way back to where you actually wanted to be.

Setting an ordinary unconditional breakpoint inside a loop that runs thousands of times, then clicking Continue over and over trying to reach the one problematic iteration.

Attach a condition to the breakpoint (breakpoint modify --condition, or Xcode's Condition field) so it only actually pauses at the iteration that matters.

A conditional breakpoint reaches the interesting iteration in a single run; manually continuing through an unconditional breakpoint thousands of times is slow, error-prone, and easy to overshoot past the exact moment you needed to inspect.

What distinguishes a watchpoint from an ordinary breakpoint? A. A watchpoint pauses when a specific memory location's value changes, regardless of which line performs the write, while a breakpoint pauses when execution reaches a specific line B. A watchpoint only works on the main thread, while a breakpoint works on any thread C. A watchpoint is a synonym for a conditional breakpoint D. A watchpoint pauses execution before the line runs, while a breakpoint pauses after

A. A watchpoint pauses when a specific memory location's value changes, regardless of which line performs the write, while a breakpoint pauses when execution reaches a specific line — A breakpoint is tied to a source location; a watchpoint is tied to a memory location and triggers on any write to it, no matter where in the program that write originates — which is exactly what makes it useful for tracking down unexpected modification.

Which LLDB command should you use to print an NSDictionary in a readable, formatted way that shows its keys and values? A. frame B. p C. po D. bt

C. po — po (print object) invokes -description on the object, which for NSDictionary produces a readable, formatted listing of its key-value pairs — exactly what you want when inspecting an object rather than a primitive.

You are paused inside a deeply nested helper function and want to return to the function that called it, without stepping through every remaining line of the helper one at a time. Which command should you use? A. step (step into) B. next (step over) C. finish (step out) D. continue

C. finish (step out) — Step out (finish) runs the remainder of the current function to completion and pauses right after it returns to its caller, which is exactly the move for leaving a function you've already learned what you needed from.

Why does a conditional breakpoint help specifically with bugs that only appear after many loop iterations? A. It runs the loop faster than an unconditional breakpoint would B. It only pauses execution when its attached expression evaluates to true, letting the program run freely until the specific iteration of interest is reached C. It automatically fixes off-by-one errors in loops D. It disables all other breakpoints while the loop runs

B. It only pauses execution when its attached expression evaluates to true, letting the program run freely until the specific iteration of interest is reached — Without a condition, the breakpoint would pause on every iteration, requiring you to manually continue through potentially hundreds of uninteresting stops; the condition lets LLDB skip straight to the one that matters.

What does symbolic debugging refer to, and why does it depend on debug symbols? A. LLDB's ability to evaluate arithmetic symbols like + and - in expressions, which requires no special build configuration B. LLDB resolving raw memory addresses back into human-readable function names, source lines, and variable names, which requires the compiler/linker to have preserved that mapping rather than stripped it C. A feature only available when debugging Swift code, never Objective-C D. The use of $ prefixed convenience variables in the LLDB console

B. LLDB resolving raw memory addresses back into human-readable function names, source lines, and variable names, which requires the compiler/linker to have preserved that mapping rather than stripped it — Debug symbols are the mapping between machine addresses and the original source-level names; without them (as in a stripped release build), LLDB can only show raw addresses instead of readable function names and line numbers.

What is the correct order of commands to inspect the call stack, move to the frame that called the current function, and then print a local variable from that frame? A. po first, then bt, then frame select B. bt to see the stack, frame select N to move to the desired frame, then po or p to print a value in that frame's context C. frame select first, then bt, then continue D. watchpoint set, then bt, then finish

B. bt to see the stack, frame select N to move to the desired frame, then po or p to print a value in that frame's context — bt shows all frames and their numbers; frame select N switches LLDB's active context to frame N; only after that does po/p operate against that specific frame's local variables rather than the innermost frame's.

breakpoint
   ↓
pause
   ↓
inspect frame
   ↓
inspect variables
   ↓
inspect object
   ↓
continue