jdb: The Java Debugger

The Java Debugger (jdb) is not something that we’ve typically needed in CS136, but its “cousin”, the GNU Project debugger (gdb), is something that many systems programmers heavily lean on to help write, debug, and explore code. We’ll explore jdb here, and then use it to examine some sample programs.

Debugger, what is it good for?

One of the main reasons to use a debugger is to, well, debug. Here is one example of a simple program with an “obvious” bug: we try to access the array strings at an index that is beyond its length.

// AIOOB = Array Index Out Of Bounds
// This class has an "obvious" error: we overflow the length of our array!
public class AIOOB {
    public static void main (String[] args) {
        String[] strings = {"array", "index", "out", "of", "bounds", "exception"};
        for (int i = 0; i <= strings.length; i++) {
            System.out.println(strings[i]);
        }
    }
}

To debug a small program like this, I may choose to insert print statements. I could also run my program, encounter a crash, and then look at my run-time error message (Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 6 out of bounds for length 6 at AIOOB.main(AIOOB.java:7)). With enough experience, we may be able to use that error message and the code near the line number where the crash occurs to reason about the cause.

However, as our programs become more complex, we will often find that the location where an error manifests itself may be very far from the location of the actual error. Worse, that distance may be far away in time, location, or both. NOT FUN!

jdb is another tool at our disposal in our quest to connect our programs’ symptoms to their causes. jdb lets us single-step through the execution of our program, one line at a time. At any point in our debugging process, we can pause our program’s execution to examine the state of our program’s memory, which lets us validate our assumptions and reason about the correctness of pieces of our code. Let’s use jdb on this simple program to demonstrate some of these features.

Using jdb

We must first compile our program (jdb helps us correct errors in logic, not in syntax; we must have a syntactically correct program to start). In addition to our normal javac command, we must pass information to our compiler so that it includes debugging symbols in our compiled code. jdb needs this extra information to maximize its usefulness. To do this, we use the -g flag when we compile.

$ javac -g AIOOB.java
$ ls
AIOOB.class AIOOB.java

Once we have our compiled program, we can run jdb on a java class. Here, we want to run jdb to step through the execution of the main method in AIOOB.java, which has the AIOOB class. We first start jdb:

$ jdb AIOOB
Initializing jdb ...
>

At this point we’re given a prompt (our prompt is > at first; later we will see our prompt changes). Our program hasn’t yet begun executing—we will use jdb to control what happens next.

Setting Breakpoints

We will first create what is called a breakpoint in our program. A breakpoint is a location in our code (e.g., line number, method) where execution will stop, and where jdb will give us control. To create a breakpoint, we use the stop command. There are several ways to invoke stop, including: * stop in <ClassName>:<line_num>. This version tells jdb to stop execution once our program hits the specified line of the program. * stop at <ClassName>.<methodName>. This version tells jdb to stop at the start of the method <methodName> inside the class <ClassName>. (If we overload out methods, we also need to specify the argument types so that jdb knows which version of the method we mean.)

Since we want to stop at the start of our main method, which begins at line 5 in AIOOB, we could say either: * stop at AIOOB:5, or * stop in AIOOB.main

$ jdb AIOOB
Initializing jdb ...
> stop in AIOOB.main
Deferring breakpoint AIOOB.main.
It will be set after the class is loaded.
> _

Running Our Program

Now we’ve set a breakpoint (and it is the only breakpoint we want to set), so we’re ready to run our program. We can use the run command (possibly with arguments).

> run
run AIOOB
Set uncaught java.lang.Throwable
Set deferred uncaught java.lang.Throwable
>
VM Started: Set deferred breakpoint AIOOB.main

Breakpoint hit: "thread=main", AIOOB.main(), line=5 bci=0
5           String[] strings = {"array", "index", "out", "of", "bounds", "exception"};

main[1] _

We’ve started execution of our program until we hit the line where we set our breakpoint. Then jdb paused our program’s execution, and ceded control back to us.

At this point, we can set additional breakpoints, examine our program’s state, or we can execute one or more lines of code.

Exploring Our Code

To get a sense of what our program is doing, we can use the list command to print the code surrounding our current point in the execution.

main[1] list
1    // AIOOB = Array Index Out Of Bounds
2    // This class has an "obvious" error: we overflow the length of our array!
3    public class AIOOB {
4       public static void main (String[] args) {
5 =>        String[] strings = {"array", "index", "out", "of", "bounds", "exception"};
6           for (int i = 0; i <= strings.length; i++) {
7               System.out.println(strings[i]);
8           }
9       }
10    }
main[1] _

The => next to the number five shows that we are currently stopped at line 5. To execute that line, we can say step, which executes the line and moves to the next line even if that line is inside a different function, or next which executes the next line in this function by resolving the entire expression on the current line (which may call several other functions, yield return values, etc.).

If I instead wanted to continue executing my program until I hit the next breakpoint, I could use the cont command (short for continue). Let’s try that next:

main[1] cont
> array
index
out
of
bounds
exception

Exception occurred: java.lang.ArrayIndexOutOfBoundsException (uncaught)"thread=main", AIOOB.main(), line=7 bci=49
7               System.out.println(strings[i]);

main[1] _

Continue executed my program, which printed out some strings, and then it encountered a crash. However, jdb did not stop! I am still able to interact with my program to see what is going on!

I cane use the print, command to evaluate expressions. I can also use the locals command to see the state of all local variables in this function. Since my exception gives me some clues as to what is going on (array index out of bounds is the name of the exception…) I may want to examine the features of the array (perhaps strings.length), as well as the value of the index variable (i) that I’m using.

main[1] print strings.length
 strings.length = 6
main[1] print i
 i = 6
main[1] print strings[i]
java.lang.IndexOutOfBoundsException: Invalid array range: 6 to 6
 strings[i] = null
main[1] _

This tells me what I need to know! I can’t use index 6 to access my 0-indexed array that only has 6 elements.

Since the locals argument is very useful for giving me complete information all at once, here is that output:

main[1] locals
Method arguments:
args = instance of java.lang.String[0] (id=419)
Local variables:
strings = instance of java.lang.String[6] (id=420)
i = 6
main[1] _

There are many other useful features in jdb. If you want to explore its behavior, the best way to do so is to try it out. Take one of the programs you’ve written this semester, compile it with the -g flag, and then run it using jdb. The where, up, and down commands are particularly useful. Try them out! If you want to know more about any command, you can type help at the jdb prompt for a list of all the options. You can also use the man jdb command on the terminal. This will open up the jdb Unix manual page. You can navigate with the arrows and exit by typing q.

Explore and enjoy!