As iOS developers, we often find ourselves spending a good amount of time working with the debugger. When an app misbehaves, the LLDB console becomes an essential companion to quickly inspect objects, evaluate expressions, and step through complex code. While most of us are familiar with common commands like p and po, LLDB offers a broader toolbox that many developers seldom use. In this post, we’ll explore both the popular and the lesser-known commands, and demystify their usage so that you can streamline your debugging sessions.

Why Mastering LLDB Commands Matters

When facing a tricky bug, the difference between flailing and efficiently diagnosing an issue often comes down to how well you know your tools. LLDB commands can help you:

  • Inspect variables and objects’ state in real-time.
  • Quickly evaluate and mutate expressions on the fly.
  • Set conditional breakpoints that automatically evaluate logic before pausing execution.
  • Step through code at a finer granularity, even at the disassembly level.

Knowing these commands grants you more control over your debugging workflow, allowing you to solve issues faster and with greater confidence.

The Essentials: p and po

p

  • What it does: The p (short for print) command is the bread-and-butter of LLDB. It evaluates an expression in the current context and prints the result.
  • Usage:
    (lldb) p myVariable
    
  • Notes: By default, p uses the LLDB’s internal formatting. For basic types (like int, double), you’ll get a straightforward printed value. For more complex Objective-C or Swift objects, the output might be a pointer or a memory address rather than a nicely formatted description.

po

  • What it does: The po (print object) command is a close cousin to p, but it calls the object’s description or debugDescription method (in Objective-C) or CustomStringConvertible conformance (in Swift) to produce more human-readable output.
  • Usage:
    (lldb) po myNSString
    
  • Notes: po is particularly useful for printing strings, arrays, dictionaries, or custom types that have descriptive representations. If you find p giving you unintelligible memory addresses, try po next.

Diving Deeper: expr (Expression Evaluations)

expr

  • What it does: The expr command (shortened as e) allows you to evaluate arbitrary Swift or Objective-C expressions at runtime. This means you can call methods, instantiate new objects, and even mutate state without stopping the program and recompiling.
  • Usage:
    (lldb) expr myArray.append("New Element")
    (lldb) expr let newValue = myStruct.transform()
    
  • Notes: This is incredibly powerful and can sometimes serve as a quick fix during debugging. However, be mindful that modifying program state through expr can affect the subsequent behavior of the app, potentially masking or altering the original bug.

Variable Inspection: frame variable (or fr v)

frame variable

  • What it does: frame variable inspects variables in the current stack frame. Unlike p, it doesn’t require parentheses or fully qualifying variable names.
  • Usage:
    (lldb) frame variable
    (lldb) frame variable myVariable
    (lldb) fr v
    
  • Notes: Calling frame variable with no arguments lists all variables in the current frame. Adding a variable name inspects just that variable. You can also filter specific members or nest deeper into structures.

Understanding the Call Stack: bt (Backtrace)

bt

  • What it does: bt (short for “backtrace”) displays the call stack at the point where the program is currently paused.
  • Usage:
    (lldb) bt
    
  • Notes: A backtrace gives you a clear picture of the sequence of function calls that led to the current breakpoint. This is essential for understanding context and diagnosing logical errors.

thread

  • What it does: thread commands help you switch between threads in a multithreaded application.
  • Usage:
    (lldb) thread list         # Lists all threads
    (lldb) thread select 2     # Switches to thread 2
    (lldb) thread backtrace    # Shows backtrace for the current thread
    
  • Notes: Understanding which thread you are inspecting is crucial, especially when dealing with concurrency bugs.

frame

  • What it does: frame commands let you inspect and switch between frames (function calls) within a particular thread.
  • Usage:
    (lldb) frame select 0      # Jump to frame 0 (top of the stack)
    (lldb) frame info          # Display detailed info about the current frame
    
  • Notes: Each frame represents a function in the call stack. Diving into different frames helps you inspect the state at various points in the call chain.

Exploring Memory: memory and x

memory

  • What it does: memory commands allow you to inspect raw memory addresses, useful for debugging low-level issues, pointer corruption, or C-level data structures.
  • Usage:
    (lldb) memory read 0x7ffeefbff6c0
    (lldb) memory write 0x7ffeefbff6c0 0xFF
    
  • Notes: memory read and memory write can be dangerous if you’re not careful. They’re best reserved for diagnosing gnarly low-level bugs.

x

  • What it does: The x command (an alias for memory read) is a short-hand for examining memory.
  • Usage:
    (lldb) x/4w 0x7ffeefbff6c0
    
  • Notes: The notation x/4w means “examine 4 words of memory starting at the given address.” You can adjust the count and format (bytes, halfwords, words, etc.) to suit your needs.

Controlling Execution: breakpoint Commands

breakpoint set, breakpoint list, breakpoint delete

  • What it does: Set, list, and delete breakpoints.
  • Usage:
    (lldb) breakpoint set --name viewDidLoad
    (lldb) breakpoint list
    (lldb) breakpoint delete 1
    
  • Notes: Use these to control where code execution stops. Setting breakpoints on methods or specific lines is the first step to interactive debugging.

Adding Conditions to Breakpoints

  • What it does: A conditional breakpoint stops execution only when a given condition is met.
  • Usage:
    (lldb) breakpoint modify -c '(myVariable == nil)' 1
    
  • Notes: This saves time, as the debugger won’t halt unless the specified condition is true.

Unveiling the Unknown: disassemble, thread step-in, and More

disassemble

  • What it does: Displays the machine instructions for the current function.
  • Usage:
    (lldb) disassemble
    
  • Notes: Rarely needed for high-level debugging, but crucial if you suspect compiler optimizations or want to understand precisely what is running at the assembly level.

thread step-in, thread step-over, thread step-out

  • What it does: These commands control how the debugger steps through code. While step-in, step-over, and step-out are commonly used from the Xcode UI, the thread variants can be used directly in LLDB for more granular control.
  • Usage:
    (lldb) thread step-in
    (lldb) thread step-over
    (lldb) thread step-out
    
  • Notes: These are useful when the UI-based stepping is not behaving as expected or when scripting LLDB commands.

image list, image dump

  • What it does: Displays information about loaded images (frameworks, libraries) in the process.
  • Usage:
    (lldb) image list
    (lldb) image dump sections <image_name>
    
  • Notes: Useful for diagnosing linking issues or exploring what code is actually loaded into the process.

Wrapping Up

Mastering LLDB’s console commands is like gaining a superpower for debugging. From simple inspections with p and po, to on-the-fly state changes with expr, to deep dives into memory with x and memory read, LLDB gives you incredible control over your debugging sessions. While not every command is necessary every day, knowing what’s available helps ensure that when you do encounter complex bugs, you have the right tools at your fingertips.

So the next time you’re facing a puzzling issue, consider reaching beyond p and po to discover the richer features LLDB has to offer. Over time, these techniques will make your debugging sessions more efficient, more informative, and—dare we say—more enjoyable.