jdb
: The Java DebuggerThe 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.
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.
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.
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.
> _
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.
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!