This document gives practical instructions on how to debug the DRL virtual machine and its baseline just-in-time compiler Jitrino.JET. For a definition of components and details on their internal structure, consult the DRL Virtual Machine Developer’s Guide supplied with the DRLVM image.
The document includes two groups of debugging tips, one for VM tips, and the other for JIT compiler tips, as shown below.
Disclaimer and Legal Information
Debugging the Jitrino.JET Baseline Compiler
How to enable tracing in Jitrino.JET?
Copyright 2006 The Apache Software Foundation or its licensors, as applicable.
Licensed under the Apache License, Version 2.0 (the License); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an AS IS BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
This section gives an insight into debugging the DRL virtual machine version 1.0 and provides tips on resolving non-standard debugging issues.
This section gives instructions on different scenarios of debugging the VM source code.
For ordinary tests, start the ij executable with the debugger enabled, as follows:
On Windows*
vm\build\vm.sln.
ij executable.
Select the Debugging tab and specify the
command and command arguments.
zlib1.dll to the
location of the VM executable.
On Linux*
LD_LIBRARY_PATH to point to the
deploy/jre/bin directory. Change the working
directory to the location of the VM executable and run:
gdb ij
run <your_params>
To attach to the running VM process, do the following:
On Windows*
On Linux*
Run:
gdb –p <PID of ij>
If the VM crashed during execution, use the core dump to analyze the crash:
gdb ij core
This section includes some tips on optimizing the debug process and getting more debug information.
Consult the Getting Started guide delivered with DRLVM bundle for information on VM standard and non-standard configuration options. Tracing-related options might be useful for debugging purposes.
The debugger might draw the stack incorrectly when the JIT or native stubs are involved. To avoid this, set esp as the memory location and 4-byte integer values as the output format. As a result, you can examine the stack word by word and look into the code for each number 0x00… by using the Disassembly window.
When the VM has crashed
A very specific case on Windows*: the VM has crashed and you only see the stack frame in the call stack
0x00000000. This means that the program has jumped or called to a null pointer.You might still get the stack trace. For that, do the following:
- Open the Disassembly window.
- Find any ret instruction with no parameters in the memory space.
Open any source file, open the context menu, and select Go to Disassembly to find anyretinstruction.- When found, select the ret instruction and click Set Next Statement in the context menu.
- Type
F11to go a single instruction.
You may see several instructions near the current point of execution. If you see a call instruction, you have found an appropriate call site and you can see the call stack in the Call Stack window. If you have found a wrong call site, repeat the instruction from step 2.
On Windows*
Running the VM in the interpreter mode, you can get Java* methods stack trace almost at any point of
execution. For that, break the execution of the VM and
select the Interpreter frame in the Visual
Studio* Call Stack window.
Then, in the watch window, add the watch
stack_dump(). The stack dump appears in VM
output window. Running the VM in the JIT mode, use the
st_print() function for the same purpose.
On Linux*
Use the gdb command print and specify the
stack_dump or st_print in the
interpreter or the JIT compiler mode respectively.
On Windows* / IA-32
Place breakpoints in source code by inserting calls to the _CrtDbgBreak() function. Placing the call inside a condition calls the function only for the specified case you need to debug. Analogously, you can use the Windows* API function DebugBreak() or print INT 3.
Note
This requires recompiling the VM.
On Linux*
Run the following:
__asm {int 3}
DRL VM has 1:1 mapping between native threads visible in the
debugger and Java* threads.
To work with Java* threads individually,
freeze the threads you do not need with the help of the
debugger, and continue execution of other threads.
The CriticalSection primitive is a common cause
of deadlocks. If the code has stopped at a critical section,
you can try to find the thread that owns this primitive.
On Windows*
The file WinNT.H located in <PlatformSDK>\Include contains the definition for the structure _RTL_CRITICAL_SECTION, which contains the description of the CriticalSection primitive. You can get the owning thread for the CriticalSection primitive in a number of ways, as indicated below.
Lookup in Memory
On Windows*
While debugging the code in Visual Studio, do the following:
- Go Debug > Windows > Memory 1.
- In the address line, enter the address of the critical section.
- Open the context menu and select 4-byte Integers. This sets the fourth DWORD from the beginning of the critical section as the owning thread ID in hexadecimal representation.
Note
Visual Studio* displays thread IDs in decimal representation.
In the watch windowIn the watch window of Visual Studio*, insert the following:
* ((int* )<cs-ptr>+3)Where
<cs-ptr>is the address of your critical section
On Linux*
The file /usr/include/bits/pthreadtypes.h
contains the description of the pthread_mutex_t
type. To get the ID of the thread owning the
CriticalSection primitive, in gdb execute:
x/4w <address of your mutex primitive>
The third word in the output contains the owning thread descriptor you are looking for.
You can often need to find out the class name for a Java* object used in VM code. For example, you may
need to get the class name for an object of the type
ManagedObject * (which is a direct pointer to
the heap). For that, insert the following expression into
the watch window in Visual Studio on Windows* or print the command of gdb on
Linux* :
((VTable*)(*((int
*)obj)))->clss->name->bytes
Variables of the types jobject and
Object_Handle are references to
ManagedObject * types. These structures contain
a single element, a pointer to ManagedObject *
type object. To use the expression above, de-reference the
variable, for example, substituting obj in the
expression above with a cast to (ManagedObject
*)(*(int *)obj).
To use debugging and tracing in Jitrino.JET, use the debug
build or the release build with the JET_PROTO
macro defined. See the file jdefs.h for a
definition of the available flags.
Currently, Jitrino.JET provides no interface or command line
to control tracing options. To enable tracing, set the
compile_flags variable at the entry point to
the method Compiler::compile(). At that point,
the global variable Compiler::g_methodsSeen
contains the ID of the method being compiled, and the
instance variable m_fname contains its fully
qualified name with no signature. Obtain these options
through a call to
Compiler::m_infoBlock.get_flags().
Tracing flags control compilation results output and trace
run-time execution, as described below. Tracing results are
created in each run in the directory where the
ij executable starts: compilation results are
in jet.log, and run-time output is in
jet.rt.log.
The following flags control tracing compilation of a method:
DBG_TRACE_SUMM prints a
short summary about the compiled method: the compilation
status (success or failure), the name, signature,
bytecode size, the start and end addresses of the
generated code, and the compilation ID (the sequential
number of the method compiled by Jitrino.JET).
DBG_DUMP_BBS dumps the
bytecode and marks up basic blocks boundaries.
DBG_TRACE_CG prints
information about each compiled bytecode instruction: the
state of the Java* operand stack before
the instruction, the known state of local variables at
the given point, and the native code generated for the
instruction. The order instructions appear in the log
file is the depth-first order, the same as when
processing instructions during compilation. See the file
trace.cpp, function toStr2() for the legend
of the operand stack items print-out.
DBG_TRACE_LAYOUT prints the
results of the code layout, mostly, the address ranges
for the basic blocks.
DBG_DUMP_CODE dumps
generated code for the given method, the method’s
actual addresses, intermixed with appropriate bytecode
instructions.
Note
For DBG_DUMP_CODE and
DBG_TRACE_CG, Jitrino.JET can print
disassembled code in addition to raw hexadecimal
dumps. For that, the compiler requires an external
disassembler. Currently, Jitrino.JET is configured to
use the external library
lwdis.dll/liblwdis.so that must be
located in the same directory as
jitrino.dll/libjitrino.so. The name lwdis
stands for light weight disassembler. The library must
export the function disasm(). Refer to
the file trace.cpp, the DISFUNC
definition for details on calling convention and
signature.
DBG_CHECK_STACK prints
nothing but instruments code to check stack integrity.
The methods compiled by Jitrino.JET use EBP-based frames.
These frames can mask errors, for example, wrong
convention usage with a VM helper call. Using this option
performs run-time checks and INT3 raised in case the
stack integrity broken.
DBG_BRK inserts INT3 at the
method entry point, so it can be stopped in the debugger.
Note
An instance variable of class
Compiler, dbg_break_pc can
be used to insert INT3 at the specified
program counter of a method.
The following flags trace run-time life of the method:
DBG_TRACE_EE prints
entering: <method-name> and exiting:
<method-name> at the method entrance
and exit points.DBG_TRACE_BC traces
execution of every bytecode instruction by printing a
string of the following format before executing the
instruction: <method-name> @
PC=<pc>Notes
The output string for DBG_TRACE_EE and
DBG_TRACE_BC uses a specific format: before the
string, an estimated call depth is printed and the string
gets indentation based on the call depth. After the string,
the string ID is printed. The estimated call depth may help
to identify where a method was called from. The string ID
can be helpful for setting a conditional breakpoint in the
debugger for a complex scenario. For that, set a condition
for the static variable cnt in the function
rt_dbg, file trace.cpp.
Turning on the option DBG_TRACE_BC may slow
down execution extremely and may result to a gigantic file
jet.rt.log.
DBG_TRACE_RT traces
run-time support calls, for example, getting address of
‘this’, support for root set enumeration and
stack unwinding.
Note
The output goes to jet.log, with the address (both
native and PC) where the event happens, and some other info.
To identify one or more problematic methods with another stable JIT compiler, use the execution manager. With this technique, some methods are compiled by the stable JIT, and the rest goes to the JIT being debugged. With a simple binary search, you can find the problematic method rather quickly.
Note
Try turning off parallel compilation when using this
technique (refer to VM’s
-Xno_parallel_jit option).
To get details in case of a crash with no adequate stack trace or IP location available, turn on the option DBG_TRACE_EE to see, in which method the crash happens. As the second step, turn on DBG_TRACE_BC for this particular method to find the exact bytecode instruction. Often, this cuts the code to analyze down to 5-10 native instructions.
To set a breakpoint and stop execution at a specific point, use trace.cpp:rt_dbg to break execution at the specified bytecode instruction or at the entry point of the specified method.
This is an example of code that turns on various tracing
scenarios. The code must be placed in the method
Compiler::compile().
#if defined(_DEBUG) || defined(JET_PROTO)
// Turns on a short summary of all methods
compile_flags |= DBG_TRACE_SUMM;
// A handy constant
static const unsigned TRACE_CG = DBG_DUMP_BBS | DBG_TRACE_CG |
DBG_TRACE_LAYOUT | DBG_TRACE_SUMM |
DBG_DUMP_CODE;
// For methods in the range (1000;15000), print out the complete code generator dumps
if (g_methodsSeen>1000 && g_methodsSeen<15000) {
compile_flags |= TRACE_CG;
}
// For methods getSomeValue() and for all methods in class MyClass,
// trace enter and exit
if (NULL != strstr(m_fname, "::getSomeValue") ||
NULL != strstr(m_fname, "MyClass::") ) {
compile_flags |= DBG_TRACE_EE;
}
// For the method crashes_some_times() in class MyClass trace every
// bytecode execution: the last bytecode in the log is the most probable
// cause of the failure
if (!strcmp(m_fname, "MyClass::crashes_some_times")) {
compile_flags |= DBG_TRACE_EE|DBG_TRACE_BC;
}
// Break into debugger (INT3) at the entry of the stop_at_entry() method
if (!strcmp(m_fname, "MyClass::stop_at_entry")) {
compile_flags |= DBG_BRK;
}
// Break into debugger (INT3) inside the method
if (!strcmp(m_fname, "MyClass::stop_somewhere_in_the_middle")) {
dbg_break_pc = 50;
}
// Trace run-time support calls: unwind, getting the address of 'this', root
// set enumeration
if (!strcmp(m_fname, "MyClass::something_wrong_with_unwind_here")) {
compile_flags |= DBG_TRACE_RT;
}
// By-pass run-time tracing for java/* classes
if (m_fname == strstr(m_fname, "java/")) {
compile_flags &= ~(DBG_TRACE_EE|DBG_TRACE_BC);
}
#endif
* Other brands and names are the property of their respective owners.