In the previous article we explored a couple of strategies to debug the OpenJDK JVM bytecodes interpreter. I will now present a procedure to debug JIT compiled code.
There are some JVM arguments useful to get information about JIT compiled methods:
1 |
-Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:PrintAssemblyOptions=hsdis-print-bytes -XX:+LogCompilation -XX:LogFile=/tmp/java_compilation_log.log |
Note: +PrintAssembly argument can be used to disassemble compiled code but requires a plugin library called hsdis in some JDK releases such as 8. In hotspot/src/share/tools/hsdis directory you will find the plugin code. To build it, open a command line and run:
1 |
export BINUTILS=<path-to-binutils> && touch $BINUTILS/bfd/doc/bfd.info && make all64 |
Once built, copy build/linux-amd64/hsdis-amd64.so library to jre/lib/amd64 and jre/lib/amd64/server directories.
Let’s see an example.
Java code:
1 2 3 4 5 6 7 8 9 |
public class Main { public static volatile int exitCode = 0xff; public static void main(String[] args) throws Throwable { System.exit(exitCode); } } |
Main::main JIT method information:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
PrintAssembly request changed to PrintOptoAssembly {method} - this oop: 0x00007ffba04642b8 - method holder: 'Main' - constants: 0x00007ffba0464058 constant pool [29] {0x00007ffba0464058} for 'Main' cache=0x00007ffba04643c8 - access: 0x81000009 public static - name: 'main' - signature: '([Ljava/lang/String;)V' - max stack: 2 - max locals: 1 - size of params: 1 - method size: 13 - highest level: 3 - vtable index: -2 - i2i entry: 0x00007ffb8d023be0 - adapters: AHE@0x00007ffb9c0fbdb8: 0xb0000000 i2c: 0x00007ffb8d1466a0 c2i: 0x00007ffb8d1467da c2iUV: 0x00007ffb8d1467ad - compiled entry 0x00007ffb8d571ec0 - code size: 7 - code start: 0x00007ffba04642a8 - code end (excl): 0x00007ffba04642af - method data: 0x00007ffba04645b8 - checked ex length: 1 - checked ex start: 0x00007ffba04642b4 - linenumber start: 0x00007ffba04642af - localvar length: 0 - compiled code: nmethod 16800 876 3 Main::main (7 bytes) |
Main::main JIT method code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
000 B1: # N1 <- BLOCK HEAD IS JUNK Freq: 1 000 # stack bang (104 bytes) pushq rbp # Save rbp subq rsp, #16 # Create frame 00c movl RSI, #21 # int 011 movq R10, java/lang/Class:exact * # ptr 01b movl R11, [R10 + #104 (8-bit)] # int ! Field: volatile Main.exitCode 01f MEMBAR-acquire ! (empty encoding) 01f movl RBP, R11 # spill nop # 1 bytes pad for loops and calls 023 call,static wrapper for: uncommon_trap(reason='unloaded' action='reinterpret' index='21') # Main::main @ bci:3 L[0]=_ STK[0]=RBP # OopMap{off=40} 028 int3 # ShouldNotReachHere |
You see there how the Main::exitCode field value is loaded into a register, and a call to a static method -System::exit, presumably- is made.
The JVM allows transitions from interpreted bytecodes to JIT compiled methods and viceversa. For this to be accomplished, methods have adapters which arrange the call frame according to the source and destination ABIs.
Once an adapter to JIT compiled code is executed, the native method starts. If we look at Main::main information above, compiled entry has the starting address. Our goal is to set a breakpoint there.
Contrary to the JVM interpreter -which is generated at the beginning-, JIT methods may be generated at any time. Even more: they can be re-compiled multiple times and by different compilers. As a result, we need to stop every time the method is generated and before its execution.
CompileBroker::invoke_compiler_on_method(CompileTask*) is an interesting point in that regard. This is where methods get compiled either by C1, C2 or any other JIT compiler. In particular, right after the task->code() call, the compiled method is ready:
1 |
if (!ci_env.failing() && task->code() == NULL) { |
If we look at the assembly in x86_64, here is the exact point:
1 2 |
0x00007ffff6084947 <+1555>: callq 0x7ffff607f486 <CompileTask::code() const> 0x00007ffff608494c <+1560>: test %rax,%rax |
We will then set our breakpoint right after the call to CompileTask::code. Note: if rebuilding the JVM is possible, we can add a filter to only break in the method of our interest:
1 2 3 |
if (strcmp(target->name()->as_utf8(), "<method_name_of_interest>") == 0) { volatile int a = 1; } |
We now have all the information needed about the compiled method:
Method name
1 |
(gdb) x/s target->_name->_symbol->_body |
Compiler level (C1, C2)
1 |
(gdb) print/x task->_comp_level |
Method blob size
1 |
(gdb) print task->_code_handle->_nm->_size |
Method’s first instruction
1 |
(gdb) x/1i task->_code_handle->_nm->_entry_point |
Note: blob size can be used as an overestimate for the number of instructions in the method -in the worst case scenario, each instruction would take 1 byte-.
It’s now possible to set a breakpoint at the beginning of the method:
1 |
(gdb) break *(task->_code_handle->_nm->_entry_point) |
Bonus
Dump the JIT compiled method to a file:
1 2 3 4 5 6 |
(gdb) set pagination off (gdb) set logging file <path/to/file> (gdb) set logging overwrite on (gdb) set logging on (gdb) x/<SIZE>i task->_code_handle->_nm->_entry_point (gdb) set logging off |
Good article.
_nm->_entry_point doesn’t exist upstream anymore. There’s _nm->_code_begin, but I’m not clear it’s equivalent