This is a follow up to Brian MacDonald’s post on Debugging for Beginners. I read Brian’s post avidly, as I am always keen to take a look at different approaches to finding those elusive problems that plague all programmers (even those with decades of experience) from time to time.
Anyhow, I have to admit that I was a little bit…disappointed. You see, Brian writes in a wonderful, readable way, about topics that concern all programmers, whatever their background. But, I found that the general focus of his article was less on how to debug (even at a higher, theoretical level), and more about how to make fewer mistakes.
Now, you could argue (as my husband did), that making fewer mistakes is the number one way to “debug” your code. Obviously, as you gain experience as a programmer, your skills should increase, common pitfalls are recognised and avoided, and you spend less time working through the problems and more time getting things done.
There are two flaws with this argument:
- It is somewhat likely that you will, at some point, have to debug code other than your own.
- It is somewhat likely that you will, at some point, introduce a hard to find bug into your own (or someone else’s) code.
As Brian clearly states, nobody is perfect, and neither is your code. He even says that being a skilled programmer means getting better at finding and fixing errors.
But then he talks primarily about how not to make those errors in the first place.
Debugging is a skill beyond just knowing how to compile with all warnings, knowing common language pitfalls, and using all the available help in your IDE of choice. It’s more than knowing who to ask.
Debugging is an armoury of intuitive approaches dependent on the type of bug you are facing.
Brian suggests two very valid points: printing variable values and printing messages to the console to confirm function entry.
This is the kind of thing I’m talking about.
These are real, practical skills, employable by all programmers.
This is the kind of thing I want to read more about. Much more.
So (oh you know where this is going, don’t you?):
My Top 10 Practical Debugging Strategies for Beginners.
1. As Brian says,
Or cout, or print, or echo, or whatever your language uses. Send some information to the screen. Check that your variables are the values that you expect. Stick a message in a function. It’s simple, and it’s (usually) quick. If this isn’t working for you, and you’ve got more printouts than code, or recompiling is just plain painful, then:
2. Learn to use a debugger — properly.
The upfront time in learning to use a good debugger (may I suggest GDB?) will save you tenfold, no, a thousand-fold, that amount of time across your programming career. Trying to use a debugger without having at least a vague idea how it all works will just frustrate you. Not only that, but the really rewarding debugging that you can do with a debugger can’t be picked up in 5 minutes. I can’t stress enough how much it is worth your time to learn to use your tool of choice properly (shameless plug – GDB Walkthrough).
3. Know exactly how to reproduce your bug.
This might sound obvious, but sometimes programmers head off into the code, looking for a problem that a client, or the test department, have complained about, without actually checking to see if they can reproduce it. If you can reproduce a bug most of the time, you can always find the source of the problem. Other tips: use the same source, or the same build, and the same set-up. Check if it’s a release build or a debug build. Bugs have a habit of cropping up between workstations/targets and between debug and released software.
4. Use core files and system log files.
Programs that crash leave evidence. Look through log files. Look through core dumps. Read the system logs and check for errors. You are a detective – leave no stone unturned.
5. Have a theory. Then prove it.
Even if you don’t have any sensible idea what might be causing the problem (graphics glitches – eugh – a low level programmers worst nightmare), take a guess. Develop a theory. Write it down. No matter how odd. Then go into the code base and look for evidence. Prove your theory. Or disprove it. Think about exactly what you can do, where you can look, to provide incontrovertible proof that you know what is happening. If your theory is completely wrong, the process of looking for proof often gives you evidence for a more accurate hypothesis.
6. Divide and conquer.
In larger programs, with layers and layers of abstraction, it can be surprisingly difficult to pinpoint exactly where the problem first occurs. Use a debugging tool and set a breakpoint before the bug occurs. This breakpoint is a snapshot of the program and its data. If all looks well, select a breakpoint around halfway (use your best guess) between the known good snapshot and the place where the bug occurs. Repeat. Binary search your way to the culprit.
Reduce peripherals, inputs and attachments until you can reproduce the bug with the simplest condition set you can find. Often, on a larger system, the sheer scale of data and connections obscures what is going on. Reduce, reduce, reduce. With a basic set-up, stripped down data, or less traffic, you will find it an easier job to debug. Often, as a side-effect of removing hardware, and/or data you will find a cut off point where the bug stops appearing. This in itself can help guide you to the source of the problem.
If your bug is one of those horrible, intermittent issues that only occurs when there is a full moon and a rook within 100 metres, it might well be worth automating the running of the program under a debugger. Any basic scripting language will be able to help you achieve this. Set your program to start-up repeatedly until you see the error. Set your program to run the same inputs repeatedly until the crash happens. Set up the GUI with repeated button presses until it dies under the pressure. Hint: a friend in the test department can be your biggest asset.
9. Compare builds.
This isn’t one of my favourites, but if a bug has suddenly appeared on a release and you are having trouble finding it, comparing the entire source tree can help give you a feel for where the changes are that have caused the issue. Warning: I mention this only because it is sometimes worth a try. On a large codebase it is time-consuming and demoralising to view all the changes that have occurred, but I have done this in the past and seen updates and “fixes” to classes that have helped me track down problems.
10. Don’t give up.
Debugging can be a tedious and drawn out process. Some bugs are the work of the devil (and yet so simple when you find them). Stick at it. Sleep on it. Fix another bug in the meantime. Do something else. Have a coffee and chat with a co-worker or fellow student. Clear your mind and then go back to it. So many times I have found a bug first thing in the morning that the previous evening I was struggling to understand. And the satisfaction of finally, finally getting to the bottom of the problem and solving it is awesome. And each time that happens, it makes you a better programmer.
Enjoy the hunt.