LLDB is more than a place to set breakpoints. You can inspect app state, test quick fixes without rebuilding, and automate common debug steps. This guide keeps things practical and beginner-friendly.
LLDB in its natural habitat
Start Here: What LLDB Is and How to Run It
LLDB (sometimes typed as lldb in lowercase by mistake) is the debugger that ships with Xcode command line tools.
If your app is not running yet, launch it under LLDB:
xcrun lldb /path/to/MyApp
Then in the LLDB prompt:
(lldb) run
If your app is already running, attach to it:
pgrep -x MyApp
xcrun lldb -p <pid>
Or from inside LLDB:
(lldb) attach --name MyApp
To exit:
(lldb) quit
Why Attaching To Production Apps Can Fail
Many people hit this early: attaching to production macOS apps is often blocked by security rules.
Main reasons:
- SIP (System Integrity Protection) blocks debugging many protected/system processes.
- Hardened Runtime + missing
get-task-allowentitlement can block debugger attach. - App Store/release builds are usually signed to prevent casual attaching.
So yes, sometimes attach starts working only after SIP is disabled on a test machine. But SIP is not the only gate. Even with SIP disabled, code-signing and entitlements can still block attach.
Check SIP status:
csrutil status
If you are doing local security research on your own Mac only, disable SIP temporarily:
- Reboot into Recovery.
- Open Terminal in Recovery.
- Run
csrutil disable. - Reboot normally.
Warning: this is dangerous. Disabling SIP removes important macOS protections that block system tampering and many privilege-escalation paths. If malware runs while SIP is off, it has a much easier time persisting and modifying protected files/processes. Do this only on an isolated test machine, never on your daily-use Mac, and re-enable SIP immediately after testing.
Re-enable it when done:
- Reboot into Recovery again.
- Run
csrutil enable. - Reboot normally.
A Fast Theory Primer
LLDB has two parts:
- The command line you type into.
- An internal toolkit called the
SB API(SB= Script Bridge in LLDB docs).
Note: this is LLDB’s API naming and is different from the separate macOS ScriptingBridge framework.
What is the SB API object model?
It is just LLDB’s internal set of objects (like building blocks): app, process, thread, stack frame, variables, and symbols. LLDB commands use these objects behind the scenes.
Typical flow:
- Create or select the app to debug (called a “target”):
target create ... - Launch or attach:
run/attach ... - Check liveness:
process status - Inspect call stack and local values:
frame info,frame variable -L(or aliasv -L)
expr -- <code> runs code inside the paused app. Start with read-only checks. Only change values on purpose.
Quick Commands I Actually Use
(lldb) breakpoint set --name viewDidLoad
(lldb) thread step-in
(lldb) thread backtrace
(lldb) frame variable request
(lldb) expr -l objc -O -- [[[UIApplication sharedApplication] connectedScenes] allObjects]
(lldb) memory read --format x --size 8 --count 8 $sp
Speed tips: br s -n, bt, v (or frame var), and mem r are short versions of common commands. Use --one-shot true for temporary breakpoints so they remove themselves after one hit.
Anatomy of a Stop
LLDB can stop because of a breakpoint, crash signal, exception, or watchpoint. First step: find out why it stopped.
Common crash signals/exceptions you will see:
EXC_BAD_ACCESS/SIGSEGV: invalid memory access (use-after-free, null/garbage pointer).SIGABRT: app calledabort()(failed assertion, fatal error, uncaught runtime issue).SIGILL: illegal CPU instruction (corrupted state, bad jump, unsupported instruction path).
process status tells you if you hit EXC_BAD_ACCESS, SIGSEGV, or a normal breakpoint. thread backtrace all shows what every thread is doing, which helps with hangs. thread return can skip the current function, but use it carefully because it changes app behavior.
Breakpoint vs watchpoint (quick difference):
- Breakpoint: stops when execution reaches a code location (file/line/function).
- Watchpoint: stops when a specific value in memory is read/written/modified.
If you get expected signals like SIGPIPE, tune handling explicitly:
process handle SIGPIPE -n true -p true -s false
Breakpoints That Pull Their Weight
br s -f File.swift -l 42 sets a breakpoint at one exact line. br s -r "viewDidLoad" matches many functions by name pattern. Conditional breakpoints like br s -n foo -c 'count > 10' stop only when a rule is true.
Auto-commands are great for data capture:
- Set breakpoint:
br s -n willDisplayCell - Add actions:
br command add <id> - Add commands like
thread backtrace,frame variable model, thencontinue
Watchpoints stop when a value changes:
watchpoint set variable myVarwatchpoint set variable -w read_write myVarwatchpoint set expression -w modify -- &myVar
Tracepoints Without Pausing
When you want logs but do not want to pause:
br s -n foo -C 'expr -O -- foo' -G true
Add --ignore-count to skip the first N hits.
Reading Swift Nicely
Swift values can look cleaner with these settings:
settings set target.swift-demangle truesettings set target.max-children-count 256settings set target.process.optimization-warnings true
Use expr -l swift -O -- for Swift objects (equivalent to po with explicit Swift context) and expr -l objc -O -- for Objective-C objects. In Release/optimized builds, some local variables may be unavailable; this is normal.
Production/Optimized Build Survival
settings set symbols.enable-external-lookup true can help LLDB find extra symbol info. If local values are optimized away, check raw CPU registers (register read) and memory (memory read) near $sp/$fp.
Noise control while stepping:
settings append target.process.thread.step-avoid-libraries UIKitCore
thread jump --by 1 can skip one source line. Use as a last resort.
If tracing is supported in your environment, this sequence is useful:
thread trace startthread trace dump instructions
Memory Work: From Sanity Checks to Surgery
Stack sanity check:
memory read --format x --size 8 --count 4 $fp
Address-to-symbol mapping:
image lookup -a 0xADDR
Controlled mutation during experiments:
expr -- myValue = 0memory write <addr> <bytes>
For memory allocation history, try memory history <address> when available. For memory-region details, use memory region <address> and vmmap (a macOS terminal tool).
Async/Await, Actors, and Queues
thread list gives a quick view of active threads and queue names. thread info -s gives extra stop details.
Useful concurrency breakpoints:
br s -n _swift_task_switchbr s -n swift_task_enqueue
To find where a task was launched, break in the task body and inspect bt for concurrency runtime frames around your app frames.
Crash and Hang Triage Playbook
For hangs:
process interruptthread backtrace all- Check for waits like
pthread_mutex_lockanddispatch_semaphore_wait
For crashes, start with the crashing thread: run process status, then frame info on that thread. If shared state looks suspicious, add watchpoints and rerun.
To keep a point-in-time artifact for offline analysis:
process save-core /tmp/foo.core
Remote and Core-File Debugging
Remote iOS debugging usually starts with:
platform select remote-iosplatform connect connect://<host>:<port>
Then add symbols locally (target symbols add ...) and verify loaded images with image list.
For core files:
target create --core crash.corethread backtrace allthread select <index>to inspect specific crashed paths
Symbols and dSYMs
Keep .dSYM files available. They help LLDB map memory addresses back to file names and line numbers. Configure search paths in .lldbinit (target.exec-search-paths, target.debug-file-search-paths).
Verification loop:
image list -bfor UUID/slide infodwarfdump --uuidto confirm matches- fallback symbolication with
atos -o MyApp -arch arm64 -l <slide> <addr>if needed
Python: Hooking Your Own Commands
LLDB supports Python. You can add your own custom commands. If you are new, you can skip this section and come back later.
# save as ~/lldb_tools/retain_cycles.py
import lldb
def find_retain_cycles(debugger, command, exe_ctx, result, _):
"""Demo command: run a Swift helper method on an expression."""
cmd = f"expr -l swift -O -- {command}.debugRetainCycles()"
res = lldb.SBCommandReturnObject()
debugger.GetCommandInterpreter().HandleCommand(cmd, res)
if res.Succeeded():
result.AppendMessage(res.GetOutput() or "")
else:
result.SetError(res.GetError() or "Expression failed")
def __lldb_init_module(debugger, _internal_dict):
debugger.HandleCommand(
"command script add -f retain_cycles.find_retain_cycles rcfind"
)
print("Registered: rcfind <expression>")
Wire it in .lldbinit:
command script import ~/lldb_tools/retain_cycles.py
Use it in-session:
(lldb) rcfind myController
A More Involved Hook: Auto-Dump on Crash Stops
# ~/lldb_tools/on_crash_dump.py
import datetime
import lldb
CRASH_REASONS = {
lldb.eStopReasonException,
lldb.eStopReasonSignal,
}
class CrashDumpHook:
def __init__(self, target, _extra_args, _internal_dict):
self.target = target
def handle_stop(self, exe_ctx, stream):
thread = exe_ctx.GetThread()
if not thread.IsValid():
return False
reason = thread.GetStopReason()
if reason not in CRASH_REASONS:
return False
ts = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
path = f"/tmp/lldb-crash-{ts}.txt"
res = lldb.SBCommandReturnObject()
interp = self.target.GetDebugger().GetCommandInterpreter()
interp.HandleCommand("thread backtrace all", res)
with open(path, "w", encoding="utf-8") as f:
f.write(f"Stop reason enum: {reason}\n\n")
f.write(res.GetOutput() or "")
stream.Printf(f"Wrote crash dump to {path}\n")
return False
def __lldb_init_module(debugger, _dict):
debugger.HandleCommand(
"target stop-hook add -P on_crash_dump.CrashDumpHook"
)
print("Registered crash stop hook")
Add to .lldbinit:
command script import ~/lldb_tools/on_crash_dump.py
This saves crash dumps only on real crash-like stops, not on every breakpoint.
Favorite .lldbinit Snippets
settings set stop-disassembly-count 4
settings set target.process.thread.step-out-avoids-no-debug true
settings set target.process.thread.step-in-avoids-no-debug true
settings set target.max-children-count 256
settings set target.swift-demangle true
command alias bt thread backtrace
command alias frv frame variable -L
command alias memr memory read --format x --size 8 --count 8
Automating Sessions
Small automations that save time:
- Keep recurring breakpoints in a file and load with
command source my_breakpoints.lldb - Add quick diagnostics:
target stop-hook add -o "thread backtrace" - Turn on protocol logging when needed:
log enable gdb-remote packets
Performance Poking Without Instruments
For instruction-level hotspots:
thread step-instthread until -a <addr>
For symbol-focused checks:
image lookup -n objc_msgSendbr s -a <addr>with a suitable condition
statistics dump helps you see if LLDB itself is slow (for example, symbol loading or heavy expression use).
UI Debugging Without Xcode
Rendering and layout:
br s -n drawRect:br s -n layoutSubviews
Responder-chain probing:
expr -l objc -O -- [UIResponder targetForAction:@selector(description) withSender:nil]
Text view hierarchy dump (old but still useful in UIKit debugging):
expr -l objc -O -- [[[UIApplication sharedApplication] keyWindow] recursiveDescription]
When LLDB Misbehaves
If LLDB starts slowly or acts weird, first simplify your .lldbinit. If module caches are broken, clear ~/Library/Developer/Xcode/DerivedData/ModuleCache*.
If expressions fail unexpectedly, force the language:
expr -l objc -- ...expr -l swift -- ...
If remote attach fails repeatedly, restart the target device and clean up stale debugserver processes on host and device.
Tiny Cheat Sheet (copy/paste)
br s -n method
br s -f File.swift -l 88
br s -n foo -c 'x > 3'
br s -n foo --one-shot true
watchpoint set variable -w read_write myVar
thread backtrace all
frame variable -L
expr -l swift -O -- myObj.debugDescription()
memory read --format x --size 8 --count 4 $sp
process handle SIGPIPE -n true -p true -s false
LLDB gets easier with repetition. Keep a small command list you trust, and add automation later.