要么改变世界,要么适应世界

JNI 避免因为本地C/C++代码崩溃而引发虚拟机终止

2024-06-02 20:46:41
0
目录

前言

上文我们说过,由于Java调用的代码是其他语言实现的,这样会带来很多不可控的因素,例如在C/C++代码中,我们常常会因为访问了空指针而导致segmentation fault,最终导致程序提前结束。

而Java调用了一个发生了segmentation fault的动态链接库时,JVM也会提前结束程序,当发生这种情况时,JVM层面是无法通过捕获异常的方式避免的。

模拟错误

我们模拟一下这个过程,我们本地的C/C++代码如下:

#include "top_yalexin_jni_HelloJNI.h"
void generateSegmentationFault() {
	int* src = NULL;
	*src = 0;
}

int  times2(int value) {
	if (value > 10) {
		generateSegmentationFault();
	}
	return value * value;
}

int getPowWrapper(int value) {
	return times2(value);
}
JNIEXPORT jint JNICALL Java_top_yalexin_jni_HelloJNI_pow2
(JNIEnv* env, jobject thisObj, jint value) {
	return getPowWrapper(value);
}

我们的Java代码如下:

package top.yalexin.jni;

public class HelloJNI {
    static {
        // helloJNI.dll in Windows,  libhelloJNI.so in Unixes
        System.loadLibrary("helloJNI");
    }

    private native int pow2(int value);

    public static void main(String[] args) {
        System.out.println("====== start ======");
        HelloJNI helloJNI = new HelloJNI();
        try {
            System.out.println("pow2(5) -> " + helloJNI.pow2(5));
        } catch (Exception e) {
            System.out.println(e);
        }
        try {
            System.out.println("pow2(15) -> " + helloJNI.pow2(15));
        } catch (Exception e) {
            System.out.println(e);
        }
        System.out.println("====== end ======");
    }
}

按照上一篇文章的流程,我们在Windows上面的执行结果为:

java -Djava.library.path=top\yalexin\jni top.yalexin.jni.HelloJNI
====== start ======
pow2(5) -> 25
#
# A fatal error has been detected by the Java Runtime Environment:
#
#  EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x00007fff20f116b8, pid=25444, tid=28040
#
# JRE version: Java(TM) SE Runtime Environment (17.0.5+9) (build 17.0.5+9-LTS-191)
# Java VM: Java HotSpot(TM) 64-Bit Server VM (17.0.5+9-LTS-191, mixed mode, sharing, tiered, compressed oops, compressed class ptrs, g1 gc, windows-amd64)
# Problematic frame:
# C  [helloJNI.dll+0x116b8]
#
# No core dump will be written. Minidumps are not enabled by default on client versions of Windows
#
# An error report file with more information is saved as:
# C:\Users\Yalexin\IdeaProjects\hello-world\src\hs_err_pid25444.log
#
# If you would like to submit a bug report, please visit:
#   https://bugreport.java.com/bugreport/crash.jsp
# The crash happened outside the Java Virtual Machine in native code.
# See problematic frame for where to report the bug.
#

在Linux平台上面显示为:

java -Djava.library.path='/root/jni' top.yalexin.jni.HelloJNI
====== start ======
pow2(5) -> 25
#
# A fatal error has been detected by the Java Runtime Environment:
#
#  SIGSEGV (0xb) at pc=0x00007fa7793d87b5, pid=6049, tid=0x00007fa77cce4700
#
# JRE version: Java(TM) SE Runtime Environment (8.0_411) (build 1.8.0_411-b09)
# Java VM: Java HotSpot(TM) 64-Bit Server VM (25.411-b09 mixed mode linux-amd64 compressed oops)
# Problematic frame:
# C  [libhelloJNI.so+0x7b5]  generateSegmentationFault()+0x10
#
# Failed to write core dump. Core dumps have been disabled. To enable core dumping, try "ulimit -c unlimited" before starting Java again
#
# An error report file with more information is saved as:
# /root/jni/hs_err_pid6049.log
#
# If you would like to submit a bug report, please visit:
#   http://bugreport.java.com/bugreport/crash.jsp
# The crash happened outside the Java Virtual Machine in native code.
# See problematic frame for where to report the bug.
#
[1]    6049 abort      java -Djava.library.path='/root/jni' top.yalexin.jni.HelloJNI

我们可以看到,我们的Java虚拟机也提前被迫中断了,有没有什么办法能够避免这种情况呢?

Windows系统解决方案

还是借助Visual Studio,具体使用方案参照上一篇文章,我们只要把最外面的代码使用捕获异常的方式即可:

#include "top_yalexin_jni_HelloJNI.h"
#include <Windows.h>
void generateSegmentationFault() {
	int* src = NULL;
	*src = 0;
}

int  times2(int value) {
	if (value > 10) {
		generateSegmentationFault();
	}
	return value * value;
}

int getPowWrapper(int value) {
	return times2(value);
}
JNIEXPORT jint JNICALL Java_top_yalexin_jni_HelloJNI_pow2
(JNIEnv* env, jobject thisObj, jint value) {

	__try {
		return getPowWrapper(value);
	}
	__except (EXCEPTION_EXECUTE_HANDLER)
	{
		// 当 segmentation fault 出现时, 抛出 Java exception
		jclass exc = env->FindClass("java/lang/RuntimeException");
		if (exc != NULL)
		{
			env->ThrowNew(exc, "Segmentation fault occurred in native code pow2");
		}
		return -1;
	}

	
}

再次执行代码,我们发现,我们的代码成功捕获了该异常:

java -Djava.library.path=top\yalexin\jni top.yalexin.jni.HelloJNI
====== start ======
pow2(5) -> 25
java.lang.RuntimeException: Segmentation fault occurred in native code pow2
====== end ======

Linux系统解决方案

在Linux上面,上述代码无法执行,这是因为Windows.h是Windows下专有的头文件。

但是可以可借助信号量机制来完成:

#include "top_yalexin_jni_HelloJNI.h"
#include <signal.h>
#include <setjmp.h>

// 定义全局变量用于存储跳转环境
static jmp_buf jump_buffer;

// 信号处理函数
void handle_sigsegv(int sig) {
	if (sig == SIGSEGV) {
		// 执行非本地跳转,恢复到设置的环境
		longjmp(jump_buffer, 1);
	}
}
void generateSegmentationFault() {
	int* src = NULL;
	*src = 0;
}

int  times2(int value) {
	if (value > 10) {
		generateSegmentationFault();
	}
	return value * value;
}

int getPowWrapper(int value) {
	return times2(value);
}
JNIEXPORT jint JNICALL Java_top_yalexin_jni_HelloJNI_pow2
(JNIEnv* env, jobject thisObj, jint value) {

	// 设置跳转环境, setjmp 返回0表示直接调用,非零表示从 longjmp 跳转回来
	if (setjmp(jump_buffer) == 0) {
		// 设置信号处理函数
		signal(SIGSEGV, handle_sigsegv);
		// 下面的代码有可能引发访问违例
		return getPowWrapper(value);
		// 如果没有违例(segmentation fault),则按照既定逻辑返回
	}
	else {
		// 当 segmentation fault 出现时, 抛出 Java exception
		jclass exc = env->FindClass("java/lang/RuntimeException");
		if (exc != NULL)
		{
			env->ThrowNew(exc, "Segmentation fault occurred in native code pow2");
		}
		return -1;
	}
}

再次运行,我们发现,我们的代码成功捕获了该异常:

java -Djava.library.path='/root/jni' top.yalexin.jni.HelloJNI
====== start ======
pow2(5) -> 25
java.lang.RuntimeException: Segmentation fault occurred in native code pow2
====== end ======

总结

实际应用中,我们调用的.dll或者.so文件可能内部逻辑非常复杂,这就需要我们对别人的动态链接库做进一步封装,然后我们自己在C/C++代码中主动捕获异常信息,然后把异常信息给到Java代码中做进一步处理。

历史评论
开始评论