// JNI tracing for the native ARM32 investigation (see // ARM64_TRANSLATION_LAYER.md's "Native ARM32 tracing harness" plan). // // JNI calls go through the JNIEnv->functions function-pointer table, not // the dynamic symbol table - LD_PRELOAD interposition (libc_gles_trace.cpp) // can't see them. Real Android ART shares ONE JNINativeInterface table // across every thread's JNIEnv (confirmed by design, not assumed - this is // standard JVM/ART behavior), so this patches the table'S CONTENTS in // place (not swapping which struct `functions` points to) - covers every // thread automatically, including whichever one actually drives the game's // real onDrawFrame/RunLoop-tick GL thread, not just whichever thread // happens to call the installer below. // // 2026-09-06: widened from the original curated dozen to a much broader // call-flow set (method/field access, object lifecycle, exceptions, local // frames) - the point of a "full" trace is reconstructing execution order // on real hardware, and the original subset was too narrow for that. Still // deliberately skips a few genuinely high-frequency, low-information calls // (DeleteLocalRef, array element accessors) - see their own comments below. #include #include #include #include #include "trace_log.h" using trace_agent::LookupName; using trace_agent::RememberName; using trace_agent::TraceLog; namespace { JNINativeInterface g_orig{}; // real function pointers, saved before patching, so wrappers can call through jclass Wrap_FindClass(JNIEnv* env, const char* name) { jclass r = g_orig.FindClass(env, name); TraceLog("JNI FindClass(\"%s\") -> %p%s", name ? name : "(null)", (void*)r, r == nullptr ? " [FAILED]" : ""); return r; } // Diagnostic for the ARM64_TRANSLATION_LAYER.md "external-storage overlay // at VFS root" investigation - is GameActivityMain.useAssetsFileSystem() // even called on real native ARM32, and what does it actually return? // The Java-side implementation is this project's own (native32 shares // GameActivityMain.kt with the translated/emulated flavor), so a // same-vs-different result here isn't about different source code - it's // about whether the same code produces a different real answer natively. jmethodID g_useAssetsFileSystemId = nullptr; jmethodID Wrap_GetMethodID(JNIEnv* env, jclass clazz, const char* name, const char* sig) { jmethodID r = g_orig.GetMethodID(env, clazz, name, sig); TraceLog("JNI GetMethodID(\"%s\", \"%s\") -> %p%s", name ? name : "(null)", sig ? sig : "(null)", (void*)r, r == nullptr ? " [FAILED]" : ""); if (r) RememberName(r, name, sig); if (name && r && strcmp(name, "useAssetsFileSystem") == 0) { g_useAssetsFileSystemId = r; TraceLog("JNI GetMethodID: cached useAssetsFileSystem jmethodID=%p for CallBooleanMethod tracing", (void*)r); } return r; } jboolean Wrap_CallBooleanMethod(JNIEnv* env, jobject obj, jmethodID id, ...) { va_list ap; va_start(ap, id); jboolean r = g_orig.CallBooleanMethodV(env, obj, id, ap); va_end(ap); TraceLog("JNI CallBooleanMethod: %s(obj=%p) -> %s", LookupName(id).c_str(), (void*)obj, r ? "true" : "false"); return r; } jboolean Wrap_CallBooleanMethodV(JNIEnv* env, jobject obj, jmethodID id, va_list args) { // va_list can only be consumed once - copy before forwarding so the // real call still gets a fresh, valid list. va_list copy; va_copy(copy, args); jboolean r = g_orig.CallBooleanMethodV(env, obj, id, copy); va_end(copy); TraceLog("JNI CallBooleanMethodV: %s(obj=%p) -> %s", LookupName(id).c_str(), (void*)obj, r ? "true" : "false"); return r; } jboolean Wrap_CallBooleanMethodA(JNIEnv* env, jobject obj, jmethodID id, const jvalue* args) { jboolean r = g_orig.CallBooleanMethodA(env, obj, id, args); TraceLog("JNI CallBooleanMethodA: %s(obj=%p) -> %s", LookupName(id).c_str(), (void*)obj, r ? "true" : "false"); return r; } jmethodID Wrap_GetStaticMethodID(JNIEnv* env, jclass clazz, const char* name, const char* sig) { jmethodID r = g_orig.GetStaticMethodID(env, clazz, name, sig); TraceLog("JNI GetStaticMethodID(\"%s\", \"%s\") -> %p%s", name ? name : "(null)", sig ? sig : "(null)", (void*)r, r == nullptr ? " [FAILED]" : ""); if (r) RememberName(r, name, sig); return r; } jfieldID Wrap_GetFieldID(JNIEnv* env, jclass clazz, const char* name, const char* sig) { jfieldID r = g_orig.GetFieldID(env, clazz, name, sig); TraceLog("JNI GetFieldID(\"%s\", \"%s\") -> %p%s", name ? name : "(null)", sig ? sig : "(null)", (void*)r, r == nullptr ? " [FAILED]" : ""); if (r) RememberName(r, name, sig); return r; } jfieldID Wrap_GetStaticFieldID(JNIEnv* env, jclass clazz, const char* name, const char* sig) { jfieldID r = g_orig.GetStaticFieldID(env, clazz, name, sig); TraceLog("JNI GetStaticFieldID(\"%s\", \"%s\") -> %p%s", name ? name : "(null)", sig ? sig : "(null)", (void*)r, r == nullptr ? " [FAILED]" : ""); if (r) RememberName(r, name, sig); return r; } jint Wrap_RegisterNatives(JNIEnv* env, jclass clazz, const JNINativeMethod* methods, jint n) { jint r = g_orig.RegisterNatives(env, clazz, methods, n); TraceLog("JNI RegisterNatives(count=%d) -> %d%s", n, r, r != 0 ? " [FAILED]" : ""); for (jint i = 0; i < n; i++) { TraceLog("JNI RegisterNatives: %s %s", methods[i].name, methods[i].signature); } return r; } jstring Wrap_NewStringUTF(JNIEnv* env, const char* bytes) { jstring r = g_orig.NewStringUTF(env, bytes); TraceLog("JNI NewStringUTF(\"%s\") -> %p", bytes ? bytes : "(null)", (void*)r); return r; } const char* Wrap_GetStringUTFChars(JNIEnv* env, jstring str, jboolean* isCopy) { const char* r = g_orig.GetStringUTFChars(env, str, isCopy); TraceLog("JNI GetStringUTFChars() -> \"%s\"", r ? r : "(null)"); return r; } jboolean Wrap_ExceptionCheck(JNIEnv* env) { jboolean r = g_orig.ExceptionCheck(env); if (r) TraceLog("JNI ExceptionCheck() -> TRUE"); return r; } void Wrap_ExceptionDescribe(JNIEnv* env) { TraceLog("JNI ExceptionDescribe() called"); g_orig.ExceptionDescribe(env); } // ---- object lifecycle / class checks ---- jclass Wrap_GetObjectClass(JNIEnv* env, jobject obj) { jclass r = g_orig.GetObjectClass(env, obj); TraceLog("JNI GetObjectClass(obj=%p) -> %p", (void*)obj, (void*)r); return r; } jboolean Wrap_IsInstanceOf(JNIEnv* env, jobject obj, jclass clazz) { jboolean r = g_orig.IsInstanceOf(env, obj, clazz); TraceLog("JNI IsInstanceOf(obj=%p, class=%p) -> %s", (void*)obj, (void*)clazz, r ? "true" : "false"); return r; } jboolean Wrap_IsSameObject(JNIEnv* env, jobject a, jobject b) { jboolean r = g_orig.IsSameObject(env, a, b); TraceLog("JNI IsSameObject(%p, %p) -> %s", (void*)a, (void*)b, r ? "true" : "false"); return r; } jobject Wrap_NewGlobalRef(JNIEnv* env, jobject obj) { jobject r = g_orig.NewGlobalRef(env, obj); TraceLog("JNI NewGlobalRef(%p) -> %p", (void*)obj, (void*)r); return r; } void Wrap_DeleteGlobalRef(JNIEnv* env, jobject obj) { TraceLog("JNI DeleteGlobalRef(%p)", (void*)obj); g_orig.DeleteGlobalRef(env, obj); } // DeleteLocalRef is deliberately NOT wrapped - by far the highest-frequency // JNI call in any real run (every local ref a native function creates gets // cleaned up through it), and it carries no name/signature information of // its own. Logging it would reintroduce the exact volume problem // libc_gles_trace.cpp's IsInteresting() was added to avoid, for a line that // never says which method/field it's tied to. jint Wrap_PushLocalFrame(JNIEnv* env, jint cap) { TraceLog("JNI PushLocalFrame(%d)", (int)cap); return g_orig.PushLocalFrame(env, cap); } jobject Wrap_PopLocalFrame(JNIEnv* env, jobject result) { TraceLog("JNI PopLocalFrame()"); return g_orig.PopLocalFrame(env, result); } // ---- exceptions ---- jint Wrap_Throw(JNIEnv* env, jthrowable obj) { TraceLog("JNI Throw(%p)", (void*)obj); return g_orig.Throw(env, obj); } jint Wrap_ThrowNew(JNIEnv* env, jclass clazz, const char* msg) { TraceLog("JNI ThrowNew(class=%p, \"%s\")", (void*)clazz, msg ? msg : "(null)"); return g_orig.ThrowNew(env, clazz, msg); } jthrowable Wrap_ExceptionOccurred(JNIEnv* env) { jthrowable r = g_orig.ExceptionOccurred(env); if (r) TraceLog("JNI ExceptionOccurred() -> %p", (void*)r); return r; } void Wrap_ExceptionClear(JNIEnv* env) { TraceLog("JNI ExceptionClear()"); g_orig.ExceptionClear(env); } // ---- object construction / arrays ---- jobject Wrap_NewObject(JNIEnv* env, jclass clazz, jmethodID id, ...) { va_list ap; va_start(ap, id); jobject r = g_orig.NewObjectV(env, clazz, id, ap); va_end(ap); TraceLog("JNI NewObject: %s -> %p", LookupName(id).c_str(), (void*)r); return r; } jobject Wrap_NewObjectV(JNIEnv* env, jclass clazz, jmethodID id, va_list args) { va_list copy; va_copy(copy, args); jobject r = g_orig.NewObjectV(env, clazz, id, copy); va_end(copy); TraceLog("JNI NewObjectV: %s -> %p", LookupName(id).c_str(), (void*)r); return r; } jsize Wrap_GetArrayLength(JNIEnv* env, jarray arr) { jsize r = g_orig.GetArrayLength(env, arr); TraceLog("JNI GetArrayLength(%p) -> %d", (void*)arr, (int)r); return r; } jobjectArray Wrap_NewObjectArray(JNIEnv* env, jsize len, jclass clazz, jobject init) { jobjectArray r = g_orig.NewObjectArray(env, len, clazz, init); TraceLog("JNI NewObjectArray(len=%d, class=%p) -> %p", (int)len, (void*)clazz, (void*)r); return r; } // ---- instance Call*Method / Call*MethodV families (mechanical - same // shape as jni_shim.cpp's own DoCall/DoCallV on the emulated side) ---- #define DEF_CALL_METHOD(RetType, Name) \ RetType Wrap_Call##Name##Method(JNIEnv* env, jobject obj, jmethodID id, ...) { \ va_list ap; \ va_start(ap, id); \ RetType r = g_orig.Call##Name##MethodV(env, obj, id, ap); \ va_end(ap); \ TraceLog("JNI Call" #Name "Method: %s(obj=%p)", LookupName(id).c_str(), (void*)obj); \ return r; \ } \ RetType Wrap_Call##Name##MethodV(JNIEnv* env, jobject obj, jmethodID id, va_list args) { \ va_list copy; \ va_copy(copy, args); \ RetType r = g_orig.Call##Name##MethodV(env, obj, id, copy); \ va_end(copy); \ TraceLog("JNI Call" #Name "MethodV: %s(obj=%p)", LookupName(id).c_str(), (void*)obj); \ return r; \ } // void has no return value to capture, so it can't share DEF_CALL_METHOD's // RetType-returning shape. void Wrap_CallVoidMethod(JNIEnv* env, jobject obj, jmethodID id, ...) { va_list ap; va_start(ap, id); g_orig.CallVoidMethodV(env, obj, id, ap); va_end(ap); TraceLog("JNI CallVoidMethod: %s(obj=%p)", LookupName(id).c_str(), (void*)obj); } void Wrap_CallVoidMethodV(JNIEnv* env, jobject obj, jmethodID id, va_list args) { va_list copy; va_copy(copy, args); g_orig.CallVoidMethodV(env, obj, id, copy); va_end(copy); TraceLog("JNI CallVoidMethodV: %s(obj=%p)", LookupName(id).c_str(), (void*)obj); } DEF_CALL_METHOD(jobject, Object) DEF_CALL_METHOD(jint, Int) DEF_CALL_METHOD(jlong, Long) DEF_CALL_METHOD(jfloat, Float) DEF_CALL_METHOD(jdouble, Double) #undef DEF_CALL_METHOD // ---- static Call*Method / Call*MethodV families ---- #define DEF_CALL_STATIC_METHOD(RetType, Name) \ RetType Wrap_CallStatic##Name##Method(JNIEnv* env, jclass clazz, jmethodID id, ...) { \ va_list ap; \ va_start(ap, id); \ RetType r = g_orig.CallStatic##Name##MethodV(env, clazz, id, ap); \ va_end(ap); \ TraceLog("JNI CallStatic" #Name "Method: %s", LookupName(id).c_str()); \ return r; \ } \ RetType Wrap_CallStatic##Name##MethodV(JNIEnv* env, jclass clazz, jmethodID id, va_list args) { \ va_list copy; \ va_copy(copy, args); \ RetType r = g_orig.CallStatic##Name##MethodV(env, clazz, id, copy); \ va_end(copy); \ TraceLog("JNI CallStatic" #Name "MethodV: %s", LookupName(id).c_str()); \ return r; \ } void Wrap_CallStaticVoidMethod(JNIEnv* env, jclass clazz, jmethodID id, ...) { va_list ap; va_start(ap, id); g_orig.CallStaticVoidMethodV(env, clazz, id, ap); va_end(ap); TraceLog("JNI CallStaticVoidMethod: %s", LookupName(id).c_str()); } void Wrap_CallStaticVoidMethodV(JNIEnv* env, jclass clazz, jmethodID id, va_list args) { va_list copy; va_copy(copy, args); g_orig.CallStaticVoidMethodV(env, clazz, id, copy); va_end(copy); TraceLog("JNI CallStaticVoidMethodV: %s", LookupName(id).c_str()); } DEF_CALL_STATIC_METHOD(jobject, Object) DEF_CALL_STATIC_METHOD(jboolean, Boolean) DEF_CALL_STATIC_METHOD(jint, Int) DEF_CALL_STATIC_METHOD(jlong, Long) DEF_CALL_STATIC_METHOD(jfloat, Float) DEF_CALL_STATIC_METHOD(jdouble, Double) #undef DEF_CALL_STATIC_METHOD // ---- instance Get*Field / Set*Field families ---- #define DEF_GET_FIELD(RetType, Name) \ RetType Wrap_Get##Name##Field(JNIEnv* env, jobject obj, jfieldID id) { \ RetType r = g_orig.Get##Name##Field(env, obj, id); \ TraceLog("JNI Get" #Name "Field: %s(obj=%p)", LookupName(id).c_str(), (void*)obj); \ return r; \ } #define DEF_SET_FIELD(ValType, Name) \ void Wrap_Set##Name##Field(JNIEnv* env, jobject obj, jfieldID id, ValType val) { \ TraceLog("JNI Set" #Name "Field: %s(obj=%p)", LookupName(id).c_str(), (void*)obj); \ g_orig.Set##Name##Field(env, obj, id, val); \ } DEF_GET_FIELD(jobject, Object) DEF_GET_FIELD(jboolean, Boolean) DEF_GET_FIELD(jint, Int) DEF_GET_FIELD(jlong, Long) DEF_GET_FIELD(jfloat, Float) DEF_GET_FIELD(jdouble, Double) DEF_SET_FIELD(jobject, Object) DEF_SET_FIELD(jboolean, Boolean) DEF_SET_FIELD(jint, Int) DEF_SET_FIELD(jlong, Long) DEF_SET_FIELD(jfloat, Float) DEF_SET_FIELD(jdouble, Double) #undef DEF_GET_FIELD #undef DEF_SET_FIELD // ---- static Get*Field / Set*Field families ---- #define DEF_GET_STATIC_FIELD(RetType, Name) \ RetType Wrap_GetStatic##Name##Field(JNIEnv* env, jclass clazz, jfieldID id) { \ RetType r = g_orig.GetStatic##Name##Field(env, clazz, id); \ TraceLog("JNI GetStatic" #Name "Field: %s", LookupName(id).c_str()); \ return r; \ } #define DEF_SET_STATIC_FIELD(ValType, Name) \ void Wrap_SetStatic##Name##Field(JNIEnv* env, jclass clazz, jfieldID id, ValType val) { \ TraceLog("JNI SetStatic" #Name "Field: %s", LookupName(id).c_str()); \ g_orig.SetStatic##Name##Field(env, clazz, id, val); \ } DEF_GET_STATIC_FIELD(jobject, Object) DEF_GET_STATIC_FIELD(jboolean, Boolean) DEF_GET_STATIC_FIELD(jint, Int) DEF_SET_STATIC_FIELD(jobject, Object) DEF_SET_STATIC_FIELD(jboolean, Boolean) DEF_SET_STATIC_FIELD(jint, Int) #undef DEF_GET_STATIC_FIELD #undef DEF_SET_STATIC_FIELD } // namespace // Kotlin side: nfs.mod.traceagent.TraceAgentBridge.install(outputPath) - a // tiny new class added purely for this investigation (see the plan's Phase // 3 file list), called from a new minimal Application.attachBaseContext, as // early as possible, before any real game library gets a chance to grab its // own JNIEnv reference. outputPath is the app's own private files dir (see // TraceApplication.kt) - trace_log.h opens it for the whole process's life. extern "C" JNIEXPORT void JNICALL Java_nfs_mod_traceagent_TraceAgentBridge_install(JNIEnv* env, jclass, jstring outputPath) { if (outputPath) { const char* path = env->GetStringUTFChars(outputPath, nullptr); trace_agent::InitFileLog(path); env->ReleaseStringUTFChars(outputPath, path); } auto* table = const_cast(env->functions); g_orig = *table; long pageSize = sysconf(_SC_PAGESIZE); uintptr_t base = (uintptr_t)table & ~(uintptr_t)(pageSize - 1); size_t span = ((uintptr_t)table + sizeof(*table)) - base; if (mprotect((void*)base, span, PROT_READ | PROT_WRITE) != 0) { TraceLog("InstallJniTraceHooks: mprotect failed - JNI tracing disabled, libc/GLES tracing (if LD_PRELOAD'd) still active"); return; } table->FindClass = Wrap_FindClass; table->GetMethodID = Wrap_GetMethodID; table->CallBooleanMethod = Wrap_CallBooleanMethod; table->CallBooleanMethodV = Wrap_CallBooleanMethodV; table->CallBooleanMethodA = Wrap_CallBooleanMethodA; table->GetStaticMethodID = Wrap_GetStaticMethodID; table->GetFieldID = Wrap_GetFieldID; table->GetStaticFieldID = Wrap_GetStaticFieldID; table->RegisterNatives = Wrap_RegisterNatives; table->NewStringUTF = Wrap_NewStringUTF; table->GetStringUTFChars = Wrap_GetStringUTFChars; table->ExceptionCheck = Wrap_ExceptionCheck; table->ExceptionDescribe = Wrap_ExceptionDescribe; table->GetObjectClass = Wrap_GetObjectClass; table->IsInstanceOf = Wrap_IsInstanceOf; table->IsSameObject = Wrap_IsSameObject; table->NewGlobalRef = Wrap_NewGlobalRef; table->DeleteGlobalRef = Wrap_DeleteGlobalRef; table->PushLocalFrame = Wrap_PushLocalFrame; table->PopLocalFrame = Wrap_PopLocalFrame; table->Throw = Wrap_Throw; table->ThrowNew = Wrap_ThrowNew; table->ExceptionOccurred = Wrap_ExceptionOccurred; table->ExceptionClear = Wrap_ExceptionClear; table->NewObject = Wrap_NewObject; table->NewObjectV = Wrap_NewObjectV; table->GetArrayLength = Wrap_GetArrayLength; table->NewObjectArray = Wrap_NewObjectArray; table->CallVoidMethod = Wrap_CallVoidMethod; table->CallVoidMethodV = Wrap_CallVoidMethodV; table->CallObjectMethod = Wrap_CallObjectMethod; table->CallObjectMethodV = Wrap_CallObjectMethodV; table->CallIntMethod = Wrap_CallIntMethod; table->CallIntMethodV = Wrap_CallIntMethodV; table->CallLongMethod = Wrap_CallLongMethod; table->CallLongMethodV = Wrap_CallLongMethodV; table->CallFloatMethod = Wrap_CallFloatMethod; table->CallFloatMethodV = Wrap_CallFloatMethodV; table->CallDoubleMethod = Wrap_CallDoubleMethod; table->CallDoubleMethodV = Wrap_CallDoubleMethodV; table->CallStaticVoidMethod = Wrap_CallStaticVoidMethod; table->CallStaticVoidMethodV = Wrap_CallStaticVoidMethodV; table->CallStaticObjectMethod = Wrap_CallStaticObjectMethod; table->CallStaticObjectMethodV = Wrap_CallStaticObjectMethodV; table->CallStaticBooleanMethod = Wrap_CallStaticBooleanMethod; table->CallStaticBooleanMethodV = Wrap_CallStaticBooleanMethodV; table->CallStaticIntMethod = Wrap_CallStaticIntMethod; table->CallStaticIntMethodV = Wrap_CallStaticIntMethodV; table->CallStaticLongMethod = Wrap_CallStaticLongMethod; table->CallStaticLongMethodV = Wrap_CallStaticLongMethodV; table->CallStaticFloatMethod = Wrap_CallStaticFloatMethod; table->CallStaticFloatMethodV = Wrap_CallStaticFloatMethodV; table->CallStaticDoubleMethod = Wrap_CallStaticDoubleMethod; table->CallStaticDoubleMethodV = Wrap_CallStaticDoubleMethodV; table->GetObjectField = Wrap_GetObjectField; table->GetBooleanField = Wrap_GetBooleanField; table->GetIntField = Wrap_GetIntField; table->GetLongField = Wrap_GetLongField; table->GetFloatField = Wrap_GetFloatField; table->GetDoubleField = Wrap_GetDoubleField; table->SetObjectField = Wrap_SetObjectField; table->SetBooleanField = Wrap_SetBooleanField; table->SetIntField = Wrap_SetIntField; table->SetLongField = Wrap_SetLongField; table->SetFloatField = Wrap_SetFloatField; table->SetDoubleField = Wrap_SetDoubleField; table->GetStaticObjectField = Wrap_GetStaticObjectField; table->GetStaticBooleanField = Wrap_GetStaticBooleanField; table->GetStaticIntField = Wrap_GetStaticIntField; table->SetStaticObjectField = Wrap_SetStaticObjectField; table->SetStaticBooleanField = Wrap_SetStaticBooleanField; table->SetStaticIntField = Wrap_SetStaticIntField; mprotect((void*)base, span, PROT_READ); // restore - real ART keeps this read-only TraceLog("InstallJniTraceHooks: installed (full call/field/exception/lifecycle set, file+logcat)"); }