Author: Apurba Nath
Anyone who has done some serious development knows about debugging. There is a lot of stuff that goes under the hood for debugging, this blog takes a look at some of these. After reading this blog the reader may have a better understanding of the reason behind rare but weird errors about the “line number information not being present” when we try setting a break point. To keep things simple I am going to cover only the basics and will oversimplify and generalize without much discretion.
There are two parties in debugging, one the program which is running and the other which is issuing debugging commands to it. The first guy being the debugee and the second guy debugger, so kinda like debugger is running the show making the debugee jump through hoops (or should it be loops). To enable this cross talk, we need to launch the program in debug mode. The most common way of talking is via sockets, depending on who is running the server socket we can term it as a local or remote debug session. We use options like
for starting a process in debug mode. Essentially this command means use socket communication as transport for the java debug wire protocol( jdwp param) with the server socket running at localhost on port 4321( address param) waiting for commands from the debugger (suspend param). For remote debugging instead of running the server socket at the debugee end we would run it at debugger, so the options address=localhost:4321 changes to address=7777 and we add server=y. Of course now we do have to run the debugger program at the 7777 port. Remote debugging is very handy while working with appservers or any other program which you need to launch outside your ide environment and then connect to for debugging. (Ahem!! -Xdebug -Xrunjdwp are there too).
The cross talk between the debugger and debugee is done via requests and events. The event is dispatched as a response when the event actually happens, for example when we set a line breakpoint, when this line is actually to be invoked, the VM would issue a BreakPointEvent and the debugger can take appropriate action (step in, inspect variables etc). The requests from a technical perspective need not be only breakpoints, but can be method entry, exit, class loading, exceptions etc, most editors however abstract this out and represent everything as a breakpoints, so they mock features like MethodBreakPoints or ExceptionBreakPoints. (Ahem!! yes, jvmti does not necessarily use the dispatching, it can use callbacks)
To stop the program at our points of interest, we tell what our points of interest are (requests) and then give the vm further instructions when it hits them (it sends us events to indicate it may have hit it, we then send more requests in response to these events). These points of interest could be lines, methods, variables or many other things. A few details about some of the obvious ones are give below.
When the point of interest is a line, we set a line breakpoint. To facilitate line breakpoints, the vm has to make sure that the class file has line information. For example if we pick up one of the base java classes like java.lang.String in rt.jar from the standalone jre and the ones in the JDK, we will notice that the ones in JRE do not really have line number information (opening up the files in any byte code viewer will give that information). From a debugging perspective, line breakpoints can only be set if the class has line number info. In the actual byte code Line number information is conveyed using many of the attributes that are a part of the class file spec, for additional details refer to the sections about attributes like LineNumberInformation, SourceVariable etc at Class File Specification. An oversimplified view would be something like
Line 129 of the String.java (1.5.06 sun version) which is something like
this.offset = 0;
would have a byte code equivalent like
LINENUMBER 129 L1
PUTFIELD java/lang/String.offset : I
This tell us that the VM instructions that follow Label L1 are actually generated because of line 129 in the source code. We can suppress generation and addition of such information in our class files by using -g:none switch at compilation time (try javac -g). There are other parts like source, sourcepath and some other details which are important for the actual functionality.
At times we are interested in the execution of a certain method or we do not have the source file, in such a case we can request the vm to wait when it hits method entry and exit. Most IDEs provide Method Break Points to cater to this, the best part about method break points is that they do not rely on the line number information, just the method names and its signature. The only disadvantage of the method breakpoint is that they are slower than their line counterparts. (Ahem!! technically method breakpoints are not breakpoints. From the api perspective, debuggers raise requests to listen to method entry and exit events and then filter them out appropriately at their ends, so a lot of crosstalk and slower responses).
We can also suspend the vm or thread when a field is accessed or modified. In eclipse parlance we will call it WatchPoint. This is really handy when the code base uses reflection to modify values. For example a field gets magically modified to null and then we see NullPointerExceptions, we can use a WatchPoint to understand which part of the code does this and if this is really a bug.
In addition to these, the actual JDI actually supports requests for a lot more stages like class loading, unloading, monitor entry etc (for a more comprehensive feel look at com.sun.jdi.request package JPDA api).
When the debugger gets the event from the debugee, it can step in or resume the thread or VM. This feature is leveraged widely for catering to finer needs of debugging via Conditional Breakpoints. Most debuggers evaluate a condition on receiving the event and could then ask for user interaction or silently resume the thread. For example in case of the field being set to null, we could set a conditional watchpoint which would be true only if the current value of field were null. In an IDE like eclipse the condition can be specified using the breakpoint properties dialog box.
Another nifty feature is to tie the request to a specific thread. This can come in very handy in debugging problems on a webserver, so that parallel flows do not impact the debugging. In Eclipse this can be done via the Filtering page of the breakpoint properties. This too is a condition which is evaluated at the debugger level, meaning it does have a performance penalty.
There are many other very interesting details like stratum or hotswap. But guess will leave them for another day …