分类:
2008-07-02 20:27:40
JDB Debugging Tutorial
By:
For:
Last Modified: 18 October 2005
|
This tutorial is designed to show how to use JDB (the Java Debugger) to find and fix errors in Java programs. It assumes no prior knowledge of debugging tools or techniques, and so should be most useful to programming beginners. Any programmer who has not had experience with a command-line debugger (or with any other debugger, for that matter) may find it useful, however.
The tutorial is split into two parts. Part 1 (this document) describes the basics of debugging and includes a lot of conceptual detail behind the most important JDB commands. covers a wider range of commands, but offers less detail for the rationale behind them.
Any debugger, whether it be JDB running at the command prompt or a graphical debugger running in a fancy development environment, is designed to let you do two basic things:
A variety of tools inside JDB can help accomplish these things. That's what this tutorial explains.
Before we begin, it's important to distinguish between the two kinds of errors you can have in a program. Compiler errors occur when you first compile your source code, and identify basic syntactic mistakes. Believe it or not, they're the easy ones to fix. If javac says Program.java:15: ';' expected you know that somewhere on or around line 15 there's a problem. Even if it's not really a missing semicolon at least you know where to look.
The errors that are hard to fix are the kind that occur as your program is running. You get it to compile without any errors, but when it runs it doesn't behave as you expect. Those are called "runtime" errors, and the debugger exists to help correct them.
Let's start by looking at an extremely simple program that has a very simple bug, just to get the hang of this. Mosquito is a program that's supposed to get two numbers from the user, multiply them, and display the result. This program "compiles clean" (no errors, no warnings) but it does not behave as expected. Here's an example of what it does:
Prompt for the numbers: Display the result: | |
Sample Run of Mosquito
|
Clearly something is wrong, even though it compiled without any trouble. First, the numbers it's supposedly multiplying aren't the numbers I entered. Second, the product of 2 and 0 is not 2.
The code for Mosquito is below. Copy and paste it into a new file, compile it, and run it to see how it works. You can probably identify the bugs just by looking at the code, but leave them there. The point is to see how the debugger can help.
/** Mosquito: Buggy Program #1 |
First, compile your program with javac -g Mosquito.java. The "-g" option tells the compiler to include extra information the debugger is going to use. If you forget and compile the program normally, some debugger functions will still work but others (like looking at variables' values) will not.
Next, instead of running your program with the java command, start the debugger by typing jdb Mosquito.
H:\>javac -g Mosquito.java
H:\>jdb Mosquito
Initializing jdb ...
>
Notice that the DOS prompt (H:\> in this example) is gone, and just a > prompt is left. This is the JDB prompt where you'll type debugging commands. For example, type help to get a list of possible commands. Some of the commands listed will make absolutely no sense, but others will sound perfectly reasonable. We'll explore the most important ones in this tutorial.
The whole idea behind the debugger is to execute only part of your program at a time. Otherwise you'd just run it the usual way. The first thing we need to do, then, is tell the debugger to "break" (stop executing and wait for your command) at some point in the program. That, logically enough, is called a "breakpoint."
You can put a breakpiont on any executable line of Java code (so not a comment or something like an import statement, but any line of code that actually does something). To start, let's just put a breakpoint right at the beginning of main() by typing stop in Mosquito.main.
>stop in Mosquito.main
Deferring breakpoint Mosquito.main.
It will be set after the class is loaded.
>
In so many words, the debugger has agreed to set that breakpoint just as soon as you start running the program. Without further ado, then, let's run the program so it can get on with it. The command, shockingly, is run.
>run
run Mosquito
Set uncaught java.lang.Throwable
Set deferred uncaught java.lang.Throwable
>
VM Started: Set deferred breakpoint Mosquito.main
Breakpoint hit: "thread=main", Mosquito.main(), line=14 bci=0
14 int num1 = 0;
main[1]
Yep, that's a lot of junk to interpret. You can ignore the two lines about Set uncaught java.lang.Throwable. There's a > prompt next, but you don't get a chance to type anything there so don't worry about that either.
Next, after "VM Started," the debugger confirms that it has set a breakpoint in main() like you asked. That's simple enough, although really not very exciting.
Finally, then, we get to the point where something interesting happens: the breakpoint is hit. The useful part of that line says Mosquito.main(), line 14. That's telling you which statement in your program will execute next. It even shows you the statement itself on the next line: 14 int num1 = 0;
Now, just to make things confusing, the prompt changes again. Gone is the recognizable > prompt and in its place you get main[1]. That might at least have made sense if "main" referred to the method currently running, but it doesn't. There is a reason for it, but for now don't worry about it. Just note that you've got a new prompt.
We can see just looking at it that int num1 = 0
is a
pretty boring statement. Let's just execute it and get it over with.
How? Type next.
main[1] next
>
Step completed: main[1] "thread=main", Mosquito.main(), line=15 bci=2
15 int num2 = 0;
You've asked jdb to execute one line of code and move on to the next. You again see the next line of code to execute. That's important. You're looking at the next line of code to execute. That will confuse you as you debug your own (more complex) programs, since it may seem like it's showing the code it just executed. It's always showing what will happen next.
Fine, so that means num1 is now officially declared and should have a value of zero. Since that's pretty dull go ahead and execute the next two lines too:
main[1] next
>
Step completed: "thread=main", Mosquito.main(), line=16 bci=4
16 int result = 0;
main[1] next
>
Step completed: "thread=main", Mosquito.main(), line=19 bci=6
19 num1 = getIntegerInput("Enter the first number", "Mosquito
");
Now we see line 19 ready to execute, and it seems to do something a little more interesting than just declaring a variable. Before you execute an interesting line of code, make a prediction about what it will do. Then, when it's done running, make sure your prediction was right. In this case, I predict, "I'll type 34, so num1 will be 34 when I'm done."
When you type next, two things happen. First, that funny > prompt comes up again with a blinking cursor. Then the JOptionPane shows up on the screen. Just to annoy you, however, if you start typing it'll show up at the > prompt, not in the JOptionPane. Use Alt+Tab or click the JOptionPane to select it and then type your number. If you don't see the JOptionPane at all, use Alt+Tab to find it (it won't be in your taskbar).
main[1] next
>
Step completed: "thread=main", Mosquito.main(), line=20 bci=14
20 num1 = getIntegerInput("Enter the second number", "Mosquit
o");
main[1]
Great. So the statement executed. Now it's time to check whether your prediction ("num1 will be 34") is correct. Ask what the value of num1 is by using the print command:
main[1] print num1
num1 = 34
Finally we find a command with output that doesn't require clarification! What's more, the results match our prediction so we know there's nothing wrong with the line of code that just executed.
Here's the secret to debugging a program. Every time you execute a statement, make a prediction about what it will do. Will a variable's value change? Will you run a method? Will the 'if' statement happen or not? Then, when you execute it, see whether your prediction was right or not.
If your prediction was right, then the program did exactly what you thought it should, and there's no point wasting time examining that statement. If everything in your program worked the way you thought it did you wouldn't have a bug! On the other hand, if your prediction is wrong, you've just found a problem that needs to be solved.
My prediction about the next line of code (line 20) is that num1 will still be 34 and num2 will be 2, because I intend to enter 2 when prompted.
main[1] next
>
Step completed: "thread=main", Mosquito.main(), line=23 bci=22
23 result = num1 + num2;
main[1] print num2
num2 = 0
Note that this time the JOptionPane gets focus right away, so you don't have to click on it. The first time you use a JOptionPane it won't get focus at first. Every time after that it will. That'll annoy you to death.
Clearly, we see that the prediction was wrong. num2 is still 0. Now, how do you figure out what did happen? Start by taking a look at all the variables. No, don't just print them all. Use the locals command instead:
main[1] locals
Method arguments:
args = instance of java.lang.String[0] (id=1243)
Local variables:
num1 = 2
num2 = 0
result = 0
You get separate lists of any arguments to the current method (which is just 'args' in this case since we're in main()) and ordinary variables just declared somewhere inside the method. We can see immediately that the 2 I had wanted to end up in num2 actually ended up in num1.
Well, we know we just executed a problematic line of code. You may find it convenient to see where that line is in context before going back to your editor to make any changes. Try the list command:
main[1] list
19 num1 = getIntegerInput("Enter the first number", "Mosquito
");
20 num1 = getIntegerInput("Enter the second number", "Mosquit
o");
21
22 // Calculate the product of those two numbers
23 => result = num1 + num2;
24
25 // Provide the result for the user
26 JOptionPane.showMessageDialog(null,"" + num1 + " x " + num
2 + " = " + result, "Mosquito", JOptionPane.INFORMATION_MESSAGE);
27
28 // And we're done!
Note the => arrow pointing to the next statement to
be executed. Now go back to the editor and change it to num1 =
...
. Recompile the program and run it again without
the debugger. Assume your change worked and test it. If it doesn't
work, come back and try again with the debugger.
Don't forget to type exit to quit jdb.
We have to add one little twist when it comes to running methods. Notice that the getIntegerInput() function just executed from top to bottom without waiting for you to keep typing "next" for each statement. What if your bug were in that method? Let's debug the program again, this time so that we get to step through each statement in getIntegerInput().
This time, set a breakpoint just before it calls getIntegerInput() on line 19 by typing stop on Mosquito:19. Then run the program and see the breakpoint come up.
> stop on Mosquito:19
Deferring breakpoint Mosquito:19.
It will be set after the class is loaded.
> run
run Mosquito
Set uncaught java.lang.Throwable
Set deferred uncaught java.lang.Throwable
>
VM Started: Set deferred breakpoint Mosquito:19
Breakpoint hit: "thread=main", Mosquito.main(), line=19 bci=6
19 num1 = getIntegerInput("Enter the first number", "Mosquito
");
Now, suppose you've been stepping through some big program and come to this line where getIntegerInput() function is called. You know there may be a problem inside getIntegerInput() so you'd like to see those statements executed individually. If you type next it'll just call the function, do everything it says to do, and then come back. Instead, type step.
main[1] step
>
Step completed: "thread=main", Mosquito.getIntegerInput(), line=39 bci=0
39 String temp = JOptionPane.showInputDialog(null, message, t
itle, JOptionPane.QUESTION_MESSAGE);
Sure enough, we're now inside the method. Use the list to get some context if you want. Now you can go back to using next and the other commands we've seen to explore this method. If you ever step into a method and then realize it was a mistake, use step up to execute the rest of the method and stop when it's done. (It steps UP to the calling function.)
These features (setting breakpoints, stepping over one line of code, stepping into a method, and looking at variables' values) are the basic features you should expect to find in any debugger. They're also the ones you'll use most often. There are other debugger commands that aren't necessarily "mainstream" but which are exactly the right tools for particular situations
If you've mastered the basics of debugging, move on to and see more of what JDB can do for you. If you're still getting used to these basic commands, don't just dive in head first to the advanced commands. Wait until you've gotten the basics figured out and then come back.
Go back to of this tutorial, if you
haven't already, and compile the Mosquito
program. If
you're new to using the debugger, you may want to walk through the
entire first part before proceeding with these new commands.
You may have noticed that there's a lot of repetition in a command-line debugger. Every time you want to check up on a variable you have to type print variable, for example. If you're watching a bunch of variables over lots of lines of code, that can be a huge hassle.
Fortunately, JDB offers a feature called a monitor
,
which is just a command that gets executed every time the debugger
stops. That means every time you hit a breakpoint and every time you
step over a line of code (or into a method) your command will get
run.
For example, type monitor print num and then try
executing some statements with the next command. The
print num
command gets executed automatically each time
the program stops:
> run
run Mosquito
Set uncaught java.lang.Throwable
Set deferred uncaught java.lang.Throwable
>
VM Started: Set deferred breakpoint Mosquito.main
Breakpoint hit: "thread=main", Mosquito.main(), line=14 bci=0
14 int num1 = 0;
main[1] monitor print num1
main[1] next
>
Step completed: "thread=main", Mosquito.main(), line=15 bci=2
15 int num2 = 0;
main[1] num1 = 0
main[1] next
>
Step completed: "thread=main", Mosquito.main(), line=16 bci=4
16 int result = 0;
main[1] num1 = 0
main[1] next
>
Step completed: "thread=main", Mosquito.main(), line=19 bci=6
19 num1 = getIntegerInput("Enter the first number", "Mosquito
");
main[1] num1 = 0
main[1] next
>
Step completed: "thread=main", Mosquito.main(), line=20 bci=14
20 num1 = getIntegerInput("Enter the second number", "Mosqui
o");
main[1] num1 = 34
main[1] next
>
Step completed: "thread=main", Mosquito.main(), line=23 bci=22
23 result = num1 + num2;
main[1] num1 = 2
Note that the num1 = ... outputs appear next to the main[1] prompt. That's just where they appear; it's not anything I typed.
You can monitor any command you like (you might monitor list, for example, if you find the single line of source code isn't helpful enough). You can just type monitor by itself to see what monitors you have right now, and use unmonitor # with the number listed to turn it off:
main[1] monitor
1: print num1
main[1] unmonitor 1
Unmonitoring 1: print num1
Being able to have a command execute automatically is nice, but is still insufficient if you don't even know which region of code is responsible for "breaking" a variable.
Consider another program called Wasp. This one doesn't really have a bug, per se. It doesn't even have a clear purpose. It constructs a Wasp object, and then at some point in the future (in the middle of the update() method after 100 iterations of a loop) it updates a class variable. We'll suppose this particular change to the variable is not one we expect.
Wasp.java
/** Wasp: Buggy Program #2 |
This may seem a little contrived, but the idea that a variable would get messed up in the middle of a huge loop is entirely realistic. It would also be utterly painful to have to do next 100 times every single time you wanted to test it in the debugger.
Start JDB with the Wasp program ( jdb Wasp ) and tell it
to watch the state
variable by typing the watch
Wasp.state command. Then run the program (you do not
need to set any breakpoints: that's the whole point of this).
H:\>jdb Wasp
Initializing jdb ...
> watch Wasp.state
Deferring watch modification of Wasp.state.
It will be set after the class is loaded.
> run
run Wasp
>
VM Started: Set deferred watch modification of Wasp.state
Field (Wasp.state) is 0, will be 5: "thread=main", Wasp.(), line=16 bci=6
16 state = 5;
main[1]
Just like when we set a breakpoint upon first starting the debugger, it now defers setting the watch. Upon typing run it starts running and doesn't stop until just before the variable gets changed.
That's an important thing to understand. Just as a breakpoint causes the debugger to stop before executing the statement on that line, a watch causes the debugger to stop before the variable is modified. The language in the output makes this clear: Field (Wasp.state) is 0, will be 5.
Use the list command (and any others you may find helpful) to determine where you are in the code and whether this particular change to the variable is important or not.
Already in the output from when the watch was hit we can see that
it's in Wasp.
main[1] cont
5
>
Field (Wasp.state) is 5, will be 7: "thread=main", Wasp.update(), line=28 bci=24
28 state = 7;
Now, perhaps, you'd be surprised to see state
changing
in the update()
method. You could again use
list and print to learn more:
main[1] list
24 public void update() {
25 for (int i = 0; i < 1000; i++) {
26 if (i == 100) {
27 System.out.println(state);
28 => state = 7;
29 }
30 }
31 }
32 }
33
main[1] print i
i = 100
main[1]
Of course, since this program has no particular purpose it would be
silly to talk about what you might infer from this information, but
when looking at a real piece of code you'd presumably find something
askew that has caused state
to change at this unexpected
point in the code.
For all JDB's great features, there are a few key areas it's not very strong. For example, it would be nice to have it print out the entire call stack (i.e. the list of functions currently executing: main() called func1() called func2() called...) so you could see how you got to where you are (particularly if you call the same method from multiple locations. JDB doesn't offer such a feature, but other debuggers definitely do.
The power of JDB is largely its ubiquity. Everywhere there's a Java compiler there ought to be JDB, so even if you don't have access to a full-featured development environment you can still use some powerful debugging tools. A development environment like (which is free) will contain a debugger that does everything JDB does, and usually more.