Skip to content
C++
Debugger
Updated 2025-01-01T00:00:00.000Z

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

bash
# 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 dump

Essential Commands

cpp
(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 debuggee

Inspecting Variables

cpp
(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 memory

Thread Commands

cpp
(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 info

Conditional Breakpoints

cpp
(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
  > DONE

Expression Language

cpp
(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

ActionGDBLLDB
Startrunrun
Breakpoint on functionbreak mainbreakpoint set -n main or b main
Breakpoint on linebreak file.cpp:42breakpoint set -f file.cpp -l 42
Print variableprint xprint x or p x
Backtracebtbt
Next linenextnext
Step instepstep
Watch variablewatch xwatchpoint set variable x
List breakpointsinfo breakpointsbreakpoint list
Delete breakpointdelete 1breakpoint delete 1
All threads btthread apply all btbt all

.lldbinit Configuration

python
# ~/.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 bt

LLDB 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.

ActionGDBLLDB
Launchgdb ./applldb ./app
Run with argsrun arg1 arg2run arg1 arg2
Set args before runset args arg1 arg2settings set target.run-args arg1 arg2
Attach to PIDattach <PID>process attach -p <PID>
Detachdetachprocess detach
Quitquitquit
Continuecontinue / ccontinue / c
Step overnext / nnext / n
Step intostep / sstep / s
Step outfinishfinish
Run until lineuntil 42thread until 42
Break on functionbreak mainbreakpoint set -n main
Break on file:linebreak file.cpp:42breakpoint set -f file.cpp -l 42
Break on methodbreak Foo::barbreakpoint set -n "Foo::bar"
Break on regexrbreak compute_.*breakpoint set -r compute_.*
List breakpointsinfo breakpointsbreakpoint list
Delete breakpointdelete 1breakpoint delete 1
Disable breakpointdisable 1breakpoint disable 1
Conditional breakcondition 1 x > 0breakpoint modify -c "(x > 0)" 1
Print variableprint x / p xprint x / p x
Print object desc.p objpo obj
Print raw memoryx/10xw addrmemory read -s4 -c10 addr
Modify variableset var x = 42expression x = 42
Backtracebtbt
Select frameframe 2frame select 2
Frame localsinfo localsframe variable
Frame argsinfo argsframe variable -a
Watch writewatch xwatchpoint set variable x
Watch read/writeawatch xwatchpoint set variable -w read_write x
List threadsinfo threadsthread list
Select threadthread 2thread select 2
All-threads btthread apply all btbt all
Disassembledisassembledisassemble
Info registersinfo registersregister read
Source listlistsource list
Define aliasdefine gdb-n\nnext\nendcommand 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 libcxx

On 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=7

Defining a custom type synthetic (child provider)

python
# 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())
main

Writing a custom command

python
# 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=3

Walking the stack from Python

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

python
# ~/.lldbinit
command script import ~/lldb_scripts/print_vec_sizes.py
command script import ~/lldb_scripts/my_formatters.py

GUI / 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) gui

Panel 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

KeyAction
TabCycle between panels
↑ / ↓Scroll within active panel
EnterExpand/collapse a variable node
FToggle full-screen for active panel
sStep over (next)
iStep into (step)
oStep out (finish)
cContinue
bSet breakpoint on highlighted source line
dDelete breakpoint on highlighted line
hShow help overlay
qQuit 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

bash
# 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 starts

Remote debugging with lldb-server

On the target machine (e.g. embedded Linux board or remote server):

bash
# 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 arg1

On 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) run

VS Code launch.json for remote LLDB

json
{
  "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

bash
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 memory

LLDB 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

bash
cmake -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER=clang++
cmake --build build -j$(nproc)
lldb build/myapp

For faster link times during iterative debugging, add split DWARF:

bash
cmake -B build \
  -DCMAKE_BUILD_TYPE=Debug \
  -DCMAKE_CXX_FLAGS="-g -gsplit-dwarf" \
  -DCMAKE_EXE_LINKER_FLAGS="-fuse-ld=lld"
cmake --build build

CMakeLists.txt target-level debug settings

cmake
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:

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:

json
{
  "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 = 24

Multi-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 = 11

Calling 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 = 3

Side-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 = 1

Variables 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
  > DONE

Changing 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
Edit on GitHubUpdated 2025-01-01T00:00:00.000Z