Do you use a debuggers much?

An idle question for anyone with a free moment and bored enough: Do you use debuggers much?

Off the bat, I don't. I don't think Debugger Driven Development is a good approach. Over the decades on the occasion I had unfathomable bugs to find it turned out the debugger/In Circuit Emulator did not help and often hindered progress.

I ask because I was just having long chat with a keen debugger user who insisted my resistance to using debuggers was down to my inexperience or ignorance of their capabilities. I could not really argue as my experience is limited, for reasons given above, and the experience was not good.

At a high level I might ask what a debugger is used for:

  1. You want to run some new functionality to see if it produces the correct results. Fine but that is what println!(), logging and assertions are for and it would be far better to create a tests to do that. A test is not ephemeral like a debugging session, it's there forever, ready to tell you when any changes break something. A test can cover a lot more cases than you will in a debugging session. A test leaves permanent results, one does not just trust the developer who says "I ran it in a debugger, the code is good, trust me".

  2. You have some huge mass of code, likely not written by yourself, and a bug as been found. So you break out a debugger. That is where I have had poor experience. When some UB somewhere randomly corrupts things in places in the code far away and it only does it once a month with some as yet unknown input at some critical time, finding the bug with a debugger is almost impossible. On occasion use of a debugger has caused the problem to hide, no chance to find it that way.

Having switched to Rust I find that tricky, unexpected problems in development have gone away, no misuses of pointers references, no out of bound errors, no undetected overflows, etc, so my motivation to hit a debug button is even less.

So, do you use a debugger much? If so why? Am I really missing a point as the guy in my chat insisted?

11 Likes

Other than as a tool to print the assembly of specific functions, I haven't used a debugger for years.

8 Likes

yes, but no. fun fact: I started using debugger before I really learned to program. but it's not for "debugging" purpose, it's for revere engineering. so I think I'm not a typical kind of debugger user.

I rarely use debugger for my own code, but when I do use them, I mainly use them as a live disassembler. so to some extent, I agree with you that debugger is not required.

one exception is when I wrote embedded code, I tend to use debuggers a lot. it's just very convenient to inspect the device states in a debugger. but I think the main reason is I'm not as fluent writing embedded code as writing PC code, so I need the help of a debugger.

3 Likes

When I worked prominently in C and C++, I used to use debuggers all the time. And that's because I needed them.

Since I switched to Rust a couple of years ago, I don't remember having opened a debugger ever since.

I guess the party you were talking to doesn't have experience with Rust. To be a decent C programmer, using a debugger is a valuable and necessary skill. In Rust, this is not the case.

14 Likes

I use debuggers to get a stack trace of a crash (C/unsafe code or stack overflows due to infinite recursion), but println! otherwise.

If I need to investigate UB, I write a #[test] and run miri. Sometimes I check output of cargo-show-asm to see what's going on.

9 Likes

It depends, but mostly no.

I feel like a debugger is most useful if I know roughly where the code isn't working, but not why, and there are a lot of possible culprits. Obviously, also nice if you can trap into running code at a point of failure.

On the other hand, I've recently been debugging some shaders, and let me tell you, being able to step backwards is amazing. "Wait... that's not right, where did that come from? *rewinds* Ooooh. It's just like I thought, I'm the problem!"

I also feel like my debugging tendencies are a result of spending years working in languages for which the debugger experience was painful or even borderline non-existent. I often don't bother trying to debug Rust because so often I can't actually look at anything because all the variables have been optimised out (even in debug builds), or the structures are just opaque to the debugger.

Oh, that reminds me of the other reason I don't use a debugger with Rust: Results are not exceptions! I really wish there was a way to trap the creation of Err variants. If you're staring at an unexpected Err from somewhere in a tree of calls, and you've already trapped all your own errors, you're basically down to speculative logging anyway.

5 Likes

Amusingly, I used Chrome's debugger today with Wasmer's JavaScript package (a wasm-bindgen package) to hack on our WASIX implementation. It was the first time I've had to use a debugger for WebAssembly running in the browser.

WASIX provides the greater chunk of an operating system (including fs, networking, multi-threading, and multi-processing), and in this case I was troubleshooting a weird bug where a syscall invokes an asynchronous operation which seems to make the thread become unresponsive. The debugger was useful because I could pause all web workers, look at the stack traces for each one, and figure out which syscall and file descriptor were causing issues.

Other than that, I might use a debugger once a year. Mostly for backtraces when you want to know how a particular library function is called and can't modify the original source code (e.g. to add a panic!() and print a backtrace). I used to use it a lot more for FFI and unsafe code, but tools like bindgen and cbindgen mean I don't need to write bindings by hand any more.

1 Like

What are you using that can debug shaders and also step backwards? Did rr grow some new capabilities or something?

With Go, yes. With Rust, no.

These days, I write debuggers as a hobby. I have little practical use for debugging [1] anything I write in Rust (similar to others here) [2].

But why write debuggers, you might ask? Because it's interesting work, and my targets do not provide source code even if I wanted to analyze it. Debuggers are great for binary analysis.

If your only bugs are logic bugs, debuggers are unnecessary and can be much more difficult to use than dbg!().


  1. I am deliberately ignoring dbg!() and other forms of "printf debugging" which should be used often. ↩︎

  2. There are rare occasions with miscompilations, and slightly more often some soundness hole turns into UB that requires an "inside look" at the machine code level. It's so uncommon that I can count the number of times I've needed to debug Rust code on one hand. ↩︎

3 Likes

I think this gets at the heart of why it isn't often necessary to reach for a debugger in Rust: most bugs in most domains in most Rust code are in fact logic bugs.

How do we know this to be true?
As is well-known by now, in legacy languages like C, there are all kinds of memory management issues (UAFs, double frees etc).
With those out of the way in Rust, the biggest class of bugs left is also the class we can't automate away as of now¹: logic bugs.

¹And let's be honest, it's unlikely that we'll fully automate those away, ever. Gödel's incompleteness theorem and the Halting Problem and friends imply that we won't be doing so by means of formal logic at least.

I'm actually still optimistic here. We used to just have fully-general pointers that we used willy-nilly and couldn't prove things about them. Then we figured out in Rust how to find a useful subset of pointers that we can check, and it's certainly incomplete in a formal sense, but that's ok.

So maybe we'll also find a useful subset of turing completeness where we give up on certain things (or require unsafe for those things) in order to be free of the halting problem and friends. After all, nearly all the code I ever write is supposed to terminate, and most of it should be "obviously" terminating.

Maybe one day we'll think of "accidental infinite loop" in the same category of "buffer overrun" :rainbow:

1 Like

I'm not so optimistic. For example first we have to sort out some simple arithmetic and loop like the Collatz conjecture - Wikipedia

1 Like

Technically, now that you mention all this, I recall that this is already possible today.
You see, when people say that real world computers are modeled by Turing Machines, whether they realize it or not, they're wrong.
Instead, real world machines are modeled by Linear Bounded Atomatons.

The main difference between a general TM and an LBA is that a TM has infinite amounts of tape to work with, whereas the tape of an LBA is finite (hence "bounded"). This means that contrary to a TM, by keeping track of the entire computational history of the LBA, eg termination can be proven.

So why don't we do it? Because it isn't practical in the sense that the amounts of memory that the computational history tracking likely would require is kind of Ridiculous™.

So you might actually be right in that new mechanisms eg control flow might help here.
That said, the engineering wouldn't be trivial, assuming it does ever become practical.

When I first started programming, I would use debuggers often, because it could be difficult to find and see the things I wanted to see using print-like statements. But since most languages started building easy ways to pretty-print objects, even deep ones, I usually just dbg!() (or the language equivalent) and run it. It's easier than trying to set a breakpoint exactly where you want to be.

Not to mention the immense frustration that can come from timing bugs, where using the debugger can slow your execution enough that the bug doesn't appear, which can REALLY scramble your noodle.

Indeed a practical computer cannot be a Turing Machine.

But does knowing that help? Does than not just move the problem from "Will this program ever terminate" to "Will this program run out of memory and crash before we get a result?"

This does not sound like much of an improvement in the situation.

It can be though: once we have machines with enough RAM to actually trace the computational history of realistic software, a lot becomes possible in practice that is now possible only in theory. The tricky part is knowing how much RAM will be enough for this; quite possibly for eg CLI programs modern hardware already has enough RAM for such a purpose.
If/once that is indeed the case, it's up to eg compilers to catch up and take advantage of this new possibility.

The thing I use a debugger for most these days is to debug Windows services that exhibit weird behaviors during early startup. I have a tracer set up that will log to a file, but in one instance I was having a problem so early that it never got to the point where it set up the tracing facility. The (remote) debugger helped me sort that out quickly.

Apart from that, I sometimes use a debugger when I feel I haven't gotten anywhere in a while trying to reason around the code to identify the source of a bug. I find that seeing things happen step-by-step can be very helpful.

With all of that said, the debugger used to be one of my most important tools back in my C/C++ days. I still like using them from time to time, but they are no longer part of my essential tools. These days, months pass without me using a debugger.

(Don't get me wrong though, I still think a good debugger story for Rust is important).

For my day job with C++, a debugger can be essential depending on the problem, though these days I find that I more often turn to various sanitizer enabled builds, sometimes combined with a debugger for particularly nasty cases.

In Rust I have rarely used a debugger. It has happened however. One issue that made me use a debugger less is that I felt that it was actually harder to use a debugger with rust than C++: gdb was particularly bad at printing many rust types in a legible manner, even standard library containers. And if I wanted to look into those containers it wasn't really able to do so.

Contrast that with C++ where there are python extensions that gdb loads to help with formatting and even to be able to implement indexing into containers from inside the debugger. That is very much needed for Rust.

And for enums or tuple structs I couldn't figure out how to refer to specific members from the gdb command line. It seems like anything where rust doesn't fit into the C/C++ pattern has pretty bad support in gdb.

However, some recent version of rust iirc added support for debugger pretty printers for types, so that may improve. Unfortunately it seems to be a completely different mechanism than Debug formatting so no idea how widely it will be supported. And I don't think it covers implementing select functions in the debugger (in particular indexing into containers).

1 Like

Not for stepping through code.

Even with C, I think a memory profiler like valgrind is more useful. That tool is a little picky with platforms they support though and doesn't seem to be updated much anymore...