I want to write a tutorial on how to use lldb to debug problems in Rust code. The primary goal is show off how to use breakpoint commands that run python scripts to parse out data, and use these outputs to help analyze problems. I want the problem to be somewhat non-obvious, be 100% deterministic, not be trivially caught by Rust itself, and also not require much code.
I quickly realized that Rust makes my classic go-to debugging examples somewhat difficult to implement in a non-convoluted way. So today I learned that unsafe is also a tool for people who are actively looking to implement bugs.
I'm not really asking anything -- I'm was just reflecting that Rust's borrow checker and bounds checks makes creating debugging tutorials more challenging than they used to be.
Although, I would take any suggestions for good bugs to debug in a tutorial. The one I came up feels too artificial, it would be better to show off something that developers might actually run into in the wild. The core idea I want to demonstrate is how one can use python scripts to parse and evaluate data, but other than that I'm open to any suggestions.
I don't really have any good concrete ideas, but baking in some assumption of std::mem::size_of which generally does the right thing but sometimes requires you to explicitly turbofish or 5_u8, or fails to work in some case like a zero sized type might be fun, since it binds together logic errors with inference and implicit default types
He (Amos) has a nice way of writing, often discussing Rust-related issues that he sometimes debugs with gdb, like in this article: A Rust match made in hell
pub fn new() -> Self {
RingBufU8 {
readloc: 0usize,
writeloc: 0usize,
buf: [0u8; RINGBUFU8_SIZE + 1],
}
}
/// Peek at two consecutive values at position 'idx'. 'None' is returned if
/// there is nothing stored there.
pub fn peek2_at(&self, idx:usize) -> Option<(u8, u8)> {
let len = self.num_elem();
// We check twice to keep track of a potential overflow situation.
if (idx < len) && (idx.wrapping_add(1) < len) {
let spot1 = (idx.wrapping_add(self.readloc)) & RINGBUFU8_SIZE;
let spot2 = (idx.wrapping_add(1).wrapping_add(self.readloc)) & RINGBUFU8_SIZE;
return Option::Some((self.buf[spot1], self.buf[spot2]));
}
Option::None
}
This is a simple method out of my small ring buffer implementation. Notice that doing a peeking at 2 consecutive spots from a positional offset requires 2 checks. With just 1 check, idx.wrapping_add(1) could wrap-around to 0, when idx is usize::max, thus end up less than len.
Maybe you can implement the wrong way, then try to debug it with a test case.
Maybe too obvious but loops without iterators. Breaking out of a loop with a count ( good old one off errors) or some strange conditionals. Loops in loops with breaks at different levels and maybe labels. Those are always fun to follow.
And maybe mix in some nested matches with the flow 3 or 4 confusions deep so figuring out what happens manually is hard.
Just today, I produced a segfault from a library which passed a &mut reference to the user, not considering that the user could std::mem::swap() it out for another one. It's one of the myriad pitfalls of using unsafe blocks near callbacks.
How about implementing some simple algorithm incorrectly, e.g. binary search? After all, you don't have to have segfaults and stuff like that to demonstrate benefits of debugging.
I'm curious to know what your classic go-to debugging examples look like. Perhaps you could provide an example in whatever language.
A while a go I was playing around creating graph structures in Rust. Rather than link up nodes with pointers, as one might do in C, I put all the nodes in an array and linked them up with array indices. See: When is a linked list not a linked list?
This technique keeps the type checker and borrow checker happy. However it allows one to introduce all the same kind of bugs one can create by messing up pointer manipulations in C, for example. The only difference is that one is messing up the graphs by incorrect array indices rather than incorrect pointers. Rust will not help you find those bugs at compile time.
For a logic-level bug, I submit my first (and near only) bug I encountered when writing my first non-tutorial app in Rust: I was trying to print things to specific locations on the screen, and in copy-pasting, accidentally wrote (x, x) instead of (x, y). This led to everything being printed along a single diagonal.
In my personal experience, I have only ever needed a debugger for certain difficult bugs caused by unsafe code. If you don't mind that, I may have something to offer.
All these are from the way I personally decided to learn Rust several years ago: take a C library of about 12,000 lines, run it through a (no longer maintained) Rust transpiler, and then clean up the 100% unsafe code into something much nicer.
While I can't say I would recommend it -- and only got 60% of my library done by API coverage -- I made certain mistakes repeatedly:
Converting pointers to slices required manually understanding the memory boundaries the slices should have. When I guessed wrong, it would segfault.
Using the Rust standard library -- e.g. replacing a hand-written C linked list with Rust's Vec<T> -- resulted in some interesting pointer provenance issues if pointers to the inside data "escaped."
Data structures often had a close() method which would free internal heap structures (because C heap is manually allocated). Once I converted these to Drop trait methods, there were interesting double-free bugs -- caused by an explicit close() call I forgot to remove.
For user-allocated structures, there was mixing of "Rust heap" and "C heap." Remember how things like Vec<T> have docs which require their memory needs to be allocated by Rust? What if it isn't because you got your memory buffers confused? Weird things, it turns out!
They are all pretty niche, I will admit. But all of these bugs were very consistent, had stable behavior (though the last one had weird edge case behaviors sometimes), and required some serious analysis.