Ist das ein Fehler in der JVM oder in NASM?
Posted: 12 Jan 2025, 14:41
Ich glaube, ich habe einen Fehler gefunden, bin mir aber nicht sicher, ob ich JVM, JNI, NASM, GCC oder LD dafür verantwortlich machen soll.
Der Fehler verursacht mein kompiliertes Modding Sprache namens grug (siehe hier für Videos und Erklärungen), um Minecraft (Java-Edition) sporadisch zum Absturz zu bringen, wenn Mods ein SIGSEGV mit unendlicher Rekursion verursachen.
Meine Modding-Sprache besteht nur aus a 9k-Linie grug.c, das einen Compiler und Linker für meine benutzerdefinierte Programmiersprache liefert. Sein gemeinsames Objekt () Die Ausgabe basiert auf ca. 300 von mir geschriebenen Tests, die sie mit der erwarteten NASM-Ausgabe vergleichen. Mein Compiler ist also im Grunde ein NASM-Compiler, der .grug-Dateien als Eingabe verwendet und nicht .s x86-64-Assembly-Dateien. Ich erkläre dies, um zu verdeutlichen, warum ich NASM nicht einfach weglassen kann.
Nachdem ich tagelang mein ursprüngliches Programm sorgfältig verkleinert habe, habe ich endlich ein minimal reproduzierbares Beispiel, das diese Fehler zeigt wie FATAL ERROR in native method: Statische Feld-ID übergeben an JNI haben eine Wahrscheinlichkeit von etwa 1 zu 20, gedruckt zu werden, wenn diese Bedingungen erfüllt sind:
Was seltsam ist, ist, dass mage.o aus einer leeren mage.c mit gcc generiert wird führt anstelle der Verwendung von NASM nicht dazu, dass die Fehler gedruckt werden, weshalb ich denke, dass NASM schuld sein könnte.
Minimal reproduzierbares Beispiel< /h1>:
:
: eine leere Datei
: eine leere Datei
Kompilieren von foo.so (Sie müssen die JDK-Include-Pfade hier durch Ihre eigenen ersetzen, die ls /usr/lib/jvm sind kann helfen bei):
Dann montieren Sie mage.s zu mage.o:
Und verknüpfen Sie mage.o mit mage.so:
Schließlich führen wir Main.java in einer Endlosschleife aus, die schließlich FATAL ERROR in native method: Statische Feld-ID übergeben an JNI ausgeben sollte:
Strg+ drücken ein paar Mal unterbricht die Schleife, wo Sie sie dann mit kill %% beenden können.
Meine Fragen
Was ich nicht verstehe, ist, warum das Generieren von mage.o aus dem Kompilieren von mage.c statt aus dem Zusammenstellen von mage.s das Programm nie dazu bringt, den Fehler auszugeben:< /p>
Ich kann sehen, dass die Ausgabe von readelf -a mage.o, objdump -D mage.o und xxd mage.o ist alles deutlich größer, wenn mage.o aus mage.c generiert wird. Dies liegt daran, dass GCC GNU-spezifische Abschnitte in der ELF-Datei usw. ablegt. Daher vermute ich, dass der Fehler, der nur ausgegeben wird, wenn NASM zum Zusammenstellen von mage.s verwendet wird, möglicherweise mit den Abschnitten in der ELF-Datei zu tun hat?< /p>
Was ich auch nicht verstehe, ist, warum die pthread_equal()-Prüfung, die ich in den Signal-Handler eingefügt habe, der sofort beendet wird, den SCHWERWIEGENDEN FEHLER in der nativen Methode nicht verhindert: Statisches Feld ID übergeben an JNI wird nicht gedruckt. Ich habe angenommen, dass der Fehler dadurch verursacht wurde, dass ein interner JVM-Thread meinen Handler eingegeben hat, während er eigentlich den JVM-eigenen SIGSEGV-Handler eingeben sollte, aber ich vermute nicht?
Ich kenne das Das Programm gibt Warnungen darüber aus und möchte, dass es mit jsig ausgeführt wird. Wie ich jedoch in dieser Antwort beschrieben habe, ist die Verwendung von jsig nicht möglich, wenn Sie den SIGSEGV-Handler der JVM mit Ihrem eigenen Handler in C (as) überschreiben möchten Soweit ich das nach einer Woche Recherche beurteilen konnte).
Es ist leicht, die Hände hochzuwerfen, indem ich die Schuld für das seltsame Verhalten der NASM-Version darauf schiebe, dass ich nicht jsig verwende, aber es ergibt für mich keinen logischen Sinn. Ich bin mir immer noch nicht sicher, ob es sich tatsächlich um ein NASM- oder JVM-Problem handelt.
Ich verwende Ubuntu 24.04.1 und hier sind die Versionen der Programme, die ich im MRE aufrufe:
Der Fehler verursacht mein kompiliertes Modding Sprache namens grug (siehe hier für Videos und Erklärungen), um Minecraft (Java-Edition) sporadisch zum Absturz zu bringen, wenn Mods ein SIGSEGV mit unendlicher Rekursion verursachen.
Meine Modding-Sprache besteht nur aus a 9k-Linie grug.c, das einen Compiler und Linker für meine benutzerdefinierte Programmiersprache liefert. Sein gemeinsames Objekt (
Code: Select all
.so
Nachdem ich tagelang mein ursprüngliches Programm sorgfältig verkleinert habe, habe ich endlich ein minimal reproduzierbares Beispiel, das diese Fehler zeigt wie FATAL ERROR in native method: Statische Feld-ID übergeben an JNI haben eine Wahrscheinlichkeit von etwa 1 zu 20, gedruckt zu werden, wenn diese Bedingungen erfüllt sind:
- JNI wird zum Aufrufen einer C-Funktion verwendet das öffnet mage.so
- wurde mit ld aus mage.o generiert, wobei mage.o mit nasm aus mage.s generiert wurde (wobei mage.s kann nur eine leere Datei sein)
Code: Select all
mage.so
Nach dem Laden von mage.so wird JNI verwendet, um eine C-Funktion aufzurufen, die ein SIGSEGV verursacht
Was seltsam ist, ist, dass mage.o aus einer leeren mage.c mit gcc generiert wird führt anstelle der Verwendung von NASM nicht dazu, dass die Fehler gedruckt werden, weshalb ich denke, dass NASM schuld sein könnte.
Minimal reproduzierbares Beispiel< /h1>
Code: Select all
Main.java
Code: Select all
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();
}
}
}
Code: Select all
foo.c
Code: Select all
#include
#include
#include
#include
#include
#include
#include
#include
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 gives SIGSEGV its own stack
};
// Handle stack overflow
// See https://stackoverflow.com/a/7342398/13279557
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();
}
Code: Select all
mage.s
Code: Select all
mage.c
Kompilieren von foo.so (Sie müssen die JDK-Include-Pfade hier durch Ihre eigenen ersetzen, die ls /usr/lib/jvm sind kann helfen bei):
Code: Select all
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
Code: Select all
nasm mage.s -felf64
Code: Select all
ld mage.o -o mage.so -shared
Code: Select all
while true; do java -Xcheck:jni -XX:+AllowUserSignalHandlers -Djava.library.path=. Main.java; done
Code: Select all
Z
Meine Fragen
Was ich nicht verstehe, ist, warum das Generieren von mage.o aus dem Kompilieren von mage.c statt aus dem Zusammenstellen von mage.s das Programm nie dazu bringt, den Fehler auszugeben:< /p>
Code: Select all
gcc mage.c -c
Was ich auch nicht verstehe, ist, warum die pthread_equal()-Prüfung, die ich in den Signal-Handler eingefügt habe, der sofort beendet wird, den SCHWERWIEGENDEN FEHLER in der nativen Methode nicht verhindert: Statisches Feld ID übergeben an JNI wird nicht gedruckt. Ich habe angenommen, dass der Fehler dadurch verursacht wurde, dass ein interner JVM-Thread meinen Handler eingegeben hat, während er eigentlich den JVM-eigenen SIGSEGV-Handler eingeben sollte, aber ich vermute nicht?
Ich kenne das Das Programm gibt Warnungen darüber aus und möchte, dass es mit jsig ausgeführt wird. Wie ich jedoch in dieser Antwort beschrieben habe, ist die Verwendung von jsig nicht möglich, wenn Sie den SIGSEGV-Handler der JVM mit Ihrem eigenen Handler in C (as) überschreiben möchten Soweit ich das nach einer Woche Recherche beurteilen konnte).
Es ist leicht, die Hände hochzuwerfen, indem ich die Schuld für das seltsame Verhalten der NASM-Version darauf schiebe, dass ich nicht jsig verwende, aber es ergibt für mich keinen logischen Sinn. Ich bin mir immer noch nicht sicher, ob es sich tatsächlich um ein NASM- oder JVM-Problem handelt.
Ich verwende Ubuntu 24.04.1 und hier sind die Versionen der Programme, die ich im MRE aufrufe:
Code: Select all
$ 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 also reproduces the error
$ gcc --version
gcc (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0
$ ld --version
GNU ld (GNU Binutils for Ubuntu) 2.42