LLDB Debugger
LLDB debugger for C++ — essential commands, breakpoints, watchpoints, expression evaluation, Python scripting, and LLDB vs GDB command mapping.
TL;DR
LLDB is the LLVM-project debugger, native on macOS and preferred with Clang on Linux. Same capabilities as GDB — different command syntax. If you know GDB, see the mapping table at the bottom.
Starting LLDB
# Compile with debug info
clang++ -g -O0 myapp.cpp -o myapp
# Launch
lldb myapp
lldb -- myapp arg1 arg2 # with arguments
lldb -p <PID> # attach to running process
lldb myapp -c core.dump # open core dumpEssential Commands
(lldb) run [args] start the program
(lldb) continue continue after stop
(lldb) next step over (source line)
(lldb) step step into
(lldb) finish step out of current function
(lldb) quit exit LLDB
(lldb) breakpoint set -n main break at function main
(lldb) breakpoint set -f file.cpp -l 42 break at file:line
(lldb) breakpoint set -n "ClassName::method"
(lldb) breakpoint list show all breakpoints
(lldb) breakpoint delete 1 delete breakpoint #1
(lldb) breakpoint disable 2 disable without deleting
(lldb) watchpoint set variable myVar stop when myVar changes
(lldb) watchpoint set expression -w read_write -- &obj.field
(lldb) bt backtrace (call stack)
(lldb) bt all backtrace all threads
(lldb) frame select 2 select frame 2
(lldb) frame info info about current frame
(lldb) print expr evaluate and print expression
(lldb) p myvar shorthand for print
(lldb) po obj print object (calls description)
(lldb) expression myvar = 42 modify variable in debuggeeInspecting Variables
(lldb) frame variable all local variables
(lldb) frame variable myvar specific variable
(lldb) frame variable -r myobj recursive (expand object)
(lldb) p *ptr dereference pointer
(lldb) p arr[0] array element
(lldb) memory read --size 4 --count 10 0x7fff5fc0 read raw memoryThread Commands
(lldb) thread list all threads
(lldb) thread select 2 switch to thread 2
(lldb) thread backtrace all all thread stacks
(lldb) thread info current thread infoConditional Breakpoints
(lldb) breakpoint set -n compute -c "(x > 100)"
(lldb) breakpoint modify -c "(errno != 0)" 1
(lldb) breakpoint command add 1
Enter commands, end with 'DONE':
> p x
> continue
> DONEExpression Language
(lldb) p (int)myChar cast expression
(lldb) p &myVar address of variable
(lldb) p sizeof(MyStruct) sizeof
(lldb) expression --language c++ -- std::vector<int>{1,2,3}.size()LLDB vs GDB Command Mapping
| Action | GDB | LLDB |
|---|---|---|
| Start | run | run |
| Breakpoint on function | break main | breakpoint set -n main or b main |
| Breakpoint on line | break file.cpp:42 | breakpoint set -f file.cpp -l 42 |
| Print variable | print x | print x or p x |
| Backtrace | bt | bt |
| Next line | next | next |
| Step in | step | step |
| Watch variable | watch x | watchpoint set variable x |
| List breakpoints | info breakpoints | breakpoint list |
| Delete breakpoint | delete 1 | breakpoint delete 1 |
| All threads bt | thread apply all bt | bt all |
.lldbinit Configuration
# ~/.lldbinit
settings set target.x86-disassembly-flavor intel
settings set thread-format "thread #${thread.index}: ${thread.name}\n"
# Alias GDB-style commands
command alias gdb-bt btLLDB vs GDB — Full Command Reference
GDB muscle memory transfers cleanly once you know the mapping. LLDB is more structured — commands follow noun verb grammar rather than GDB's abbreviated forms. Many GDB shortcuts (b, p, bt) also work in LLDB.
| Action | GDB | LLDB |
|---|---|---|
| Launch | gdb ./app | lldb ./app |
| Run with args | run arg1 arg2 | run arg1 arg2 |
| Set args before run | set args arg1 arg2 | settings set target.run-args arg1 arg2 |
| Attach to PID | attach <PID> | process attach -p <PID> |
| Detach | detach | process detach |
| Quit | quit | quit |
| Continue | continue / c | continue / c |
| Step over | next / n | next / n |
| Step into | step / s | step / s |
| Step out | finish | finish |
| Run until line | until 42 | thread until 42 |
| Break on function | break main | breakpoint set -n main |
| Break on file:line | break file.cpp:42 | breakpoint set -f file.cpp -l 42 |
| Break on method | break Foo::bar | breakpoint set -n "Foo::bar" |
| Break on regex | rbreak compute_.* | breakpoint set -r compute_.* |
| List breakpoints | info breakpoints | breakpoint list |
| Delete breakpoint | delete 1 | breakpoint delete 1 |
| Disable breakpoint | disable 1 | breakpoint disable 1 |
| Conditional break | condition 1 x > 0 | breakpoint modify -c "(x > 0)" 1 |
| Print variable | print x / p x | print x / p x |
| Print object desc. | p obj | po obj |
| Print raw memory | x/10xw addr | memory read -s4 -c10 addr |
| Modify variable | set var x = 42 | expression x = 42 |
| Backtrace | bt | bt |
| Select frame | frame 2 | frame select 2 |
| Frame locals | info locals | frame variable |
| Frame args | info args | frame variable -a |
| Watch write | watch x | watchpoint set variable x |
| Watch read/write | awatch x | watchpoint set variable -w read_write x |
| List threads | info threads | thread list |
| Select thread | thread 2 | thread select 2 |
| All-threads bt | thread apply all bt | bt all |
| Disassemble | disassemble | disassemble |
| Info registers | info registers | register read |
| Source list | list | source list |
| Define alias | define gdb-n\nnext\nend | command alias gdb-n next |
Data Formatters — Pretty-Printing STL
LLDB ships with built-in libc++ and libstdc++ formatters that make STL containers readable at a glance. Without them, p vec prints raw internal fields. With them, you see elements directly.
Enabling built-in formatters
# In ~/.lldbinit — enable the bundled LLDB data formatters
command script import lldb.formatters.cpp.libcxx
command script import lldb.formatters.cpp.gnu_libstdcpp
# Or one-liner: import all at session start
(lldb) type category enable gnu-libstdc++
(lldb) type category enable libcxxOn macOS with Xcode's LLDB the libc++ formatters are active by default. On Linux with LLDB from the llvm-toolchain packages, import them explicitly in ~/.lldbinit.
What pretty-printing looks like
# Without formatters
(lldb) p vec
(std::vector<int>) $0 = {
__begin_ = 0x0000600003a04020
__end_ = 0x000060000 3a04038
__end_cap_ = { ... }
}
# With libc++ formatters active
(lldb) p vec
(std::vector<int>) $0 = size=3 {
[0] = 10
[1] = 20
[2] = 30
}
(lldb) p myMap
(std::map<std::string, int>) $1 = size=2 {
[0] = { first = "alpha", second = 1 }
[1] = { first = "beta", second = 2 }
}
(lldb) p myStr
(std::string) $2 = "hello world"Defining a custom type summary
# Define inline in the session or add to ~/.lldbinit
(lldb) type summary add --summary-string "x=${var.x}, y=${var.y}" Point
# Now every Point prints concisely
(lldb) p pt
(Point) $3 = x=3, y=7Defining a custom type synthetic (child provider)
# my_formatters.py — expose vector<T> internals as indexed children
import lldb
class VectorSynthProvider:
def __init__(self, valobj, internal_dict):
self.valobj = valobj
def num_children(self):
begin = self.valobj.GetChildMemberWithName('__begin_')
end = self.valobj.GetChildMemberWithName('__end_')
item_size = begin.GetType().GetPointeeType().GetByteSize()
return int((end.GetValueAsUnsigned() - begin.GetValueAsUnsigned()) / item_size)
def get_child_at_index(self, idx):
begin = self.valobj.GetChildMemberWithName('__begin_')
addr = begin.GetValueAsUnsigned() + idx * begin.GetType().GetPointeeType().GetByteSize()
return self.valobj.CreateValueFromAddress(f'[{idx}]', addr,
begin.GetType().GetPointeeType())
def get_child_index(self, name):
return int(name.lstrip('[').rstrip(']'))
def update(self):
pass(lldb) command script import /path/to/my_formatters.py
(lldb) type synthetic add -l my_formatters.VectorSynthProvider "std::vector<int>"Python Scripting
LLDB exposes a full Python API that lets you write commands, formatters, and automation scripts with the same interface the debugger uses internally. The lldb module gives access to SBTarget, SBFrame, SBValue, and the entire symbol graph.
Interactive Python shell
(lldb) script
Python Interactive Interpreter. To exit, type 'quit()', 'exit()'.
>>> import lldb
>>> target = lldb.debugger.GetSelectedTarget()
>>> process = target.GetProcess()
>>> thread = process.GetSelectedThread()
>>> frame = thread.GetSelectedFrame()
>>> print(frame.GetFunctionName())
mainWriting a custom command
# print_vec_sizes.py
# Usage: command script import print_vec_sizes.py
# (lldb) vec-sizes
import lldb
def print_vec_sizes(debugger, command, result, internal_dict):
"""Print name and size of every std::vector<*> variable in current frame."""
frame = debugger.GetSelectedTarget().GetProcess() \
.GetSelectedThread().GetSelectedFrame()
for var in frame.GetVariables(True, True, True, True): # args, locals, statics, in-scope
type_name = var.GetType().GetName()
if 'std::vector' in type_name:
size_child = var.GetChildMemberWithName('__end_')
begin_child = var.GetChildMemberWithName('__begin_')
if size_child and begin_child:
item_size = begin_child.GetType().GetPointeeType().GetByteSize()
if item_size > 0:
sz = (size_child.GetValueAsUnsigned()
- begin_child.GetValueAsUnsigned()) // item_size
result.AppendMessage(f"{var.GetName()} : {type_name} — size={sz}")
def __lldb_init_module(debugger, internal_dict):
debugger.HandleCommand(
'command script add -f print_vec_sizes.print_vec_sizes vec-sizes'
)
print("'vec-sizes' command installed.")(lldb) command script import /path/to/print_vec_sizes.py
(lldb) vec-sizes
numbers : std::vector<int> — size=5
names : std::vector<std::string> — size=3Walking the stack from Python
process = lldb.debugger.GetSelectedTarget().GetProcess()
for thread in process:
print(f"Thread {thread.GetThreadID()}: {thread.GetName()}")
for frame in thread:
print(f" #{frame.GetFrameID()} {frame.GetFunctionName()}")Persistent ~/.lldbinit hook
# ~/.lldbinit
command script import ~/lldb_scripts/print_vec_sizes.py
command script import ~/lldb_scripts/my_formatters.pyGUI / TUI Mode
LLDB ships a full-screen terminal GUI that shows source, threads, variables, and a command prompt simultaneously — no IDE required. Invoke it with the gui command from inside any LLDB session.
(lldb) run
# ... stops at breakpoint ...
(lldb) guiPanel layout
The GUI splits the terminal into three panes:
- Source panel (top-left) — current source file with the PC arrow (
->) on the active line - Variables panel (top-right) — local variables, auto-expanded
- Threads / Backtrace panel (bottom-left) — all threads with the selected frame highlighted
- Command panel (bottom-right) — standard LLDB prompt, results shown here
Keyboard shortcuts
| Key | Action |
|---|---|
Tab | Cycle between panels |
↑ / ↓ | Scroll within active panel |
Enter | Expand/collapse a variable node |
F | Toggle full-screen for active panel |
s | Step over (next) |
i | Step into (step) |
o | Step out (finish) |
c | Continue |
b | Set breakpoint on highlighted source line |
d | Delete breakpoint on highlighted line |
h | Show help overlay |
q | Quit GUI, return to command prompt |
Limitations
The LLDB GUI is a quick productivity win but not a full IDE replacement. It does not support mouse input in all terminal emulators, and expression evaluation must be done in the command panel. For a richer experience consider the VS Code CodeLLDB extension (see below).
Multiprocess and Remote Debugging
LLDB's platform model cleanly separates the host (your machine) from the target (the remote process) — the same commands work whether debugging locally or over a network.
Attaching to a running process
# By PID
lldb -p 12345
# Or from inside a session
(lldb) process attach -p 12345
(lldb) process attach -n myapp # by name (attaches to first match)
(lldb) process attach -n myapp --waitfor # wait until process with that name startsRemote debugging with lldb-server
On the target machine (e.g. embedded Linux board or remote server):
# Install lldb-server (same version as host LLDB)
# Ubuntu: apt install lldb
# Start the platform server
lldb-server platform --listen "*:1234" --server
# Or start as a gdbserver-compatible stub
lldb-server gdbserver *:1234 -- ./myapp arg1On the host machine:
(lldb) platform select remote-linux
(lldb) platform connect connect://192.168.1.100:1234
(lldb) file /path/to/local/myapp # match the binary on target
(lldb) runVS Code launch.json for remote LLDB
{
"version": "0.2.0",
"configurations": [
{
"name": "Remote LLDB",
"type": "lldb",
"request": "attach",
"program": "${workspaceFolder}/build/myapp",
"pid": "${command:pickProcess}",
"initCommands": [
"platform select remote-linux",
"platform connect connect://192.168.1.100:1234"
],
"preRunCommands": [
"settings set target.x86-disassembly-flavor intel"
]
}
]
}Debugging a core dump
lldb ./myapp -c /var/core/core.12345
(lldb) bt # inspect call stack at crash
(lldb) frame variable # locals at crash site
(lldb) memory read 0x... # inspect memoryLLDB with CMake
Attaching a debugger to a CMake project is a one-flag change — CMAKE_BUILD_TYPE=Debug adds -g and disables optimisations project-wide. Never debug a Release build unless you enjoy reading optimised assembly.
Build for debugging
cmake -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER=clang++
cmake --build build -j$(nproc)
lldb build/myappFor faster link times during iterative debugging, add split DWARF:
cmake -B build \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_CXX_FLAGS="-g -gsplit-dwarf" \
-DCMAKE_EXE_LINKER_FLAGS="-fuse-ld=lld"
cmake --build buildCMakeLists.txt target-level debug settings
add_executable(myapp main.cpp)
# Ensure debug info even in RelWithDebInfo
target_compile_options(myapp PRIVATE
$<$<CONFIG:Debug>:-g3 -O0 -fno-omit-frame-pointer>
)VS Code CodeLLDB extension
Install the CodeLLDB extension (vadimcn.vscode-lldb) then add to .vscode/launch.json:
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug with LLDB",
"type": "lldb",
"request": "launch",
"program": "${workspaceFolder}/build/myapp",
"args": [],
"cwd": "${workspaceFolder}",
"env": {},
"preLaunchTask": "cmake-build-debug"
}
]
}Pair with a .vscode/tasks.json build task:
{
"version": "2.0.0",
"tasks": [
{
"label": "cmake-build-debug",
"type": "shell",
"command": "cmake --build build --config Debug",
"group": { "kind": "build", "isDefault": true }
}
]
}CodeLLDB supports inline variable display, LLDB's Python API for custom visualisers, and a disassembly view — all inside VS Code.
Expression Evaluation
LLDB's expression command is a full C++ REPL with access to every live variable in scope. Use it to call functions, construct temporaries, and mutate state — the expression runs inside the debuggee process.
Basic evaluation
(lldb) expr x + y
(int) $0 = 42
(lldb) expr (int)ptr # cast
(lldb) expr *ptr # dereference
(lldb) expr sizeof(MyStruct)
(unsigned long) $1 = 24Multi-statement expressions
Use -- to pass raw C++ to the compiler:
(lldb) expr --language c++ -- \
std::string s("hello"); \
s += " world"; \
s.size()
(unsigned long) $2 = 11Calling live functions
# Call a function in the running process
(lldb) expr myObj.computeHash()
(uint32_t) $3 = 0xdeadbeef
# Call a free function
(lldb) expr printf("x=%d\n", x)
x=17
(int) $4 = 3
# Construct and inspect a temporary
(lldb) expr std::vector<int> v{1,2,3}; v.size()
(unsigned long) $5 = 3Side-effecting expressions — use with care
Calling functions with side effects inside expr changes your program's state, which can mask bugs or move them. LLDB warns when it detects this. To allow side effects explicitly:
(lldb) expr --allow-jit -- myVec.push_back(99)To suppress side effects and only allow reads:
(lldb) expr --no-side-effects -- myObj.value()Defining persistent variables across expr calls
(lldb) expr int $counter = 0
(lldb) expr $counter += 1
(lldb) expr $counter
(int) $0 = 1Variables prefixed with $ persist for the life of the session and can be referenced in breakpoint conditions:
(lldb) expr int $hitcount = 0
(lldb) breakpoint set -n processItem
(lldb) breakpoint command add 1
> expr $hitcount += 1
> p $hitcount
> continue
> DONEChanging variables mid-execution
(lldb) expr x = 100 # overwrite a local variable
(lldb) expr errno = 0 # clear errno
(lldb) expr myPtr = nullptr # null out a pointer