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 alongside rustc, cargo, and friends. Use rustup if you don't have rust-lldb; then make sure that $HOME/.cargo/bin is in the PATH.

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

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 program
  • r - start a new process
  • list - shows the code where we paused
  • frame variable - shows variables current state in the current frame
  • p 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.