The most important part of debugging is to make sure we understand the problem. Before jumping into the debugger, we need to ask lots of questions to make sure we know what the problem is: "Are you sure you're using the function correctly? Can you send me a test program to demonstrate the problem? What version of the operating system are you using? Does the bug only occur on multithreaded code, or on systems with non-default I/O drivers, or anything unusual like that?"
With luck, one of these questions will point out something useful—maybe even something useful enough to make the engineer realize, "Oh yeah, I wasn't thinking about multithreading when I wrote that code; I bet the problem is I didn't protect my variables with a semaphore.…"
But after clarifying the problem, are we ready to start stepping through code with the debugger now? Not yet. Next a good engineer would suggest trying things like the following: "I'd check if the bug sounds like it might be caused by some other bug that we'd already found and fixed: I'd check the bug tracking database or ask my teammates to see if any of them were familiar with the issue—maybe someone else is already working on a fix."
If that doesn't work, should we start stepping through the code now? We could; but we'd probably be better off trying to reproduce the bug first. After all, if we can't even reproduce the bug, then it's unlikely that the debugger would show us the answer. This is when a great interview candidate will say something along the lines of, "I'd write the simplest possible program I could to test that function and see if the bug occurs. If not, I'd try to figure out what's different from my test program and the client's program since that gives me a clue about where to look."
And of course, we give bonus points to the person who notices that most I/O library functions were probably handwritten in low-level assembly language, and since we're writing the compiler, it's somewhat unlikely (though not impossible) that the bug is ours because that assembly language code doesn't need to be compiled. Instead, we should first check out the assembler or the coding of the function itself, perhaps.
But in any case, next we'll spend a few minutes thinking about all the information we've gathered. Many times, a theory will spring to life: "Hey, I bet it's XYZ! Let's see, how can I prove that?" If the theory is correct, then we just saved ourselves the tedious work of playing in a debugger. Of course, fate isn't always that kind, and we often end up in the debugger anyway. But at least then we have a clear idea of what the bug looks like, when it happens, when it doesn't happen, and we probably even have a theory or two about the general area where the bug is occurring. We can start our search there rather than stepping through the code from the very first line to the very last.
Whew! That sounds like a lot of work, and there's a good chance we still aren't done. Was all this effort worthwhile? Sure, we know more about the bug now than we did when we started, but does that justify the extra time spent? Many day-to-day, run-of-the-mill bugs are actually fairly easy to solve and just don't require deep analysis. There are hard bugs that take days to find, but most bugs are often a simple typo that you'll see as soon as you open up the debugger, or maybe you forgot to increment a loop counter. Given this, is it worth spending the extra effort on every bug that comes in? Why can't we just dive into the debugger, at least sometimes?
First, you'll do well to develop these other debugging skills for times when you can't use the debugger. When would that be? Sooner or later, a customer will report a bug that occurs on his or her machine but that you can't reproduce. In some cases, you might be able to use Visual Studio .NET's remote debugger feature, but it has some severe limitations, as we'll see later. You should develop the ability to ferret out these irreproducible customer bugs by logic alone, or else you'll soon have a lot of angry customers to deal with. And then you'll have no customers to deal with.
But that's not the whole story. Here's the real answer to the question of whether it was worthwhile to spend so much extra time thinking ahead before starting our debugging: Sometimes, it is not. Most debugging books insist their 12-step method always works, but the truth is that nothing works all the time. Sometimes, the extra effort spent attacking the bug from multiple angles really is wasted. Now, most developers would be far better off if they did more planning before debugging the majority of their bugs, but I'd be lying if I said it was never appropriate to use brute-force debugging.
Simple bugs often call for simple solutions. Experience and intuition are the best ways to tell which bugs need to be studied and which bugs can be attacked head on, and the best way to gain experience and intuition is to learn from the experiences of others. Capturing war stories of some of these debugging experiences is the main goal of this book.
Too many debugging books focus on obscure debugging tools: how to use a kernel debugger, how to read .NET's assembly-like language (MSIL), how to play tricks with the register display window. Unfortunately, those obscure tricks are worthless without real-world examples of how to use them. In the end, the engineer is given a report of a nasty bug—sure, you can look at all the register values and read all the MSIL code you want, but then what? How do you begin to track down this bug that's eluded your team for two weeks? How do you track down this bug that you might not even be able to reproduce on your computer at all?
Microsoft's .NET initiative provides the perfect incentive for developers to expand their debugging skills. Not only is Visual Studio .NET full of new features that simplify what used to be tedious debugging tasks, but it also enhances developer productivity so you can write far more code each day than before. Remember how frustrating MFC message maps and DDX were in Visual C++? Remember how annoying all that extra "plumbing" code to get COM objects working was in Visual Basic? The great thing about .NET is that it removes most of these headaches— now you can just focus on the code for your business logic, and all the GUIs and component stuff will just plain work the way you'd expect on the first try.
This book assumes you already know the basic tools, and that you've used a debugger and you know how to set a breakpoint. It also assumes that you don't need a refresher course in whatever language you program in. Instead, let's look at real-world bugs we've seen, the debugger tricks that were used to solve these bugs, and how we recognized which approach was appropriate. Let's trade real-world examples of hard-to-track-down bugs we've encountered and what steps we used to solve them. Let's analyze what information we had, what we were thinking at the time, what we learned, and what was the actual cause of the bug. Basically, let's talk about becoming a better debugger.