Building JDK with HSDIS on Linux

5 minute read

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    
--------------------------------------------------------------------------------