How do I Debug my Program?
This is a critical skill in programming, and you must learn how to do it well. It is largely an art; there is no single way to do it, and the best way to learn it is to do lots of it. Here are some ideas to get you started, based on our experience.
The Zen of Debugging
The biggest obstacle to overcome is your belief that your code is right and the computer is somehow wrong. Compiler and library bugs exist, but they are very rare compared to programming errors. So, 99.99999% of the time the computer is doing exactly what you told it to do, and you just told it wrong. Your task is to find the wrong thing you said - it is in there somewhere!
The overall best way to find a bug is to gather information about the conditions under which the bug happens and exactly what the bug does. There are a variety of ways to do this, such as trying different input to see if there is a pattern in the bug's behavior, or checking program flow and variable values.
As you come to understand the bug, you can begin to deduce what parts of your program are relevant, and start to zero-in on ever smaller parts of your program. Eventually you can ask "What does this line of code do compared to what it is supposed to do?" and you will find the exact wrong thing in your code.
Most of these are "stupid" small errors or typos that passed the compiler, but expect many of them to be "profound" errors, especially if you are learning the programming language and the software concepts.
The overall best way to gather information is to use the debugger. That's why professionals use them. The debugger is your friend! Learn how to use it, and how to use it well! It will repay your learning time investment with truly extraordinary interest.
Consult the course materials and your IDE documentation and us, for help with learning and using the debugger.
What We Expect
Avoiding learning and using a debugger is shortsighted, unprofessional, and, frankly, stupid. We expect you to be using a debugger on all your projects. When you ask us for help finding a bug, we expect that you have already used the debugger yourself and have gone as far as you can.
Using the Debugger
There are two approaches depending on your program’s misbehavior. If your program runs without crashing, then it is happily miscomputing thing but running apparently successfully. The basic technique in this first situation is to use the debugger to watch the program do its work, by stepping though the program one statement at a time.
A graphical debugger will show your source code with an arrow pointing to the current statement. In additional windows you can see the current values of your variables. You can hit the control button to advance by one statement, and then look to see what variables changed - they are usually highlighted! Additional buttons allow you to step through a function call statement without stepping into it, step into a function, finish a function call and return to the top level.
Using these neat capabilities, check the program flow and the values of variables. See if the program goes where it should, and that input values, function arguments, and the results of calculations are what they should be. As soon as you see something that is wrong, stop and try to figure out what caused it. As needed, restart the program and go through that section again and in more detail. To save time, use breakpoints and the step-over buttons to skip through parts of the program that you know to be good. For example, once you have concluded that something is wrong in your defrangulate function, you can set a breakpoint on its first statement, and then tell the program to start running. The debugger will then automatically halt on the breakpoint statement, thus taking you right to the point in the execution you are interested in. Once you have learned the debugger, using it this way is much faster than littering your code with print statements and trying to track the spew of output.
The second situation is that your program is crashing - it gets terminated by the system. Normally, if you start the program running under the debugger, and then let it go ahead, the debugger will automatically get control at the point of the crash-causing event, and you can see where the problem is happening and examine variable values at that point. Sometimes the crash-causing event happens in system or library code, and you see strange stuff - even assembler language - in the debugger display! Don’t panic! The problem is usually that you called a library routine (say strcmp) with some invalid parameters (like a zero pointer), and that’s why the library code crashed. You can have the debugger show you a list of the calling stack frames in effect when the crash happened, and pick the most recent one that is your code, and have the debugger switch contexts to that function. Then you can see what your code was doing and what it had in its variables that led to the crash. If you conclude the problem was in the calling code for that function, you can move up the stack again, or even start down from the top!
What to do When you Find the Bug
Once you find a bug and understand it, go ahead and fix it right away, and recompile and rerun the program. If you have more than one bug, they tend to interact in really nasty and confusing ways. Don't waste time trying to figure out all the perverted things a screwed-up program is doing. It is almost always better to remove each bug immediately rather than try to understand what is happening with multiple bugs.
However, if your program has a zillion bugs, and really is a screwed-up mess, the problem may be more serious. Often your approach to the problem, your program design, or your understanding of how to write the code is fundamentally flawed. Take a break and review what you are trying to do - is there a bug in your ideas or your knowledge?
Once you understand what to do, sometimes it is easier to throw part or all of your code away and start over, than straightening out a mess you made before you understood the problem or how to write the code. Happens to us all the time! "Plan on throwing the first version away!" is a slogan in software development.
Always run your program in debugging mode from the beginning, and keep it there until you are pretty confident in your program. Your new program almost certainly has some bugs, so why not start with the debugger already available? The debugger can prevent certain system crashes caused by some bugs, saving you time as well.
How does the debugger work?
The debuggers we want you to use are "symbolic debuggers", and are very sophisticated programs in their own right. Normally, the compiler and linker throw away all information about function and variable names (the "symbols"), leaving just the raw machine instructions. When you put your project into debugging mode, the compiler and linker arrange to save the names and memory locations of all variables and functions in special data files for use by the debugger, and insert special "break" code into the compiled program to mark where in the machine code each statement starts and where each function call is made.
When you run your program under the debugger, the CPU encounters the "break" code inserted at the beginning of the machine code for an original program statement or function call. The break code calls the debugger, which then does whatever you've specified with your debugging options or functions, such as pausing before executing the statement.
Using the information saved during compiling and linking, the debugger can open the appropriate source code file, display it in a window, and show you which statement produced the machine code that is about to be executed. Using other saved information, it can also show you the contents of the memory locations corresponding to the variables currently on the run-time stack.
There are some important implications of how the debugger works: First, although the basic features are pretty reliable and easy to use, it is a very complicated program with lots of extra features. So realistically, you should expect some bugs and quirks in the debugger itself. Second, the debugger actually modifies the size and locations of things in memory compared to a normally-running program. In some cases, the exact symptoms of a bug can change when you use the debugger, especially when you have problems with bad pointers and arrays, whose symptoms are hypersensitive to exactly where things are in memory. So, if using the debugger changes the bug symptoms, suspect your array and/or pointer code!
Why is it called a "bug"?
The story starts in the 1940s, in a U.S. Navy computer project using one of the first general-purpose computers, the Mark I, which was an electromechanical computer predating even the ENIAC. The programmer found that a problem was due to a dead moth stuck in a relay in the machine. Removing the bug allowed the computer to run as programmed. The programmer, U.S. Navy LTJG Grace Hopper (later Admiral), a famous computing pioneer, preserved the actual bug in her project notebook where it can be seen to this day:
Adm. Hopper developed one of the first compilers, led the development of business applications of computers, and co-invented COBOL, perhaps still the single most heavily-used computer language because it was the first to directly incorporate language features tailored to the domain of application. In fact COBOL worked so well that the many programs written in it were still in regular use after decades (longer than anyone expected), which directly contributed to the Y2K problem - nobody expected that you needed more than two digits for a year!