GDB Debug Native Part of Java Application (C - C++ Libraries and JDK) by Alexey Pirogov Medium
GDB Debug Native Part of Java Application (C - C++ Libraries and JDK) by Alexey Pirogov Medium
Listen Share
1 of 20 27-10-23 11:36
GDB: Debug native part of java application (C/C++ libraries and JDK) |... https://medium.com/@pirogov.alexey/gdb-debug-native-part-of-java-ap...
I worked with a few java projects that used native libraries created by another team
from the same organization. Usually, we invoked C++ code from java.
The issue with C++ code invoked from Java is that it is usually not visible from java.
We only see a top-level interface with JNI/JNA but don’t know what is going under
the hood.
As a result, we can’t get a lot of information from java debugger and profiler that we
use daily.
In this post, I’ll describe a gdb debugger that is able to work with the native code. As
an example, we will build a C++ library for Linux (.so-file), invoke from Java and
debug it.
As low-level part of JDK is also written in C++, we will take a look at how to debug
JDK native code also.
GDB
GDB or GNU Debugger is a command-line debugger that comes with most Linux
distributions and supports lots of processors. GDB supports both remote and local
mode.
It is important to note that as of now GDB doesn’t support debugging of Java code
(on https://www.gnu.org/software/gdb/ you won’t find Java in the list of supported
languages). If you want to debug java code from command-line you can try JDB. It
looks similar to GDB but has less functionality.
Having said that GDB doesn’t allow you to debug java-code, it can perfectly debug a
native part (written not in java) of Java application. If it is important for you to debug
both Java and native code “from the same IDE”, you can try to use Netbeans or
Eclipse. Netbeans uses default debugger for Java code and GDB for native code.
However, for IDE users switch between Java and the native code won’t be visible.
Of course, command-line debugging may look weird at first for people familiar with
IDEs. But it has some benefits. One of them is the ability to run on a remote host.
Although remote debug is built-in in java, debugging an application running in
2 of 20 27-10-23 11:36
GDB: Debug native part of java application (C/C++ libraries and JDK) |... https://medium.com/@pirogov.alexey/gdb-debug-native-part-of-java-ap...
another part of the globe using IDE remote debuggers could be very sloooooow. GDB
from the other hand runs on the target host. This is especially useful when you need
to evaluate some code during debugging.
To be able to see a native code in the debugger, the code should be compiled in a
special way. Information about method names and variables should be included in
the library or comes as a separate package.
Often the easiest way to include debug info is to add it to the resulted library. Let’s
write a simple java application and a C++ library.
We define an interface of native methods and load the library. The code won’t run,
because the library doesn’t exist right now. We will build it soon.
3 of 20 27-10-23 11:36
GDB: Debug native part of java application (C/C++ libraries and JDK) |... https://medium.com/@pirogov.alexey/gdb-debug-native-part-of-java-ap...
1 package jnidemo;
2
3 public class JNIDemoJava {
4
5 static {
6 System.load("/home/alex/src/JNIExperiments/JNIDemo/dist/libJNIDemo.so");
7 }
8
9 private static final int REPETITIONS = 100_000_000;
10
11 private native int nativeCrash();
12 private native int nativePrint();
13 private native int nativeSleep(int ms);
14 private native Double[] nativeAllocate(int n);
15
16 public static void main(String[] args) {
17 JNIDemoJava nativeCall = new JNIDemoJava();
18
19 for (int i = 0; i < REPETITIONS; i++) {
20 nativeCall.nativePrint();
21 nativeCall.nativeSleep(1000);
22 Double[] dArr = nativeCall.nativeAllocate(100);
23 for (Double d : dArr) {
24 System.out.println(System.currentTimeMillis());
25 }
26 }
27 }
28 }
4 of 20 27-10-23 11:36
GDB: Debug native part of java application (C/C++ libraries and JDK) |... https://medium.com/@pirogov.alexey/gdb-debug-native-part-of-java-ap...
5 of 20 27-10-23 11:36
GDB: Debug native part of java application (C/C++ libraries and JDK) |... https://medium.com/@pirogov.alexey/gdb-debug-native-part-of-java-ap...
1 #include <jni.h>
2 #include <stdio.h>
3 #include <string.h>
4 #include <unistd.h>
5 #include "jnidemo_JNIDemoJava.h"
6
7 JNIEXPORT jint JNICALL Java_jnidemo_JNIDemoJava_nativeCrash(JNIEnv *env, jobject obj) {
8 const char *s = NULL;
9 printf( "%c\n", s[0] );
10 return 0;
11 }
12
13
14 JNIEXPORT jint JNICALL Java_jnidemo_JNIDemoJava_nativePrint(JNIEnv *env, jobject obj) {
15 printf("\nHello World from C!\n");
16 return 0;
17 }
18
19 JNIEXPORT jint JNICALL Java_jnidemo_JNIDemoJava_nativeSleep
20 (JNIEnv *env, jobject obj, jint ms) {
21 usleep(ms);
22 return 0;
23 }
24
25 JNIEXPORT jobjectArray internalNativeAllocate(JNIEnv const *env, jint objNumber) {
26 jclass classDouble = (*env)->FindClass(env, "java/lang/Double");
27 jobjectArray outJNIArray = (*env)->NewObjectArray(env, objNumber, classDouble, NULL
28
29 for (int i = 0; i < objNumber; i++) {
30 jmethodID midDoubleInit = (*env)->GetMethodID(env, classDouble, "<init>", "(D)V"
31 if (NULL == midDoubleInit) return NULL;
32 jobject iDouble = (*env)->NewObject(env, classDouble, midDoubleInit, (double) i
33 (*env)->SetObjectArrayElement(env, outJNIArray, i, iDouble);
34 }
35
36 return outJNIArray;
37 }
38
39 jobjectArray JNICALL Java_jnidemo_JNIDemoJava_nativeAllocate
40 (JNIEnv *env, jobject obj, jint objNumber) {
41 return internalNativeAllocate(env, objNumber);
42 }
6 of 20 27-10-23 11:36
GDB: Debug native part of java application (C/C++ libraries and JDK) |... https://medium.com/@pirogov.alexey/gdb-debug-native-part-of-java-ap...
run “./scripts/buildLib.sh”
1 #!/usr/bin/env bash
2
3 rm -r build/ dist/
4 mkdir -p build dist
5 gcc -shared -c -g -I/usr/lib/jvm/jdk-11.0.1/include \
6 -I/usr/lib/jvm/jdk-11.0.1/include/linux \
7 -fPIC -MMD -MP -MF "build/JNIDemo.o.d" -o build/JNIDemo.o src/cpp/JNIDemo.c
8 gcc -shared -o dist/libJNIDemo.so build/JNIDemo.o -fPIC
One important option is -g. It tells GCC compiler to include debug information in the
library. It increases the size of the library and allows us to see source code during
debugging.
GDB debug
Let’s try to debug our code. You can start gdb and java app together or you can attach
gdb to the running app. Let’s check 2nd option as starting gdb together with the java
app may require modification of startup scripts (modification is quite simple).
Note: I had an issue with default ptrace settings on Ubuntu described here. I fixed it
with next command echo 0 > /proc/sys/kernel/yama/ptrace_scope.
7 of 20 27-10-23 11:36
GDB: Debug native part of java application (C/C++ libraries and JDK) |... https://medium.com/@pirogov.alexey/gdb-debug-native-part-of-java-ap...
# add a breakpoint to our code (all java packages has Java prefix)
(gdb) break Java_jnidemo_JNIDemoJava_nativeAllocate
Breakpoint 1 at 0x7f5e77dfe944: file src/cpp/JNIDemo.c, line 35.
# we can try to debug jdk code. In this case we don't have debug
# symbols and won't get a lot of information (but info about
# threads, registers, stacktrace is still available
(gdb) set step-mode on
(gdb) set step-mode onQuit
(gdb) s
24 jmethodID midDoubleInit = (*env)->GetMethodID(env,
classDouble, "<init>", "(D)V");
(gdb) s
0x00007f5ee44fc320 in jni_GetMethodID ()
from /usr/lib/jvm/jdk-11.0.1/lib/server/libjvm.so
#exit
quit
Debug JDK
We just got some experience in debugging native libraries. Our next step is to debug
the native part of JDK (a big part of jdk is written in C++).
8 of 20 27-10-23 11:36
GDB: Debug native part of java application (C/C++ libraries and JDK) |... https://medium.com/@pirogov.alexey/gdb-debug-native-part-of-java-ap...
#FileOutputStream.java
...
private native void writeBytes(byte b[], int off, int len, boolean
append)
throws IOException;
...
First, we need to download and compile JDK with extra parameters to preserve
debug symbols.
make clean
make all
Let’s start and debug our app with gdb using jdk from build/linux-x86_64-server-
slowdebug folder. Now we can see the source of writeBytes method
(Java_java_io_FileOutputStream_writeBytes). If we debug our app with usual jdk we
won’t see the source. This also allows us to see variables names.
9 of 20 27-10-23 11:36
GDB: Debug native part of java application (C/C++ libraries and JDK) |... https://medium.com/@pirogov.alexey/gdb-debug-native-part-of-java-ap...
append) {
70 writeBytes(env, this, bytes, off, len, append, fos_fd);
71 }
72
(gdb)
When java app crashes somewhere in the native code, Linux produces core-dump.
This file contains full memory snapshot with information about threads and
another useful information. We can analyze this file with different tools and gdb is
one of them.
However, in some production system core dumps may be disabled. One of the
reasons is their size. Let’s says that your app uses 80 GB of RAM (Java heap + objects
allocated by native code) then each crash will create an 80 GB core dump file. If you
have some system/script that checks if your app is alive every 5 minutes and restarts
it if it is dead, you can run out of disk space quite fast.
To demonstrate this scenario I created the nativeCrash method in our library (it tries
to access memory that wasn’t properly allocated).
1 ...
2
3 JNIEXPORT jint JNICALL Java_jnidemo_JNIDemoJava_nativeCrash(JNIEnv *env, jobject obj) {
4 const char *s = NULL;
5 printf( "%c\n", s[0] );
6 return 0;
7 }
8
9 ...
10 of 20 27-10-23 11:36
GDB: Debug native part of java application (C/C++ libraries and JDK) |... https://medium.com/@pirogov.alexey/gdb-debug-native-part-of-java-ap...
1 package jnidemo;
2
3 public class JNIDemoJava {
4
5 static {
6 System.load("/home/alex/src/JNIExperiments/JNIDemo/dist/libJNIDemo.so");
7 }
8
9 private native int nativeCrash();
10 private native int nativePrint();
11 private native int nativeSleep(int ms);
12 private native Double[] nativeAllocate(int n);
13
14 public static void main(String[] args) {
15 JNIDemoJava nativeCall = new JNIDemoJava();
16 nativeCall.nativeCrash();
17 }
18 }
Let’s start this app with -XX:OnError=”gdb — %p” option. Immediately after start
application will crash and we will get gdb in the terminal:
11 of 20 27-10-23 11:36
GDB: Debug native part of java application (C/C++ libraries and JDK) |... https://medium.com/@pirogov.alexey/gdb-debug-native-part-of-java-ap...
To check the source that failed and values assigned to it’s variable we can do next:
find thread gdb-id, switch to this thread, switch to the frame where we expect the
issue. Here how we can do this:
We found the code that caused the issue. If required we can print values of different
12 of 20 27-10-23 11:36
GDB: Debug native part of java application (C/C++ libraries and JDK) |... https://medium.com/@pirogov.alexey/gdb-debug-native-part-of-java-ap...
variables.
P.S. Note
More fromabout segfaults
Alexey Pirogov
When you will start debugging a large application, you may notice that the
execution of gdb suddenly stops. The reason is that gdb automatically stops app
when the segfault occurs. This makes sense for some applications but not for Java
apps. JDK uses different tools that may produce segfaults (speculative memory load,
NullPointerException, etc.). JDK handles SIGSEGV internally, but gdb has no idea
about this. This is why we need to force gdb to ignore them.
Alexey Pirogov
275 1
13 of 20 27-10-23 11:36
GDB: Debug native part of java application (C/C++ libraries and JDK) |... https://medium.com/@pirogov.alexey/gdb-debug-native-part-of-java-ap...
Alexey Pirogov
14 of 20 27-10-23 11:36
GDB: Debug native part of java application (C/C++ libraries and JDK) |... https://medium.com/@pirogov.alexey/gdb-debug-native-part-of-java-ap...
Alexey Pirogov
41
Alexey Pirogov
11
Recommended from Medium
15 of 20 27-10-23 11:36
GDB: Debug native part of java application (C/C++ libraries and JDK) |... https://medium.com/@pirogov.alexey/gdb-debug-native-part-of-java-ap...
Jorge Gonzalez
3 1
16 of 20 27-10-23 11:36
GDB: Debug native part of java application (C/C++ libraries and JDK) |... https://medium.com/@pirogov.alexey/gdb-debug-native-part-of-java-ap...
Chittaranjan Sethi
2 1
Lists
17 of 20 27-10-23 11:36
GDB: Debug native part of java application (C/C++ libraries and JDK) |... https://medium.com/@pirogov.alexey/gdb-debug-native-part-of-java-ap...
1.5K 20
18 of 20 27-10-23 11:36
GDB: Debug native part of java application (C/C++ libraries and JDK) |... https://medium.com/@pirogov.alexey/gdb-debug-native-part-of-java-ap...
Helidon 4 released!
We are happy and proud to announce that the long-awaited Helidon 4 has been released!
52 1
Naveen
19 of 20 27-10-23 11:36
GDB: Debug native part of java application (C/C++ libraries and JDK) |... https://medium.com/@pirogov.alexey/gdb-debug-native-part-of-java-ap...
Alexander Obregon
4 1
20 of 20 27-10-23 11:36