Debugging GraalVM-native images using gdb
The GraalVM project includes, amongst other capabilities, a component called GraalVM Native Image. GraalVM Native Image supports the delivery of Java applications as shrink-wrapped, self-contained, standalone executables, commonly referred to as Java-native images. Native images often have a smaller footprint and faster startup time compared to running the same application in the traditional way on the JVM. This is often a win for short-running applications or small, container-based services. The trade-off is usually lower peak performance for long-running programs, and higher garbage collection overheads and latencies for programs with large amounts of resident data.
We are especially interested in GraalVM-native images as an alternative back-end delivery option for applications based on Quarkus. The Java team has worked hard to ensure that Quarkus is well integrated with GraalVM Native Images. In the process, they have found that one important usability issue is the ability to debug the delivered native image.
Of course, this is not primarily a development problem. Most of the hard work of debugging an application can be done on the JVM during development and testing. However, there is always the question of what to do if a deployed native image behaves differently. While that should not happen, it may occur because of errors in the application, or problems configuring the build. In rare cases, a deployed native image might behave differently because of a disparity introduced by the native compilation process when compiling either application code or JDK runtime code to native machine code.
What makes debugging hard is that a generated native image is heavily optimized code with minimal symbol information. Much of the original Java method code is inlined, so it is very difficult to relate specific instructions to their originating Java method, never mind a specific line of code in a Java source file. It’s at least as hard to debug as a C or C++ program compiled at the highest optimization level and without any symbol information.
Of course, a C/C++ compiler solves this problem by embedding debuginfo into the binary. This information tells the debugger precisely how to interpret the generated code and relate it back to the original sources. So, I recently took it upon myself to remedy the GraalVM native image problem by adding DWARF debug information to Linux images. This is enough to enable effective source-level debugging using the standard Linux debugger, gdb. A comparable solution for debugging Windows binaries is under development.
The debugger now knows about the Java classes and methods that have been compiled into an image. Breakpoints can be placed at method entry or on specific lines in a method. The debugger can correlate machine instructions in the native image with specific source lines in Java files. It can stop and step through method code line by line and show the call stack. The debugger even knows whether instructions belong to inlined methods, and will switch back and forth between source lines in the outer compiled method and the inlined source code as you step through the code.
This debug capability has now arrived in the latest GraalVM release. I recently uploaded a video presentation to the Quarkus channel explaining what has been implemented more precisely. It includes a demonstration based on a small Java program showing how you would build a native image containing debug info and run/debug it in gdb. I hope you enjoy the demo and would welcome feedback on usability or any errors encountered when debugging.