Skip to main content

Command Palette

Search for a command to run...

Step by step interactive debugger for Rust

Published
โ€ข8 min read
Step by step interactive debugger for Rust
C

Hey, I'm Cristian Zapata ๐Ÿ‘จโ€๐Ÿ’ป Coming from graphic design and front-end dev, I've fallen in love with embedded systems โ€” microcontrollers and low-level programming. Lately I've been diving into Rust and exploring how to bring it into the embedded world. The best things never come easy, but the ride is worth it. โšก

At the beginning, when I was working as a frontend developer and made a change in the code, I saved, and instantly it compiled automatically and the results showed up in the browser. It felt almost magical and very simple.

But when I started programming in C, that changed.

Suddenly I was writing more bugs than features. I'd compile, run the program, and boom โ€” segmentation fault, memory leak, or worse, nothing happened at all and I had no idea what was going wrong.

The frustration of not knowing why something wasn't working led me to look for tools that could help me "see" what the program was doing and where it might be failing โ€” something that with low-level languages like Rust or C is a bit less "intuitive."

๐Ÿ‘๏ธ Visualizing to understand

One of the first tools that helped me was Python Tutor. It's a website that lets you visualize the execution of a program step by step.

For example, if you're learning what a pointer is in C:

int number = 5;
int *ptr = &number;

Python Tutor shows you how variables are stored in memory, what points to what, and how everything changes step by step.

It's great for understanding small, basic structures. But when your program grows and you have multiple functions, dynamic memory, separate files, and more โ€” it's no longer that useful.

๐Ÿ’ช And this is where LLDB comes in

LLDB is a debugger, which in other words is a tool that lets you execute your program step by step, see variable values, structures, the execution stack, and even errors like segmentation faults in real time.

It's part of LLVM (like clang is) and is similar to GDB, but more modern and with better support for readable commands.

Think of it as having a magnifying glass inside your executable. You can see what it does, line by line, and even pause time during execution.

In this post we're going to use LLDB with Rust, because even though Rust protects you from many errors at compile time, when something fails at runtime you still need to understand what's happening under the hood.

How to get started?

We'll need two things: Rust and LLDB. Let's install them step by step.

Since my main system is based on Arch, the installation guide will be oriented to set up the necessary tools for that distribution, but you can find the installation guide for Rust and LLDB for other systems here.

Installation

First we install Rust through rustup, which is the official Rust version manager:

sudo pacman -S rustup
rustup default stable

The first command installs rustup, the second sets the stable version of Rust as the default. This installs the compiler (rustc), the package manager (cargo), and all necessary tools.

Now we install LLDB:

sudo pacman -S lldb

Verify everything is correctly installed by running these three commands:

rustc --version
cargo --version
lldb --version

If all three commands return a version, you're ready to continue.

โš ๏ธ Common installation issues

If any of the previous commands don't work, here are solutions:

โŒ rustc: command not found or cargo: command not found

This means Rust was installed but your shell can't find it. You need to load the environment variables:

source "$HOME/.cargo/env"

If this works, add this line to your shell configuration file so Rust is always available:

# For bash
echo 'source "$HOME/.cargo/env"' >> ~/.bashrc

# For zsh
echo 'source "$HOME/.cargo/env"' >> ~/.zshrc

Then restart your terminal or run source ~/.bashrc (or the file corresponding to your shell).

Our first Rust program (Or not...๐Ÿฆ€)

Let's create something simple but that lets us see how LLDB works.

cargo new debug_example
cd debug_example

Now edit the src/main.rs file with this:

fn addition(a: i32, b: i32) -> i32 {
    let result: i32 = a + b;
    result
}

fn main() {
    let x: i32 = 10;
    let y: i32 = 40;
    println!("The total is: {}", addition(x, y));
}

It's a very simple program which adds 2 numbers and prints the result.

Compile with debug information

This step is key. For LLDB to see variable names, lines of code, and functions. In other languages like C we would have to add a flag for this, but when we compile a program in Rust it already does it in debug mode by default.

cargo build

This generates the executable in target/debug/debug_example. If we had compiled with cargo build --release, LLDB wouldn't be able to see much because the compiler optimizes and removes information.

๐Ÿ” Getting into LLDB

Before diving into the debugger, there are some important commands we should learn to navigate better.

๐Ÿ“‹ Basic commands

CommandWhat it does
break <function> or break <file>:<line> or b <function>Sets a breakpoint
run or rRuns the program until the next breakpoint
next or nExecutes the current line and moves to the next
step or sSteps into a called function
print <variable>Shows a variable's value
btShows the call stack
continue or cResumes execution until the next breakpoint
quit or qExits LLDB

Now knowing all this we can begin. The first thing will be to launch LLDB with our binary.

lldb target/debug/debug_example

And when entering LLDB we'll see something like this:

(lldb)

This is LLDB's command line and it's from here where we can control everything. For now the program hasn't started executing yet, we've only loaded it.

LLDB TUI Interface

LLDB also has a visual TUI (Terminal User Interface) similar to GDB.

To activate it, once inside LLDB:

(lldb) gui

This opens an interface with panels showing:

  • Source code with the current line highlighted

  • Local variables

  • Registers

  • The stack

You can navigate with the arrow keys and exit by pressing Esc or q.

Note: LLDB's TUI interface may vary depending on the version and terminal you're using. In some cases it might not work perfectly, but when it does, it makes debugging much more visual and intuitive.

Setting a breakpoint

Now we'll use the commands we saw earlier. The first thing we'll do is define a breakpoint which is nothing more than telling LLDB at which point we want it to pause the program's execution. It's like placing a "STOP" sign on a specific line of code.

Let's set a breakpoint on the addition function:

b set --name addition
b addition

Both commands will do the same thing so it's a personal choice which one to use. We can use the help b command to see the simplified syntax.

We can also make it much more specific by defining the file and line where we want our breakpoint:

(lldb) b main.rs:2

Running the program

Now we can start the program's execution:

(lldb) r

The program will execute until it reaches the breakpoint we defined earlier:

* thread #1, name = 'debug_example', stop reason = breakpoint 1 at main.rs:2
    frame #0: 0x... in addition at main.rs:2
 -> 2        let result = a + b;

The arrow (->) tells us exactly which line the program is paused at. At this moment the line let result = a + b has not been executed yet.

Seeing variable values

Now we can ask what the variables are worth at that exact moment:

(lldb) print a
(i32) 10
(lldb) print b
(i32) 40

LLDB shows us the data type and the value.

Stepping through line by line

To execute the current line and move to the next one we use next:

(lldb) n

Now the line let result = a + b has executed. We can verify:

(lldb) print result
(i32) 50

We can now see that result is 50. This is how we advance line by line and observe how the program's state changes.

Viewing the call stack

Something very useful is seeing where the function we're in was called from. That's called the call stack and we can see it like this:

(lldb) bt

And something like this will appear:

* frame #0: addition at main.rs:2
  frame #1: main at main.rs:9

This tells us we're inside addition (frame #0) and that it was called from main at line 9 (frame #1). When our programs grow and have many functions calling each other, bt (backtrace) is our best friend for understanding the flow.

Exiting LLDB

Finally when we've finished doing all our checks and so on to exit LLDB we do it with this command:

(lldb) q

Conclusion

Why use LLDB?

โœ… Visualize the invisible - See exactly what your program does line by line.

โœ… Find subtle bugs - Errors that don't cause crashes but give incorrect results.

โœ… Understand the flow - Inspect the call stack and how functions call each other.

โœ… Save time - Instead of filling your code with println!(), pause and observe.

โœ… Industry standard - Essential tool for low-level development in Rust, C, and C++.

If you're not using a debugger in your projects yet, now is the perfect time to start! ๐Ÿš€

๐Ÿ“ฅ Want to practice?

Here's a practical challenge with a non-obvious bug that you'll need to find using LLDB. Can you find the problem?

๐Ÿ”— Download the challenge โ†’