On Using Debuggers
plope.com
plope.com
This is the key statement of the original post.
If you don't understand the code, then you need to sit down and read it until you do.
Using a debugger can lure you into quick, localized fixes to make small problems disappear. If you just need to make a minor fix to an external library that you'll never look at again, this is probably OK. But if you own the code, you're in a rut.
Constant use of a debugger is a crutch that prevents you from deeply understanding a problem or discovering core issues.
In the long run, a debugger slows you down. Don't step through code to verify fixes; write automated tests. Don't set breakpoints to verify that a value is what you expect; write assertions to confirm that they will always be so. Don't step through new code to verify it is correct; write a unit test first or compose your new code from smaller, proven pieces.
When confronted with a bug, sometimes the best thing to do is just stare at the ceiling and think about what might be causing it -- quite often you'll find the right answer.
Some bugs -- the really hard, nasty ones -- may never be revealed by stepping with a debugger. Performance problems aren't shown by a debugger. Only understanding your system at a high level can inspire you to make fundamental changes in your code. A debugger blinds you to these.
For example, if your C compiler is generating bad code, or if you've violated some low-level assumption, it's hard to figure out what's going on by inspecting the source code. Sooner or later, the high-level abstractions break, and there's nothing you can do except single-step through the code that's going wrong.
I once found a bug in MSVC++ while compiling Quake 2. The compiler was performing a tail-call optimization, but it hadn't noticed that Carmack had stored a pointer to a local variable in a global variable. The stack frame was overwritten while the variable was still live, causing weird stuff to go wrong. This is not the kind of bug that you can quickly find by staring at C code. :-)
That being said, I'm planning on using gdb this afternoon to debug an x86 (16-bit) bootloader I wrote 4 years ago. It was part of a class project, and the prof ended up using it as example code. I think there are subtle bugs in it though...
A debugger can be a very fine tool for understanding how code you don't know works. Sometimes better than just reading it. Always better than litering the code with print statements.
I once had to work for a few years with a development environment that didn't have a debugger. At first, this seemed like it would be impossible.
But I learned ways to work without one, and now I find that I prefer them. Only rarely do I actually require a debugger. I do still pull a debugger out occasionally for particular needs, but by and large I find that I work faster with simpler tools -- or by just stopping to think for a moment before I reach for the keyboard.
A debugger gives you a detailed picture of the state of your program at one moment in time, or (when stepping) over a short period of time. Other tools -- logging, monitoring, etc. -- provide a different slice through your program state, making it much easier to observe the flow over time, but relatively little detail about any given instant. Each approach has its uses.
Logging has a number of properties that make it relatively more useful for experienced programmers, mature codebases, and/or "infrastructure" systems (compilers, operating systems, libraries) that are used far from the developer's workstation. That might explain its popularity among the group interviewed in this book. For instance:
1. Programmability. The nature of logging -- both generation and analysis -- lends itself to building up a library of utilities. An experienced programmer develops a toolkit -- part code, part knowledge -- that makes logging more and more useful to him/her over time. There's also a learning curve for debugging techniques, of course, but it flattens out sooner. I've been programming for 35 years, and my logging techniques are better than they were even 5 years ago, but my productivity in the debugger plateaued decades ago.
2. Accumulation. If you work on a single body of code for a long time, you can continually improve the usefulness of its logging. Eventually you reach the point where many bugs are quickly obvious from the log, because you'd previously invested a lot of effort in logging exactly the data you typically need, in the most convenient format. Printf debugging can take a long time if you're starting from scratch (the program contained no relevant logging to start with), but in a mature codebase where attention has been paid to logging, the story can be very different.
3. Universality. Logging tends to work in a wide variety of environments, from manual testing on the developer's workstation, to production servers, to installations on a customer's computer. If you spend your life tracking down hard-to-reproduce bugs reported by other people, logging is essential.
4. Repetitiveness. Tracking down problems in a debugger tends to involve a lot of boring, repetitive actions -- "aha, this is null because that other flag had been set, let me restart and walk through the whole thing again to see when that happens". It's still sometimes the best tool for a job, but it has a time-sink flavor that probably pushes some people away.
On the other hand, the only thing I use gdb for is cheating at nethack (+99 blessed Unicorn horn anyone?) and looking for buffer overflows. There are a few PHP debuggers out there but I've yet to get one to really work right.
My main trouble with printf-debugging in all it's forms is that you end up making changes to code that don't always go away. Even if you do it right 99% of the time, you'll insert 1000's of printf's over the course of a month and sooner or later one of those will end up visible to end users, will fill up your logs with junk messages, or otherwise gunk up the works.
Of course, there's something to say with fixing bugs by looking at the code ~thinking~. Yesterday I was tracking down a bug involving an area of code that I didn't trust, and ended up refactoring it, converting some properties in an interface into readonly methods of an abstract base class so I knew for a fact that certain variables wouldn't change after initialization.
This work convinced me that the code was wack, but when it was done, I knew the problem involved initialization of the objects. For a second I thought about using the debugger, but finding the specific case that was causing trouble would have been tough.
At that point I decided to trace the codepath involved and immediately saw the cause of the problem and understood the root cause (certain initialization code was duplicated in two places, which is just asking for things to break.)
I could have fixed the bug quicker by just following the codepath, but spending some time cleaning up code I was scared of will pay dividends in the future... And it's a good example of the tradeoffs you face when you fix bugs.
That said, I particularly find printf-debugging useful when something strange and difficult-to-reproduce is going on, particularly in a production system.
Might as well use wizard mode, and wish for that stuff.
I prefer playing on public servers, they take away that temptation, and give you more bragging rights.
I've had to deal with all of those specific issues in the last couple of months on our compiler+runtime, and there's sadly not a lot of tools in the free space to help with situations like that.
If you're making something of a reasonable size, you should be logging tons of stuff.
If you've made sure your logging is solid, often fixing a bug is just a case of looking in the log to see what went wrong.
Under ideal circumstances (i.e. not real world), no one would ever need to fire up a debugger, but (most of us) don't work in those conditions.
Can you create world-class software using only print statements to debug? Sure. Is that an efficient use of your time? Probably not.
The bottom line is debuggers are just another tool in the development aresenal, and dismissing them merely because you can accomplish the same task in another way is asinine.
When you have a question you want to answer about the state of a program, a debugger gives you powerful tools to answer your questions right now, but doesn't leave any permanent record of your investigation. Each time you subsequently want the answer to that same question you have to run the program, hit that breakpoint again, and cast whatever incantations are necessary to reveal the relevant program state.
If you used printfs instead, you would probably have had to iterate a few times to get the answer the first time; first you print one thing, then another, moving your prints around until they tell you what you want to know. When you're done, though, you've probably left some log output hanging around. So after you're done investigating, say, how the data access layer works, you have something to cross reference while you're poking around the user interface code.
I think this is a more useful technique for unfamiliar source. Comprehending a large hunk of source typically requires integrating your understanding of several sections of code ranging widely across the codebase.
Anyway - a good debugger is a wonderful thing. I can't stand up straight sometimes for leaning on them, though.
The integrated debuggers in IDE's are much better, particularly the Java ones. The C/C++ ones have the problem that they will work fine on code compiled with debug symbols, but on release code they're awful.
And when it comes down to it, the really hard to fix problems are the ones that only show up in the optimized release code.
I also sometimes find it useful when calling code I don't have decent documentation for. I can examine the various properties of the object I'm trying to work with, and see which ones contain useful information.
For me, this is also a wonderful way to get accustomed to someone else's code, seeing exactly what it does first before spending the brain cycles upfront to reason about exactly what is going on.
E.g. methods cannot be declared 'non-reentrant'. Data cannot be declared 'atomic'. Yes, you can hack together solutions using library methods etc. but the language doesn't help much.
We wrote an 'operating environment' for a major computer manufacturer once - a storage device they eventually cancelled (hardware was problematical). Programmers wrote objects that were each invoked single-threaded, passed workitem messages between services instead of directly calling. The environment managed message queues, handled all the threading and reentrancy invisibly.
The programming team loved it. They could write restartable, failoverable modules in this model without every dealing for an instant with concurrency explicitely.
Concurrency is only hard because it's a stupid thing to do - to give a bunch of instructions to the CPU and tell it to execute them in a random order.
I don't even care in which order my single-threaded code gets executed (if it's in the right language).
As some one else mentioned, I typically only use gdb for crash dump analysis. The one tool I do use a lot though is Valgrind (to check for memory leaks).
For large projects (especially large projects that don't have the files structured well) you can end up with very high compile and link times (> 2 minutes). Using a debugger in this case is much faster than incrementally adding print statements to the code.
Being able to see a callstack is also incredibly valuable on complex projects. The value of printing things out drops dramatically once you have multiple entry points into the same codepath.
Also, if you always run your program through the debugger, when you crash you can immediately diagnose and fix the problem without having to reproduce the crash. This is useful for hard to reproduce bugs.
Besides, it's not like this is an 'either or' type thing. You can use a combination of reading the code, debugging, and printing to track down problems.
The Kernel Traffic summary: http://kt.earth.li/kernel-traffic/kt20001002_87.html (sections 1 and 4 are relevant).
Linus' "I'm a bastard": http://lkml.indiana.edu/hypermail/linux/kernel/0009.0/1148.h...
Richard J Moore discussing IBM's experience: http://lkml.indiana.edu/hypermail/linux/kernel/0009.1/1307.h...
I haven't fired up a debugger for a long time, partly because I'm in to defensive programming big time. If you have something as simple as assert(ptr != NULL) in a function, if it fails, you get hit with an assert failed message, the file, line, function and condition. Most of the time this results in me thinking, "oh, duh" and immediately correcting the problem, sans debugger.
A debugger can be useful when debugging production code, especially when the heat is on. This is no time to stand on ceremony and idealism.
For learning new code, particularly frameworks ime, a step debugger is incredibly valuable. But you are not really debugging in that case.
A debugger is a non essential tool these days. But if the kernel boys said they found it useful, then I'd not argue.
The truth: Print statements work so well that they dramatically decrease the need for a debugger. Then when I actually do need a debugger, I have used it so infrequently that I forgot the commands, so I just use more Print statements.
There is absolutely no point single stepping through code, setting up breakpoints, inspecting variables etc, when you could instead just insert an extra log statement to see what is going wrong.
If your logging is good, often solving a bug is just a case of looking in a production log and seeing where things went wrong.
The other fact is that the majority of bugs I've seen in my own code have been caused when under load, and it's not feasible to run a debugger on a production system.
I've tried debuggers a fair amount, and they end up being completely useless to me.
I assume that statement refers to the amount of noise in question?
Sure, print statements are my normal tool, but often I'm working on transforming one data structure into another (compiler-style) and I'm encountering an error "somewhere" down in the middle. After the first few megabytes of logging output get generated, you start to really lose the thread of what's going on.
It depends on what you're doing. If you're a CRUD programmer, yeah, I can see how they'd be useless; that's a good deal of my job too and I don't use them then. But when I'm actually doing something interesting and twisty I often need them, if only to set some conditional breakpoints somewhere so I can get into the middle of a complicated process instead of trying to reconstruct the state later.
Also they are great for learning libraries by stepping through them.
A debugger is just a tool and to dismiss a tool as being pointless for all people is ridiculous. Being familiar with all the tools at your disposal as a software developer is never a bad thing.
Debugging helps you work with code you don't understand. So, the longer you work with the same code the less useful it becomes. However, plenty of coders move from project to project patch up a few problems and then move on.
I'd say not having a debugger is more like programming on paper. When you have a problem, you might end up thinking about it a little more because you can't rely on that one other tool.
I think it's more like never being taught how to use a knife, and thus eating the steak with your bare-hands, as you've always done. Hands work fine, why learn something new? I have no formal training in programming and it never really occurred to me to use a debugger. I still don't entirely understand what they do for you that you can't do by adding print/trace statements. That said I mostly use scripting languages and don't need to read too much code written by other people...
They allow you to evaluate state and often see things you'd miss otherwise.
Plus they can do what prints can do, but even easier. I can use tracepoints to print anything at any point in code, including the callstack. The nice thing though is I can turn the tracepoints on and off while never pausing/breaking the code.
Additionally I can attach a debugger to a running process.
Plus with more modern features, like historical debugging), I can go back to predefined instances of time (like a past exception that was thrown) and examine the callstack then.
And the other thing you can do interactively is (a) change the value of variables, (b) change the code, and (c) change the instruction pointer. I regularly do (a) & (c) -- (b) less so. It's a huge time saver when you step over function foo() and realize, hey it didn't set ABC. So then just set the IP back to foo(), but this time step into it.
When I would consult I would often find and fix bugs in a day or two with oode that I'd only seen for the first time, while others had been looking for weeks in code they had helped build. I'd look at their debugging techniques and it was obivous that they were often just not at all efficient.
That sounds better than what I tell others, that I'm not smart enough to use a debugger.
Debuggers let me get away with code that is on the edge of my understanding at the time that I'm writing it. The problem is that I'm not nearly as smart when I have to revisit that same code later.
In addition, debugging with print requires more understanding, so it's more likely to be correct than debugging with a debugger.
That said, I really liked my time in visual studio.
I don't believe this is true. I suspect the way most people use print statements for debugging goes something like: "I think the problem is somewhere in this function, let's stick a print statement here. Oops, that doesn't seem to be it, let's try one here. Oops, that's not it either, let's try printing out a different variable." and so on.
That's remarkably similar to using a debugger only a lot less efficient, because you have to go back through the whole edit-compile-run cycle each time (or maybe edit-deploy-run if you're working on a web app using a scripting language).
What is the basis for your suspicion? I ask because the folks who I see using print as their first-choice for debugging don't work that way. The overhead associated with changes (as opposed to restarting) gives them the opportunity to think (or grab a new cup of tea).
That said, that "poke around randomly" approach does seem to be "not rare" for folks who are "debugger first", as you yourself say.
To be fair, the folks I know who are most effective with a debugger also put in some time on understanding before they fire it up.
Debugger is more powerful, it can do everything print can, and more. Sure if it's a 5 minute walk to the market, then by all means, walk.
Since I write time-dependent C++ code I must test/debug the 'release' build. The debuggers universally screw up almost everything related to dynamic symbols (locals, parameters), so I have to read the assembler anyway to figure out what's where.
Naturally this makes it much quicker to use prints (logs actually) than struggle in the debugger.
Unless we suppose a compiler bug, reallocating registers around a print (ok, log) is not a problem. It's still an order of magnitude faster than the 'debug' build.
In the end, it means I know that section of code inside and out and can rewrite and/or refactor it as needed to solve the problem. It usually results in cleaner, more efficient code.
I've been struck over the years at how one of key benefits of the Smalltalk environment -- being able to explore/change things in a program while it's running -- is not more widely regarded in the field. I occurs to me now that the interviewed programmers might have already gotten much of the cognitive benefit of such exploration at the beginning of a project. For them, print statements and listings are a way of confirming details of what they expect to be happening as the code runs. For un-godlike programmers (and maintenance work), debugging is complicated by the lack of deep understanding of how the code is supposed to work, hence the need for more exploration time and debuggers.
In Python that's sufficient to catch most logic errors -- a full stack trace with values of all locals and member variables is enough to diagnose 90% of errors. When the error does not lead to an exception but undesired behaviour print debugging is easiest -- I have the software running so it restarts on source code changes and the restart takes a second and does not lose any state as it's a web app.
I could see myself using pdb if the Python software was a more complex server that took longer to restart and had some complex state that'd be lost on restarting.
If I was in VS.Net or Eclipse and not Emacs all day, and/or if I was writing something more "straightforward" like a desktop application, I bet I'd be much more inclined to use a debugger.
(Edit for word choice.)
I couldn't imagine coding Ruby or PHP with a debugger.
Maybe I've just not been incentivised enough to learn how to properly debug Ruby/PHP though.
Perhaps that is the problem. What are you doing working with, using, or writing code that you don't understand
To me this is a bit like the folks who randomly change their loop conditions until they get the desired behavior. Sure it works correctly now, but they have no idea why their changes made it that way.
A debugger is a great tool to help you understand unfamiliar code. Drop me in to a breakpoint in somebody else's codebase, and chances are I'll quickly be able to figure out what's going on.
Assuming you didn't write all the code in your organization, debugging is pretty much your most valuable skill. Be it looking at a stack trace and digging in by hand, or using the tools available to you. Your worth revolves around being able to immediately dig in to an unfamiliar system, find our what's wrong, and fix it.
Debuggers help you do that.
I use profiler a lot lately though (Java Visual VM).
wat