It is essential to debug and profile native images to understand whether you experience any issues related to the Native Image technology and contact your vendor for advice before pushing the native executable to production.
This article offers instructions on creating a native image with debug capabilities in a Docker container and Eclipse IDE.
Table of Contents
How to debug Native Image
Native executables contain the application code, optimized and interpreted into machine coffee. They contain minimal symbol information, which makes the debugging process challenging.
Therefore, to debug native images, we need to generate the debugging information at build time. After that, we provide the debug information to the binary file during the debugging session. Debugging info can be stored on a host or a target machine and sent to the host as needed. This process can be depicted as follows:
Native Image Debug Setup
It is important to note that a combination of two environments or all three environments can be located on one or multiple machines. For instance, the application that was built in the build environment, in the CI/CD pipeline, for instance, runs in the target environment, e.g., in the cloud. But it can be debugged from the host.
There are two ways of setting up a remote debugging session: by using SSH or gdbserver. In both cases, you will have to adjust the setting of your container and network access.
Native Image debugging: main considerations
There are two factors that you should consider when debugging your native image:
- Debugging native images is trickier than debugging regular Java applications because there’s no support for Java debugging and Java programs are modeled as C++ programs. So, if you experience any Java-related problems, it is better to debug your application with the usual tools before converting it to native executable.
- Native Image debugging is available on Linux with limited support on macOS and Windows. Therefore, we recommend debugging a native image in a Docker container or on Linux. If you have macOS or Windows, you can set up a virtual machine with Linux inside.
Create a native image with debug information in a Docker container
We will use Spring Petclinic as an example application and a Liberica Native Image Kit Container with Liberica Native Image Kit and Alpaquita Linux as a base image to create and run a native image inside a Docker container.
First of all, we need to add several build arguments to the GraalVM Maven plugin:
- The
-g
flag instructs thenative-image
tool to generate debug information in the format compatible with the GDB (GNU Debugger). This flag doesn’t affect the execution speed or memory consumption of the native image; - The
-O0
flag disables compiler optimizations.
It is also possible to include additional flags for better debugging experience:
- The
-H:+SourceLevelDebug
flag enables including full parameter and local variable information into the debug info. Note that using this flag may result in slower program execution; - The
-H:-DeleteLocalSymbols
flag enables including linkage symbols identifying compiled Java methods into the native image. This info is required for some perf use cases, such asperf annotate
.
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<configuration>
<buildArgs>
<buildArg>-g</buildArg>
<buildArg>-O0</buildArg>
</buildArgs>
</configuration>
</plugin>
After that, add the following Dockerfile to the project (the discussion of its contents is below):
FROM bellsoft/liberica-native-image-kit-container:jdk-21-nik-23.1.3-stream-musl as builder
RUN apk add libstdc++ freetype gdb libstdc++-dev freetype-dev
WORKDIR /app
ADD spring-petclinic-main /app/spring-petclinic-main
RUN cd spring-petclinic-main && ./mvnw -Dcheckstyle.skip -Dmaven.test.skip -Pnative native:compile
EXPOSE 8080
EXPOSE 2345
ENTRYPOINT ["gdbserver", ":2345", "/app/spring-petclinic-main/target/spring-petclinic"]
Let’s see what is happening here:
- First, we take Liberica Native Image Kit Container with Liberica Native Image Kit for JDK 21 and Alpaquita Linux as a base image for generating a native image;
- Then, we add the required packages from the Alpaquita repository: libstdc++ and libstdc++-dev for compiling C++ code, freetype and freetype-dev for font rendering, and gdb with the GDB Debugger;
- After that, we build a native image using the Maven plugin;
- Finally, we use the
gdbserver
command to launch the application in the debug mode (the gdbserver will listen on the specified port) and expose two ports, one for the application (8083), and another one for the debugger (2345).
Note that in this case, for simplicity's sake, we don’t copy the resulting binary into a fresh base image, meaning that our Docker image contains the whole project plus the binary and generated source files. As an alternative, you can store source files and symbols locally and deploy only the binary file.
Now, we can build the image with the standard docker build command:
docker build -t petclinic-native-debug . -f Dockerfile-ni-debug
Check the images
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
petclinic-native-debug latest 24efe11fc544 11 seconds ago 1.63GB
As you can see, the native image with the debug information included takes up 1.63GB, but as we are not going to use it in production, it is not critical.
Let’s run the image with the following flags:
Three essential flags making it possible to debug the app in a container. If you are using a Linux distro with a kernel version older than 5.9, you can use the --priviledged
flag instead of these options, but it is not recommended running the containers with elevated permissions:
--cap-add=SYS_PTRACE
makes it possible to debug the app inside the container;--security-opt seccomp=unconfined
enables running a container without the default Secure computing mode (seccomp) profile;--security-opt apparmor=unconfined
enables running a container without the AppArmour security profile.
The remaining options help us to configure Docker settings:
--rm
removes the container after it has been stopped;-it
opens an interactive container instance;--name
specifies the name of our container;-m
sets the memory limit for the container;-p
specifies the ports for the container.
docker run --rm -it --name spring-debug -m 1g -p 8083:8080 -p 2345:2345 --cap-add=SYS_PTRACE --security-opt seccomp=unconfined --security-opt apparmor=unconfined petclinic-native-debug
Process /app/spring-petclinic-main/target/spring-petclinic created; pid = 9
Listening on port 2345
That’s it! Your native image is ready for the debug session!
Debug native images in Eclipse IDE
With the setup described in the previous section, you can perform native image debugging from the console with GDB. An alternative approach is to use the IDE. For that purpose, you need to run the Docker container as described above and connect to it from the IDE using its IP address. Let’s see how we can do that in Eclipse. Note that you can do that only on x86 systems as the local GDB installation is required, and there are no builds for Aarch64 yet.
If your build and host environments are both located on a local machine, you can first generate a native executable of our project on a local machine so that the IDE has a C++ application to work with. You can do that by downloading Liberica Native Image Kit for your platform. After the installation, go to the project root directory and run the following command :
JAVA_HOME=/Path/To/NIK/Home mvn -Dcheckstyle.skip -Dmaven.test.skip -Pnative native:compile
The native image will be generated in the target directory.
Now,, we can move on to setting up the IDE.
Firstly, you need to install the Remote C++ Debugging plugin for Eclipse.
After that, export the Spring Petclinic project into Eclipse.
Right click on the project, select Properties → Project Natures. Add C++ Nature and C Nature.
Add a debug configuration to connect to a remote host where our container is running. Select Debug Configurations… → C/C++ Remote Application. Add the spring-petclinic project in the Main tab.
Then, select the Connection tab in the Debugger tab, and fill in the connection details (port number and IP address or hostname). For instance, if the container is running on localhost, leave localhost, otherwise, fill in the IP address. Remember that our debugger is listening on port 2345. Click Apply.
Click Debug.
Conclusion
In this article, we discussed how to set up remote native image debugging. We learned how to prepare a Docker container image with a native executable for the debugging session and how to set up native image debugging in Eclipse IDE. In the following article, we will discuss how to debug native images from Intellij IDEA. Subscribe to our newsletter so as not to miss it!