JNI加载NASM生成的.so后偶现JNI致命错误,需排查JVM/NASM问题
偶现JNI致命错误的定位疑问:JVM/NASM/GCC/ld?
我发现一个偶现Bug,会导致我开发的编译型模组语言grug在模组触发无限递归SIGSEGV时,偶尔让Minecraft Java版崩溃。grug的编译器核心是将.grug文件转换为NASM输出,依赖NASM生成共享对象(.so),因此无法弃用NASM。
经过精简,我得到最小复现场景:满足以下条件时,约1/20概率出现**"FATAL ERROR in native method: Static field ID passed to JNI"**错误:
- 通过JNI调用C函数加载
mage.so mage.so由ld链接NASM汇编空文件生成的mage.o得到- 加载
mage.so后,通过JNI调用C函数触发SIGSEGV
该问题已在Ubuntu和Arch Linux系统上复现。奇怪的是,若用GCC编译空文件mage.c生成mage.o再链接为mage.so,则不会出现错误,因此我怀疑问题可能出在NASM上。
最小复现案例
Main.java
class Main { private native void init(); private native void foo(); public static void main(String[] args) { new Main().run(); } public void run() { System.loadLibrary("foo"); init(); long iteration = 0; for (int i = 0; i < 2; i++) { System.out.println("Iteration: " + ++iteration); foo(); } } }
foo.c
#include <dlfcn.h> #include <jni.h> #include <pthread.h> #include <setjmp.h> #include <signal.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> jmp_buf jmp_buffer; volatile pthread_t expected_thread; static void segv_handler(int sig) { (void)sig; { char msg[] = "In segv_handler()\n"; write(STDERR_FILENO, msg, sizeof(msg)-1); } if (!pthread_equal(pthread_self(), expected_thread)) { char msg[] = "Unexpected thread entered handler; exiting\n"; write(STDERR_FILENO, msg, sizeof(msg)-1); _exit(EXIT_FAILURE); } siglongjmp(jmp_buffer, 1); } JNIEXPORT void JNICALL Java_Main_init(JNIEnv *env, jobject obj) { (void)env; (void)obj; fprintf(stderr, "Initializing...\n"); struct sigaction sigsegv_sa = { .sa_handler = segv_handler, .sa_flags = SA_ONSTACK, // SA_ONSTACK 为SIGSEGV分配独立栈 }; // 处理栈溢出 static char stack[SIGSTKSZ]; stack_t ss = { .ss_size = SIGSTKSZ, .ss_sp = stack, }; if (sigaltstack(&ss, NULL) == -1) { perror("sigaltstack"); exit(EXIT_FAILURE); } if (sigfillset(&sigsegv_sa.sa_mask) == -1) { perror("sigfillset"); exit(EXIT_FAILURE); } if (sigaction(SIGSEGV, &sigsegv_sa, NULL) == -1) { perror("sigaction"); exit(EXIT_FAILURE); } void *dll = dlopen("./mage.so", RTLD_NOW); if (!dll) { fprintf(stderr, "dlopen(): %s\n", dlerror()); } } void recurse() { recurse(); } JNIEXPORT void JNICALL Java_Main_foo(JNIEnv *env, jobject obj) { (void)env; (void)obj; expected_thread = pthread_self(); if (sigsetjmp(jmp_buffer, 1)) { fprintf(stderr, "Jumped\n"); return; } fprintf(stderr, "Recursing...\n"); recurse(); }
mage.s:空文件
mage.c:空文件
编译与运行步骤
- 编译
foo.so(需替换为本地JDK头文件路径,可通过ls /usr/lib/jvm查看):
gcc foo.c -o libfoo.so -shared -fPIC -g -Wall -Wextra -Wpedantic -Werror -Wfatal-errors -Wno-infinite-recursion -I/usr/lib/jvm/jdk-23.0.1-oracle-x64/include -I/usr/lib/jvm/jdk-23.0.1-oracle-x64/include/linux
- 汇编
mage.s生成mage.o:
nasm mage.s -felf64
- 链接
mage.o生成mage.so:
ld mage.o -o mage.so -shared
- 循环运行
Main.java触发错误:
while true; do java -Xcheck:jni -XX:+AllowUserSignalHandlers -Djava.library.path=. Main.java; done
- 按
Ctrl+Z可暂停循环,使用kill %%终止进程
核心疑问
- 为什么用GCC编译空
mage.c生成的.so不会出错?对比两种方式生成的mage.o,GCC会添加GNU特定节,是否错误与ELF文件节结构有关? - 信号处理函数中加入了
pthread_equal检查,非预期线程进入就直接退出,为何仍无法阻止JNI致命错误?原本以为是JVM内部线程进入自定义处理函数导致的,但现在看似乎并非如此。
补充:我知道官方提示需用jsig运行,但在自定义SIGSEGV处理函数的场景下无法使用jsig。目前仍无法确定问题根源是JVM还是NASM。
环境版本
$ java --version java 23.0.1 2024-10-15 Java(TM) SE Runtime Environment (build 23.0.1+11-39) Java HotSpot(TM) 64-Bit Server VM (build 23.0.1+11-39, mixed mode, sharing) $ nasm --version NASM version 2.16.01 # 2.16.03-1也能复现错误 $ gcc --version gcc (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0 $ ld --version GNU ld (GNU Binutils for Ubuntu) 2.42
内容的提问来源于stack exchange,提问作者MyNameIsTrez
相关产品推荐
相关产品推荐

