Tutorial 7: Debugging

Today's lab is a blast from the past on debugging; we include it because it gives an introduction to jdb, the debugger that comes with the JDK. jdb can provide a more convenient method of debugging programs than the manual approach involving generous sprinkling of System.out.println calls throughout your code.

The first section covers the manual sort of debugging you already know about, but also the additional idea of having a special debugging log class that allows you to turn debugging output on and off in your programs without having to change the source code all over the place.

The second section covers some of the basics of using jdb, the Java source-level debugger. jdb is reputed to be more stable in JDK 1.2 than in previous versions, but if you try it out on the upcoming assignments and find that you have problems using it, you can always fall back on the debug class idea.



Introduction to Debugging

Debugging, as you are probably aware, is the process of removing errors from programs. Typically, at this stage, the program compiles and may even run, but one or more errors prevent the program from functioning properly on some or all inputs.

Ideally, programs are written correctly the first time and therefore never require debugging. Almost as ideal is to catch the errors in the program simply by thoroughly examining it, usually running it on various test cases in your mind and/or verifying invariants about what should be and must be true at this point. Evaluating the correctness of a program by examining the code is known as inspection.

Unfortunately, most of us have difficulty writing a correct program from scratch, or even removing our errors by inspection. This is particularly difficult in larger programs, or even virtually impossible unless programs are written with careful attention to modularity and abstraction. Therefore, programmers learn a variety of debugging techniques that involve actually executing the code on a computer.




Rudimentary Technique: Print Statements

A rudimentary debugging technique programmers are often introduced to early is debugging through the addition of print statements to a program. These statements are strategically placed to show the flow of control and the values of key variables. The output produced may make the problem obvious or be used to successively narrow down the problem location.

Although inserting print statements is a simple technique that can be used on virtually any system, it has the major disadvantage that the programmer has to specify what to display before executing the program. If one realizes that key information is not displayed, the program usually has to be edited to add additional print statements and then recompiled and restarted. A "shotgun" approach that displays any potentially relevant data may be employed instead, but significant time will be required to make sense of the large quantity of output.

While debugging using print statements is usually not the best technique for finding bugs, it may be useful in a context with limited interactivity or language support. Systems currently in use often support logging status data to a file, so as to provide at least some information about what went wrong if a problem occurs. Likewise, if software support for more sophisticated approaches such as interactive debuggers is not available, logging status output to a console or file may be useful.

If debugging using print statements is done, many programmers like to be able to control the output without having to modify each print statement. Here's a simple way to do that in Java:

class Debug {
        public static boolean on = true;

	public static void print( String s ) {
		if (on) System.out.println(s);
	}
}
Use Debug.print() inside this file much as you would have used println():
Debug.print("We're here!");
Allowing your Debug class allow logging to a file is straightforward.


Advanced Technique: Using an Interactive Debugger

Good development environments provide a tool known as an interactive debugger, or more simply, a debugger. Debuggers allow examination of the status of a program that is currently executing. This provides one with a "window" into the operation of a program, through which control flow and variable values are visible whenever sought.

The Java tools we're the using, known collectively as the Java Development Kit, include a debugger called jdb. It allows us to do many useful things, such as examine variable values, see which methods are currently executing and who invoked them, and set breakpoints to make the program stop at various points.

Be warned that jdb is sometimes buggy. However, the same concepts covered here will apply to using other debuggers as well.

  • When compiling with javac, use the -g flag to include debug information: For example, javac -g DoubleList.java

    (You may still use jdb with code which hasn't been compiled with the debug flag, however you will not be able to (for instance) print the value of local variables.

  • jdb DoubleList
    starts jdb and loads the class DoubleList. You can get the same thing by starting jdb with no arguments and then using the load command (described below) to load the class DoubleList manually;

  • Tell jdb to stop every time a certain method is entered and exited:
    stop in DoubleList.main
    This is a breakpoint; as jdb runs the program for you, it will stop at any breakpoint you have set. You can set several different breakpoints.

    You may stop at DoubleList:88 to set a breakpoint at a particular line of DoubleList.java. (Note that jdb is very particular; if you use stop at on a method or stop in on a line number you will get a very unhelpful error.)

  • run DoubleList
    begins execution at the main method of DoubleList. To prevent execution from running its full course (and then terminating jdb) you need to use stop in or stop at to set breakpoints first.

  • list
    will list the source code and show which line is about to be executed next.

  • next
    executes one line of code, and then stops again. Note step steps into the code; i.e., executes the next source statement at some level, delving into method invocations instead of jumping over them like next does.

  • list
    (to confirm that next went only one line.)

  • next
    (once more, to set up myList so it has something to print.)

  • print myList (General form: print var)
    Print out the value of a variable. If var is an object, the toString method should be used (though I did find one case where this doesn't always seem to be true; jdb is still a bit buggy).

  • dump myList (General form: dump var)
    Prints out the value of a variable, but in a more detailed format that does not rely on the toString method. Object references will appear by name and have their values (memory addresses) printed at hexidecimal values.

  • cont
    Continue running the program until the next breakpoint is hit.

  • where
    List the current execution stack, that is find out not only where you are, but why. The stack shows not only which method is currently running, but where this method was called from, where that method was called from, and so forth.

  • up
    Move debugger scope up a stack frame. That is, have it show variables and position in the method that invoked this one. down is the inverse command.

  • Use list again to see where you are in the higher level.

  • help
    A summary of more commands, though the above should be the most helpful.

  • Other useful commands:



  • Advanced puzzle:
    (For those of you who intensely bored with this lab because it contained nothing you couldn't have figured out yourself in a couple of minutes. This particular question requires considerable background.)

    In our example, the initial debugger prompt is:
    >

    Later it becomes:
    main[1]

    Why does it change, and what do main and 1 signify? (Hint: what else could be listed there? Think about the fact that you'll probably never see them change to any other values (at least in any Java program we'll write in Comp212).)