LLDB Ruby

Ruby bindings for the LLDB debugger.
Overview
This gem provides Ruby bindings for LLDB (Low Level Debugger), allowing you to access LLDB's debugging functionality from Ruby. It uses FFI (Foreign Function Interface) with a C wrapper around LLDB's C++ API.
Requirements
- Ruby 3.0 or later
- LLDB 14 or later (Linux and macOS)
- C++17 compatible compiler (gcc 8+ / clang 10+)
- libffi-dev (for the FFI gem)
Installing LLDB
Ubuntu/Debian:
sudo apt-get install lldb-14 liblldb-14-dev
Fedora/RHEL:
sudo dnf install lldb-devel
macOS:
xcode-select --install
# or
brew install llvm
Installation
Add this line to your application's Gemfile:
gem 'lldb'
And then execute:
bundle install
Or install it yourself as:
gem install lldb
Usage
Basic Example
require 'lldb'
# Initialize LLDB
LLDB.initialize
# Create a debugger
debugger = LLDB::Debugger.create
debugger.async = false # Run synchronously
# Create a target from an executable
target = debugger.create_target('./my_program')
# Set a breakpoint at main
bp = target.breakpoint_create_by_name('main')
puts "Breakpoint #{bp.id} created with #{bp.num_locations} locations"
# Launch the process
process = target.launch
# Check if stopped at breakpoint
if process.stopped?
thread = process.selected_thread
frame = thread.selected_frame
puts "Stopped at: #{frame.function_name}"
puts "Location: #{frame.location}"
# Find a variable
var = frame.find_variable('argc')
puts "argc = #{var.value}" if var
# Evaluate an expression
result = frame.evaluate_expression('argc + 1')
puts "argc + 1 = #{result.value}" if result
# Continue execution
process.continue
end
# Clean up
process.kill if process.valid?
LLDB.terminate
Creating Breakpoints
# By function name
bp = target.breakpoint_create_by_name('main')
# By source location
bp = target.breakpoint_create_by_location('main.c', 10)
# By address
bp = target.breakpoint_create_by_address(0x100001000)
# Configure breakpoint
bp.condition = 'x > 5'
bp.ignore_count = 2
bp.one_shot = true
bp.disable
bp.enable
Stepping Through Code
thread = process.selected_thread
# Step over (next line)
thread.step_over
# Step into (enter function)
thread.step_into
# Step out (return from function)
thread.step_out
# Step one instruction
thread.step_instruction
Inspecting Variables
frame = thread.selected_frame
# Find a variable by name
var = frame.find_variable('my_variable')
if var && var.valid?
puts "Name: #{var.name}"
puts "Type: #{var.type_name}"
puts "Value: #{var.value}"
puts "Size: #{var.byte_size} bytes"
# For integer types
puts "As integer: #{var.to_i}"
# For complex types with children
if var.might_have_children?
var.each do |child|
puts " #{child.name} = #{child.value}"
end
end
end
Target#launch passes the launch request directly to LLDB. It does not add
STOP_AT_ENTRY, wait for a state transition, or auto-continue to a breakpoint.
Set launch_flags: LLDB::LaunchFlags::STOP_AT_ENTRY explicitly when that
behavior is required; callers are responsible for waiting on asynchronous
processes.
Handling Events Explicitly
Use LLDB's listener queue when asynchronous state changes are needed:
listener = debugger.listener
broadcaster = process.broadcaster
listener.start_listening_for_events(broadcaster, LLDB::Process::BroadcastBit::STATE_CHANGED)
if (event = listener.wait_for_event(timeout_seconds: 1))
puts "state = #{LLDB::State.name(event.process_state)}"
end
timeout_seconds: 0 performs a non-blocking poll. The library does not start
background event threads.
Source locations remain structured when needed: Frame#line_entry exposes its
LineEntry, including start_address, end_address, file, line, and column;
BreakpointLocation#address exposes an Address with separate
file_address and load_address values. FileSpecList can collect and return
FileSpec objects without reducing them to path strings.
Type inspection also preserves member metadata. Use Type#field_at_index,
#direct_base_class_at_index, or #virtual_base_class_at_index to obtain a
TypeMember with its name, type, byte/bit offset, and bitfield width.
Modules and frames expose structured debug objects as well: use
Module#symbol_at_index, Frame#function, Frame#symbol,
Frame#compile_unit, and Frame#block to retain symbol, function, source
unit, and lexical block metadata.
For structured disassembly, use Frame#instruction_list. Each
Instruction exposes its address, mnemonic, operands, comment, byte size, and
raw bytes; the existing Frame#disassemble string API remains available.
Debugger and launch choices are explicit option objects. For example,
LLDB::Debugger.create(source_init_files: true) opts into LLDB init files,
while LLDB::ExpressionOptions can be passed to expression evaluation without
changing the no-options call path.
Working with Threads
# Get all threads
process.threads.each do |thread|
puts "Thread #{thread.id}: #{thread.name || 'unnamed'}"
puts " Stop reason: #{thread.stop_reason_name}"
end
# Get the call stack
thread.frames.each_with_index do |frame, i|
puts "##{i}: #{frame.function_name} at #{frame.location}"
end
Attaching to a Running Process
process = target.attach(pid: 12345)
API Reference
Main Classes
LLDB::Debugger- Entry point for debugging operationsLLDB::Target- Represents a debug target (executable)LLDB::Process- Represents a running processLLDB::Thread- Represents an execution threadLLDB::Frame- Represents a stack frameLLDB::Breakpoint- Represents a breakpointLLDB::BreakpointLocation- Represents a breakpoint locationLLDB::Value- Represents a variable or expression resultLLDB::Type- Represents debug type informationLLDB::TypeMember- Represents a field or base-class memberLLDB::Module- Represents a loaded moduleLLDB::Symbol- Represents a symbolLLDB::Function- Represents a functionLLDB::CompileUnit- Represents a compilation unitLLDB::Block- Represents a lexical blockLLDB::InstructionList/LLDB::Instruction- Represents structured disassemblyLLDB::FileSpec/LLDB::Address/LLDB::LineEntry- Represents source locationsLLDB::Listener/LLDB::Event/LLDB::Broadcaster- Represents LLDB eventsLLDB::Error- Represents an error from LLDB
Constants
Process states are available in LLDB::State:
INVALID,UNLOADED,CONNECTED,ATTACHING,LAUNCHINGSTOPPED,RUNNING,STEPPING,CRASHED,DETACHED,EXITED,SUSPENDED
Stop reasons are available in LLDB::StopReason:
INVALID,NONE,TRACE,BREAKPOINT,WATCHPOINTSIGNAL,EXCEPTION,EXEC,PLAN_COMPLETE,THREAD_EXITING,INSTRUMENTATIONPROCESSOR_TRACE,FORK,VFORK,VFORK_DONE
Development
The native wrapper build discovers LLDB in this order:
--with-lldb-includeand--with-lldb-lib--with-lldb-dirLLDB_DIRllvm-configor a versionedllvm-config-NonPATH- Homebrew, Xcode, and standard LLVM prefixes
Discovery succeeds only after compiling and linking a C++17 probe against
liblldb. The selected paths and optional API capabilities are printed during
the build. Windows is not a supported platform yet.
After checking out the repo, run:
bundle install
cd ext/lldb && ruby extconf.rb && make && cd ../..
bundle exec rspec
Binding and type-surface checks can be run with:
bundle exec rake bindings:check
bundle exec rake rbs:verify
bindings/surface.yml records the classification and exception-safety review
for every native export. Update it together with any new header declaration.
Only calls that can wait for the inferior, debugger server, or command script release Ruby's GVL. The wrapper does not invoke Ruby callbacks from those calls; adding callback APIs requires revisiting this policy.
To run tests, you need to compile the test fixtures:
gcc -g -O0 -o spec/fixtures/simple spec/fixtures/simple.c
Contributing
Bug reports and pull requests are welcome on GitHub.
License
Dual licensed under the MIT License and Apache License 2.0.