I'll explain how to step through a Rust application using LLDB and illustrate some of the most basic commands. This post assumes MacOS throughout. Things are probably very different on Windows, but might be very similar on Linux and friends.
Why debuggers in Rust
There's no repl for Rust, so its especially important to use other techniques to interact with Rust code; I believe that its unlikely there would ever be a Rust repl given the fact that Rust is a compiled language. Additionally, its very cumbersome to use the online playground for understanding the code I'm actually working on right now. So, the main options for debugging code are (1) lean on the compiler and (2) lots of println!()
, which can only get you so far.
Fortunately, Rust has C Application Binary Interface (ABI) compatibility so we can mostly pretend that compiled Rust is C code. The Rust compiler team has also embedded debug symbols using the DWARF protocol, so that means that we can use the always-fashionable GDB and LLDB. Some of the formatting isn't quite right, so Rust's package manager cargo ships with a wrapper script for each: rust-gdb
and rust-lldb
that includes a python script for formatting purposes.
1 - Compile with debug symbols
By default, cargo build
creates a "debug" build that contains the debug symbols. However, cargo install
does a "release" build with is optimized and contains no debug symbols. I haven't figured out to use LLDB with cargo install
, but it's probably very similar what I'm going in this post.
$ cargo build -v
Compiling mycrate v0.1.0 (file:///home/user/programming/mycrate)
Running `rustc --crate-name mycrate src/main.rs --crate-type bin --emit=dep-info,link -C debuginfo=2 -C metadata=8b3e6af7e4113faf -C extra-filename=-8b3e6af7e4113faf --out-dir /home/user/programming/mycrate/target/debug/deps -L dependency=/home/user/programming/mycrate/target/debug/deps --extern log=/home/user/programming/mycrate/target/debug/deps/liblog-6d2adb16c0d397ce.rlib --extern mkv=/home/user/programming/mycrate/target/debug/deps/libmkv-391472e7b11b31e6.rlib --extern env_logger=/home/user/programming/mycrate/target/debug/deps/libenv_logger-8b11da9f68ecb368.rlib`
Finished dev [unoptimized + debuginfo] target(s) in 13.4 secs
Notice the Finished dev [unoptimized + debuginfo]...
on the last line. The "debuginfo" tells us we have produced a debugable binary.
Digression - Compiling debug symbols using rustc directly
If you don't use cargo
(I'm looking at you Dropbox!), the rustc
compiler supports two command line flags which will do this:
-g
-C debuginfo=2
As far as I can tell, these appear to do exactly the same thing. There's no reason to pick one over the other. However, you may only use one or the other form but not both. Full example:
rustc -g src/main.rs
2 - Test loading your program in LLDB
I got stuck here for quite a while, so I'm going make verifying the LLDB/Rust setup a whole step in this tutorial.
Quick primer for folks new to LLDB/GDB like me -- these debuggers are invoked directly from the command line and passed an executable that they will target. The executable is not started -- by default there are no breakpoints set, so program would just run to completion without ever letting LLDB pause and explore.
Okay, let's feed LLDB our program mainMYGITHASH
(which is in target/debug/deps/), by typing:
$ sudo rust-lldb target/debug/deps/mainMYGITHASH
- Notice the path is target/debug/deps/mainMYGITHASH -- you must use the binary in the deps directory. See this cargo issue
- On MacOS,
sudo
seems to be required, otherwise I see errors about "codesigned" and nothing works. I kind of suspect Linux and friends are similar. - On my machine,
lldb
is already installed though I have no idea if this was accidental or ships with MacOS by default. Linux systems may need install it, but I have no idea - Rustup installs
rust-lldb
in $HOME/.cargo/bin alongsiderustc
,cargo
, and friends. Use rustup if you don't haverust-lldb
; then make sure that $HOME/.cargo/bin is in thePATH
.
You should see output like this:
(lldb) command source -s 0 '/tmp/rust-lldb-commands.6F5RYs'
Executing commands in '/tmp/rust-lldb-commands.6F5RYs'.
(lldb) command script import "/Users/bryce/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/etc/lldb_rust_formatters.py"
(lldb) type summary add --no-value --python-function lldb_rust_formatters.print_val -x ".*" --category Rust
(lldb) type category enable Rust
(lldb) target create "target/debug/deps/rcplayer-8b3e6af7e4113faf"
Current executable set to 'target/debug/deps/rcplayer-8b3e6af7e4113faf' (x86_64).
(lldb)
If instead you see lots of warnings about missing symbols, instead go back and re-read this cargo issue. TL;DR -- use the executable in target/debug/deps/ which ends in a git hash. Otherwise, at the time of writing this, its not guarranteed that LLDB will work.
Exiting an LLDB session
If LLDB is even close to working, the prompt will change to "lldb" as we saw in the expected output sample above. We're now inside a LLDB repl, and any time LLDB pauses or the exexecutable we're running finishes, we'll be dropped back into this repl.
To get out of the repl, we have a couple of options:
- CTRL-D - works basically anywhere: ssh, bash, and most interactive command line tools
- q - this works in LLDB, GDB (and also
less
). Using q will cause LLDB to confirm with me that I really really want to quit
2 - Load program with args
Okay, so we know how to enter and exit a LLDB session with our Rust program at this point. Lots of the time, I want to feed arguments to my program while its being debugged or else nothing interesting will happen. LLDB makes this super simple. We just put our commands at the end of the rust-lldb invocation. Example:
$ sudo rust-lldb target/debug/deps/mainMYGITHASH arg1 arg2
If that worked you should see a line in the terminal like this:
(lldb) settings set -- target.run-args "arg1" "arg2
Change arguments between runs of your rust program
At the (lldb)
prompt type this to change the argument to "foobar":
settings set -- target.run-args "arg1"
3 - Debug your rust program
Set a breakpoint
The b
command sets a new breakpoint taking the name of a function (or a regular expression) as an argument.
(lldb) b my_fn_name
If it worked, you'll see something like:
Breakpoint 1: where = rcplayer`rcplayer::parse_element + 21 at main.rs:83, address = 0x00000001000113f5
See help b
for more details on specifying setting breakpoints in files at line numbers. I haven't quite figured this out yet in multi-file projects.
To see the current list of breakpoints use breakpoint list
Run the program
The r
command (short for run
). Example:
(lldb) r
The rust executable will run as a new process until lldb either (1) hits a breakpoint, at which point it will pause, OR (2) the process terminates. In either case, LLDB will present the repl.
See variables in the current stack frame
The frame variable
command prints out all the variables in the current stack. Example:
(lldb) frame variable
(mkv::elements::Element *) element = &0x100617190
(mkv::elements::Element *) element = &0x0
Print out a variable or expression
The p
command prints out a variable (and sometimes expressions, though some trial error may be required). Example:
(lldb) p element
(mkv::elements::Element *) $0 = &0x0
(lldb) p *element
error: Couldn't apply expression side effects : Couldn't dematerialize a result variable: couldn't read its memory
See some context from the source code of the program where execution is paused
The list
command will show where are you now. If there's no process running, it seems to print out part of fn main(){}
's code for me. Example:
(lldb) list
87 content: Binary(ref bytes),
88 } => {
89 let raw_id = four_u8_to_u64(bytes.as_slice());
90 let class = id_to_class(raw_id);
91 format!("SeekID ({:?})", &class)
92 }
93 Element {
Summary
To get ready to debug, make sure you have a debug build available. Run rust-lldb with sudo on the rust executable inside target/debug/deps/. The most important commands to debug a program are:
b
- set breakpoint(s) before running the programr
- start a new processlist
- shows the code where we pausedframe variable
- shows variables current state in the current framep
print an expression- CTRL-D - quits GDB immediately
Let me know about other tips you have on using LLDB in general or with Rust in particular!
Join the conversation on Reddit.