Building JDK with HSDIS on Linux
Published:
Intro
hsdis
is a disassembler plugin for the HotSpot JVM (Java Virtual Machine). The hsdis
plugin is very useful for some Java developers that want to see the code generated by the JVM’s Just-In-Time (JIT) compiler into human-readable assembly language. Unfortunately, this plugin is not included out of the box with JDK (Java Development Kit), so we need to build our own JDK and enable the hsdis
plugin manually.
In this post, I’ll walk you through the process of building hsdis
for both the latest JDK (JDK 25 at the time of writing) and the latest Long-Term Support (LTS) version, JDK 21.
I’ll focus on a Linux environment, so get your terminal ready!
Getting the dependencies
For Fedora 41:
sudo dnf install autoconf alsa-lib-devel cups-devel libXtst-devel libXt-devel libXrender-devel libXrandr-devel libXi-devel
sudo dnf install gmp gmp-devel mpfr mpfr-devel libmpc libmpc-devel
For Ubuntu 24.04 LTS:
sudo apt-get install autoconf libasound2-dev libcups2-dev libfontconfig1-dev libx11-dev libxext-dev libxrender-dev libxrandr-dev libxtst-dev libxt-dev texinfo
sudo apt-get install libmpfr-dev libmpc-dev libgmp-dev
Get Binutils
Clone the binutils
project:
cd ~/bin/jdk/binutils/
git clone git://sourceware.org/git/binutils-gdb.git
export BIN_UTILS_DIR=$PWD/binutils-gdb
Build a JDK with HSDIS from the master
branch (e.g., JDK 25)
Clone the JDK repo:
cd ~/bin/jdk
git clone https://github.com/openjdk/jdk.git
cd jdk
And run the configure
script with the following options:
bash configure --with-hsdis=binutils --with-binutils-src=$BIN_UTILS_DIR
If the configure is correct, then we can start the build:
make clean
make images
make build-hsdis
make install-hsdis
Finally, we load environment for the new JDK:
$ export JAVA_HOME=$PWD/build/linux-x86_64-server-release/jdk/
$ export PATH=$JAVA_HOME/bin:$PATH
## Check
$ java --version
openjdk 25-internal 2025-09-16
OpenJDK Runtime Environment (build 25-internal-adhoc.juan.jdk)
OpenJDK 64-Bit Server VM (build 25-internal-adhoc.juan.jdk, mixed mode)
Build hsdis
for JDK 21
The configuration process is almost identical to the upstream version, except that we need a specific version of binutils
, the 2.37 in order to build JDK21.
cd BIN_UTILS_DIR
git checkout binutils-2_37
Besides, we can obtain an updated version of JDK 21 by changing the repository:
cd ~/bin/jdk
git clone https://github.com/openjdk/jdk21u-dev.git
cd jdk21u-dev
I am going to use the JDK 21+6 update:
git checkout jdk-21.0.6-ga
Now, we can use the same command as the one used to build the upstream version:
bash configure --with-hsdis=binutils --with-binutils-src=$BIN_UTILS_DIR
make clean
make images
make build-hsdis
make install-hsdis
Finally, we load the environment:
$ export JAVA_HOME=$PWD/build/linux-x86_64-server-release/jdk/
$ export PATH=$JAVA_HOME/bin:$PATH
## Check
$ java --version
openjdk 21.0.6-internal 2025-01-21
OpenJDK Runtime Environment (build 21.0.6-internal-adhoc.juan.jdk21u-dev)
OpenJDK 64-Bit Server VM (build 21.0.6-internal-adhoc.juan.jdk21u-dev, mixed mode)
Enabling the Disassembler: An Example
Let’s write an example and see the disassembler in action:
public class SampleCompute {
public static void main(String[] args) {
SampleCompute compute = new SampleCompute();
int[] array = new int[100_000];
for(int i = 0; i < array.length; i++) {
array[i] = compute.compute(i);
}
}
private int compute(int i) {
return (i * i) + i;
}
}
We compile the program with javac
as usual:
javac SampleCompute.java
To enable the assembler, we run java
with the -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly
options:
java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly SampleCompute
This is very verbose because it dumps into the standard output all methods compiled. However, it is possible to add filters for specific methods using the -XX:CompileCommand='print,MyKlass::method’
option. For example, the following code snippet only enables the assembly dump for the compute
method.
java -XX:+UnlockDiagnosticVMOptions -XX:CompileCommand='print,SampleCompute.compute' SampleCompute
Sample output:
CompileCommand: print SampleCompute.compute bool print = true
============================= C1-compiled nmethod ==============================
----------------------------------- Assembly -----------------------------------
Compiled method (c1) 73 477 3 SampleCompute::compute (6 bytes)
total in heap [0x00007f8eb909e290,0x00007f8eb909e580] = 752
relocation [0x00007f8eb909e3e8,0x00007f8eb909e418] = 48
main code [0x00007f8eb909e420,0x00007f8eb909e4f8] = 216
stub code [0x00007f8eb909e4f8,0x00007f8eb909e528] = 48
oops [0x00007f8eb909e528,0x00007f8eb909e530] = 8
metadata [0x00007f8eb909e530,0x00007f8eb909e538] = 8
scopes data [0x00007f8eb909e538,0x00007f8eb909e548] = 16
scopes pcs [0x00007f8eb909e548,0x00007f8eb909e578] = 48
dependencies [0x00007f8eb909e578,0x00007f8eb909e580] = 8
[Disassembly]
--------------------------------------------------------------------------------
[Constant Pool (empty)]
--------------------------------------------------------------------------------
[Entry Point]
# {method} {0x00007f8e90700320} 'compute' '(I)I' in 'SampleCompute'
# this: rsi:rsi = 'SampleCompute'
# parm0: rdx = int
# [sp+0x30] (sp of caller)
0x00007f8eb909e420: mov 0x8(%rsi),%r10d
0x00007f8eb909e424: shl $0x3,%r10
0x00007f8eb909e428: cmp %rax,%r10
0x00007f8eb909e42b: jne 0x00007f8ec04ad080 ; {runtime_call ic_miss_stub}
0x00007f8eb909e431: data16 data16 nopw 0x0(%rax,%rax,1)
0x00007f8eb909e43c: data16 data16 xchg %ax,%ax
[Verified Entry Point]
0x00007f8eb909e440: mov %eax,-0x14000(%rsp)
0x00007f8eb909e447: push %rbp
0x00007f8eb909e448: sub $0x20,%rsp
0x00007f8eb909e44c: cmpl $0x1,0x20(%r15)
0x00007f8eb909e454: je 0x00007f8eb909e45b
0x00007f8eb909e456: call Stub::nmethod_entry_barrier ; {runtime_call StubRoutines (final stubs)}
0x00007f8eb909e45b: movabs $0x7f8e907004c0,%rax ; {metadata(method data for {method} {0x00007f8e90700320} 'compute' '(I)I' in 'SampleCompute')}
0x00007f8eb909e465: mov 0xf4(%rax),%edi
0x00007f8eb909e46b: add $0x2,%edi
0x00007f8eb909e46e: mov %edi,0xf4(%rax)
0x00007f8eb909e474: and $0x7fe,%edi
0x00007f8eb909e47a: test %edi,%edi
0x00007f8eb909e47c: je 0x00007f8eb909e49d
0x00007f8eb909e482: mov %rdx,%rax
0x00007f8eb909e485: imul %edx,%eax
0x00007f8eb909e488: add %edx,%eax
0x00007f8eb909e48a: add $0x20,%rsp
0x00007f8eb909e48e: pop %rbp
0x00007f8eb909e48f: cmp 0x448(%r15),%rsp ; {poll_return}
0x00007f8eb909e496: ja 0x00007f8eb909e4bb
0x00007f8eb909e49c: ret
0x00007f8eb909e49d: movabs $0x7f8e90700320,%r10 ; {metadata({method} {0x00007f8e90700320} 'compute' '(I)I' in 'SampleCompute')}
0x00007f8eb909e4a7: mov %r10,0x8(%rsp)
0x00007f8eb909e4ac: movq $0xffffffffffffffff,(%rsp)
0x00007f8eb909e4b4: call 0x00007f8ec0570a00 ; ImmutableOopMap {rsi=Oop }
;*synchronization entry
; - SampleCompute::compute@-1 (line 12)
; {runtime_call counter_overflow Runtime1 stub}
0x00007f8eb909e4b9: jmp 0x00007f8eb909e482
0x00007f8eb909e4bb: movabs $0x7f8eb909e48f,%r10 ; {internal_word}
0x00007f8eb909e4c5: mov %r10,0x460(%r15)
0x00007f8eb909e4cc: jmp 0x00007f8ec04b4000 ; {runtime_call SafepointBlob}
0x00007f8eb909e4d1: mov 0x4f8(%r15),%rax
0x00007f8eb909e4d8: movq $0x0,0x4f8(%r15)
0x00007f8eb909e4e3: movq $0x0,0x500(%r15)
0x00007f8eb909e4ee: add $0x20,%rsp
0x00007f8eb909e4f2: pop %rbp
0x00007f8eb909e4f3: jmp 0x00007f8ec056ac00 ; {runtime_call unwind_exception Runtime1 stub}
[Exception Handler]
0x00007f8eb909e4f8: call 0x00007f8ec056d900 ; {no_reloc}
0x00007f8eb909e4fd: movabs $0x7f8ed0349604,%rdi ; {external_word}
0x00007f8eb909e507: and $0xfffffffffffffff0,%rsp
0x00007f8eb909e50b: call 0x00007f8ecfb79340 ; {runtime_call MacroAssembler::debug64(char*, long, long*)}
0x00007f8eb909e510: hlt
[Deopt Handler Code]
0x00007f8eb909e511: movabs $0x7f8eb909e511,%r10 ; {section_word}
0x00007f8eb909e51b: push %r10
0x00007f8eb909e51d: jmp 0x00007f8ec04b32a0 ; {runtime_call DeoptimizationBlob}
0x00007f8eb909e522: hlt
0x00007f8eb909e523: hlt
0x00007f8eb909e524: hlt
0x00007f8eb909e525: hlt
0x00007f8eb909e526: hlt
0x00007f8eb909e527: hlt
--------------------------------------------------------------------------------