Stable checkpoint: game reaches playable 3D gameplay on ARM64
Verified live on a Pixel 6a: the game passes its EULA, loads the prologue and renders real 3D gameplay, with zero heap exhaustion, zero faults and zero rejected frees over a full session. Root causes fixed in this state, each backed by a measurement (details and the list of refuted theories live in ARM64_TRANSLATION_LAYER.md): * JNI varargs float->double promotion. C promotes float to double in any varargs call and every Call*Method form is varargs, so reading one 4-byte slot yielded the double's always-zero low half. EVERY float argument passed to Java was silently becoming 0; text was just where it showed. * GuestHeap ~4x memory overhead. Power-of-two size classes carving the full class, plus segregated free lists that could never share memory between sizes. Reworked to exact sizing with O(log n) best-fit reuse and splitting (deliberately not a linear scan - this allocator already had an O(n) perf cliff in its history). Peak live now 207MB against the real A9's 199MB, fragmentation ~2.5MB. Also fixed: realloc reading past the old block on shrink, a 32-bit overflow in calloc, and drifting payload alignment. * Unbounded FMOD fake-handle leak into the never-freeing permanent arena, which is why enlarging that arena had not helped. * Frame presentation, corrected against A9 ground truth: the real frame has three default-framebuffer colour clears and ONE present at the end; this engine had been presenting on each of them. Load-time acceleration (zlib_accel.cpp): host zlib now serves inflate and crc32, the latter measured by the block profiler as the single hottest guest routine at 17.7%. Streams are only taken over when this layer saw their own inflateInit2_, so unknown streams (libpng's, among others) still run the original emulated path. name_lookup_accel.cpp is present but its hook is NOT registered - it crashed on bad assumptions about guest table lifetime and is kept as a starting point, with both mistakes recorded in its comments. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
# Standalone armeabi-v7a tracing agent for the native ARM32 investigation
|
||||
# (see ARM64_TRANSLATION_LAYER.md's "Native ARM32 tracing harness" plan).
|
||||
# Deliberately NOT wired into the main Gradle build (settings.gradle.kts) -
|
||||
# this is a diagnostic-only artifact built and pushed to the device
|
||||
# independently via build.sh, never bundled into any APK. Runs exclusively
|
||||
# on the "native32" flavor's real, unmodified libapp.so on the Galaxy A9.
|
||||
cmake_minimum_required(VERSION 3.22.1)
|
||||
project(trace_agent)
|
||||
|
||||
add_library(trace_agent SHARED
|
||||
libc_gles_trace.cpp
|
||||
jni_trace.cpp
|
||||
)
|
||||
|
||||
# Deliberately NOT linking against libGLESv2.so here, even though the GLES
|
||||
# wrappers exist below - confirmed live on-device that an eager DT_NEEDED
|
||||
# dependency on it crashes the WHOLE process immediately on launch (exit
|
||||
# code 1, right after Zygote's early "-Xcheck:jni" specialization, well
|
||||
# before any Activity/EGL/GL context exists to make libGLESv2.so resolvable
|
||||
# that early). Each GLES wrapper already resolves its real implementation
|
||||
# lazily via dlsym(RTLD_NEXT, ...) on first real call - by then the app's
|
||||
# own GL usage has long since loaded libGLESv2.so itself, so no explicit
|
||||
# link-time dependency is needed at all. <GLES2/gl2.h> is included only for
|
||||
# its type definitions (GLenum/GLuint/...), not any function linkage.
|
||||
find_library(log-lib log)
|
||||
find_library(dl-lib dl)
|
||||
|
||||
target_link_libraries(trace_agent
|
||||
${log-lib}
|
||||
${dl-lib}
|
||||
)
|
||||
|
||||
target_compile_options(trace_agent PRIVATE -Wall -Wno-unused-parameter)
|
||||
Executable
+26
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env bash
|
||||
# Builds libtrace_agent.so for armeabi-v7a via the NDK's CMake toolchain
|
||||
# file - standalone, deliberately outside the Gradle build (see
|
||||
# CMakeLists.txt's own comment). Output: build/libtrace_agent.so, ready to
|
||||
# `adb push` to the Galaxy A9 (see the plan's Phase 4).
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
NDK="${ANDROID_NDK_HOME:-/home/megboyzz/Android/Sdk/ndk/27.0.12077973}"
|
||||
TOOLCHAIN="$NDK/build/cmake/android.toolchain.cmake"
|
||||
|
||||
if [ ! -f "$TOOLCHAIN" ]; then
|
||||
echo "NDK toolchain file not found at $TOOLCHAIN - set ANDROID_NDK_HOME" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cmake -B build -G Ninja \
|
||||
-DCMAKE_TOOLCHAIN_FILE="$TOOLCHAIN" \
|
||||
-DANDROID_ABI=armeabi-v7a \
|
||||
-DANDROID_PLATFORM=android-27 \
|
||||
-DCMAKE_BUILD_TYPE=Debug \
|
||||
.
|
||||
|
||||
cmake --build build
|
||||
|
||||
echo "Built: $(pwd)/build/libtrace_agent.so"
|
||||
@@ -0,0 +1,501 @@
|
||||
// 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 <jni.h>
|
||||
#include <cstring>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
#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<JNINativeInterface*>(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)");
|
||||
}
|
||||
@@ -0,0 +1,567 @@
|
||||
// LD_PRELOAD interposition for libc file I/O (including whatever the game
|
||||
// does to read its .obb data) and GLESv2 draw calls, on the REAL,
|
||||
// unmodified armeabi-v7a libapp.so running natively on the Galaxy A9 (see
|
||||
// ARM64_TRANSLATION_LAYER.md's "Native ARM32 tracing harness" plan). This
|
||||
// is the ground-truth counterpart to this session's own gles_shim.cpp
|
||||
// counters and libc_shims.cpp - deployed via Android's per-app
|
||||
// `wrap.<packageName>` debuggable-app mechanism (no root needed), so every
|
||||
// dynamically-resolved libc/GLESv2 symbol the real game calls resolves to
|
||||
// these wrappers first (standard dlsym(RTLD_NEXT, ...) interposition), each
|
||||
// logging then calling straight through to the real implementation -
|
||||
// observation only, never changes behavior.
|
||||
#include <dlfcn.h>
|
||||
#include <fcntl.h>
|
||||
#include <atomic>
|
||||
#include <cstdarg>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
#include <dirent.h>
|
||||
#include <link.h>
|
||||
#include <pthread.h>
|
||||
#include <GLES2/gl2.h>
|
||||
#include <EGL/egl.h>
|
||||
#include "trace_log.h"
|
||||
|
||||
using trace_agent::TraceLog;
|
||||
#define LOGI TraceLog
|
||||
|
||||
namespace {
|
||||
|
||||
template <typename Fn>
|
||||
Fn RealSym(const char* name) {
|
||||
void* p = dlsym(RTLD_NEXT, name);
|
||||
return reinterpret_cast<Fn>(p);
|
||||
}
|
||||
|
||||
// Declared here (used by open() below) rather than down near pthread_once's
|
||||
// own definition - see pthread_once's comment for why this flag exists.
|
||||
// Plain namespace-scope atomic (constant-initialized), NOT a function-local
|
||||
// static - see pthread_once's own comment for why lazy function-local
|
||||
// statics are unsafe to touch from inside this specific interposer.
|
||||
std::atomic<bool> g_pastBootstrap{false};
|
||||
std::atomic<bool>& PastBootstrap() { return g_pastBootstrap; }
|
||||
|
||||
// Ground-truth counterpart (2026-09-16, ARM64_TRANSLATION_LAYER.md - the
|
||||
// dword_AE00D8 reentrancy investigation) to the emulated engine's own
|
||||
// SnapshotAE00D8Window (guest_engine.h's G2H-based version). Since this
|
||||
// agent runs INSIDE the real process via LD_PRELOAD, it shares the exact
|
||||
// same address space as the real armeabi-v7a libapp.so - no guest/host
|
||||
// translation needed, just the real ASLR load bias for this one shared
|
||||
// object, resolved once via dl_iterate_phdr and cached.
|
||||
uintptr_t LibappBase() {
|
||||
// A POD `static` with a literal 0 initializer gets plain constant
|
||||
// (zero) initialization, not a dynamic/guarded one - safe from the
|
||||
// lazy-function-local-static hazard pthread_once's own comment
|
||||
// documents (no emutls/pthread_once guard is generated for this kind
|
||||
// of trivial static at all). Deliberately NOT cached on failure though:
|
||||
// the first few pthread_once() calls observed live fire before
|
||||
// libapp.so itself has even been dlopen'd (its own early static-init
|
||||
// routines, other libraries' locale setup, etc.), so caching a "not
|
||||
// found" result after the very first attempt would permanently blind
|
||||
// this to libapp.so once it DOES load moments later - confirmed live
|
||||
// (every pthread_once call this session logged "base unresolved" even
|
||||
// well after real gameplay/glClear activity had started). Only cache a
|
||||
// SUCCESSFUL resolution.
|
||||
static uintptr_t base = 0;
|
||||
if (base) return base;
|
||||
dl_iterate_phdr(
|
||||
[](struct dl_phdr_info* info, size_t, void* data) -> int {
|
||||
if (info->dlpi_name && strstr(info->dlpi_name, "libapp.so")) {
|
||||
*static_cast<uintptr_t*>(data) = info->dlpi_addr;
|
||||
return 1; // stop iterating
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
&base);
|
||||
return base;
|
||||
}
|
||||
|
||||
// Same window as the emulated engine's probe: [0xae00d8, 0xae0108) -
|
||||
// dword_AE00D8/dword_AE00DC/dword_AE00E0/dword_AE00FC, the small cluster of
|
||||
// globals sub_56962C's crash-site arithmetic reads directly by fixed
|
||||
// offset. libapp.so's own file vaddrs equal these offsets 1:1 (see
|
||||
// guest_engine.cpp's LoadImage - no rebasing in the emulated engine either),
|
||||
// so `LibappBase() + 0xae00d8` is the real, live runtime address.
|
||||
constexpr uintptr_t kAE00D8Offset = 0xae00d8;
|
||||
constexpr size_t kAE00D8WindowLen = 0x30;
|
||||
std::string SnapshotAE00D8Window() {
|
||||
uintptr_t base = LibappBase();
|
||||
if (!base) return std::string();
|
||||
return std::string(reinterpret_cast<char*>(base + kAE00D8Offset), kAE00D8WindowLen);
|
||||
}
|
||||
void LogAE00D8WindowDiff(const std::string& before, const std::string& after) {
|
||||
if (before.empty() || after.empty()) {
|
||||
LOGI("pthread_once: dword_AE00D8 window snapshot unavailable (libapp.so base not resolved)");
|
||||
return;
|
||||
}
|
||||
std::string diffs;
|
||||
for (size_t i = 0; i + 4 <= kAE00D8WindowLen; i += 4) {
|
||||
uint32_t b = 0, a = 0;
|
||||
memcpy(&b, before.data() + i, 4);
|
||||
memcpy(&a, after.data() + i, 4);
|
||||
if (b != a) {
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), " [AE00D8+0x%zx]:0x%x->0x%x", i, b, a);
|
||||
diffs += buf;
|
||||
}
|
||||
}
|
||||
LOGI("pthread_once: dword_AE00D8 window [0x%lx,0x%lx) diffs:%s",
|
||||
(unsigned long)kAE00D8Offset, (unsigned long)(kAE00D8Offset + kAE00D8WindowLen),
|
||||
diffs.empty() ? " (none)" : diffs.c_str());
|
||||
}
|
||||
|
||||
// Confirmed live on-device: logging EVERY single libc file call (including
|
||||
// the many thousands ART's own dex/oat/vdex loading does during normal app
|
||||
// startup, well before any game code runs) adds enough per-call overhead
|
||||
// to intermittently blow past ActivityManager's ~10s process-attach
|
||||
// timeout, getting the whole process killed before it ever reaches game
|
||||
// code - not a deadlock, just volume. Filtering to paths actually relevant
|
||||
// to this investigation (game data, not framework/system housekeeping)
|
||||
// keeps the signal without the ART-startup noise. A null path (rare) is
|
||||
// always logged, since that's unusual enough to be worth seeing regardless.
|
||||
bool IsInteresting(const char* path) {
|
||||
if (!path) return true;
|
||||
static const char* kKeywords[] = {
|
||||
"nfs13", "libapp", ".obb", ".sb", "Nimble", "fmod", "var/", "var1/",
|
||||
};
|
||||
for (const char* kw : kKeywords) {
|
||||
if (strstr(path, kw)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2026-09-18 (ARM64_TRANSLATION_LAYER.md task #39 - missing text overlay,
|
||||
// A9 ground-truth comparison): logcat alone lost the exact frames this
|
||||
// investigation needed - the game's own per-frame draw-call volume wraps
|
||||
// logcat's small ring buffer in well under a second, so by the time `adb
|
||||
// logcat -d` was dumped, the texture-upload/draw lines from the moment
|
||||
// that actually mattered were already gone (confirmed live: 95,000+ lines
|
||||
// captured, zero glTexImage2D lines survived). trace_log.h already writes
|
||||
// every line to an append-only file too (flushed per-line, exactly for
|
||||
// this reason) - the missing piece was simply that nothing ever called
|
||||
// InitFileLog(). Fixed here: resolve OUR OWN library's on-disk path via
|
||||
// dl_iterate_phdr (same technique LibappBase() uses for the real
|
||||
// libapp.so) and write the trace alongside it, in the app's own private
|
||||
// files dir (writable - that's literally where this .so was pushed to
|
||||
// run at all). Deliberately reuses the SAME "wait for the first
|
||||
// proven-safe hook" bootstrap gate as LibappBase()/pthread_once's own
|
||||
// diagnostics (see their extensive comments above) - dl_iterate_phdr this
|
||||
// early is the same documented re-entrant-linker-lock hazard regardless
|
||||
// of which library it's trying to locate.
|
||||
std::string SelfLibraryDir() {
|
||||
std::string dir;
|
||||
dl_iterate_phdr(
|
||||
[](struct dl_phdr_info* info, size_t, void* data) -> int {
|
||||
if (info->dlpi_name && strstr(info->dlpi_name, "libtrace_agent.so")) {
|
||||
std::string path(info->dlpi_name);
|
||||
size_t slash = path.find_last_of('/');
|
||||
*static_cast<std::string*>(data) = slash != std::string::npos
|
||||
? path.substr(0, slash) : std::string(".");
|
||||
return 1; // stop iterating
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
&dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
std::atomic<bool> g_fileLogInitAttempted{false};
|
||||
void InitFileLogOnceSafe() {
|
||||
bool expected = false;
|
||||
if (!g_fileLogInitAttempted.compare_exchange_strong(expected, true)) return;
|
||||
std::string dir = SelfLibraryDir();
|
||||
if (dir.empty()) {
|
||||
__android_log_print(ANDROID_LOG_INFO, "TRACE_AGENT",
|
||||
"InitFileLogOnceSafe: could not resolve own library path, "
|
||||
"file logging stays off this run (logcat-only)");
|
||||
return;
|
||||
}
|
||||
std::string path = dir + "/trace_output.log";
|
||||
trace_agent::InitFileLog(path.c_str());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
// ---- libc file I/O ----
|
||||
|
||||
int open(const char* path, int flags, ...) {
|
||||
static auto real = RealSym<int (*)(const char*, int, ...)>("open");
|
||||
mode_t mode = 0;
|
||||
if (flags & O_CREAT) {
|
||||
va_list ap;
|
||||
va_start(ap, flags);
|
||||
mode = (mode_t)va_arg(ap, int);
|
||||
va_end(ap);
|
||||
}
|
||||
int fd = real(path, flags, mode);
|
||||
// First proven-safe hook to fire each run (confirmed across prior
|
||||
// sessions) - marks bootstrap as over for pthread_once's own guard, see
|
||||
// its comment above for why that matters. Also the earliest safe point
|
||||
// to turn on file-based logging (see InitFileLogOnceSafe's own comment)
|
||||
// - logcat's ring buffer wraps almost immediately once real gameplay
|
||||
// starts, so the file is the only reliable record for anything beyond
|
||||
// the first fraction of a second.
|
||||
PastBootstrap().store(true, std::memory_order_relaxed);
|
||||
InitFileLogOnceSafe();
|
||||
if (IsInteresting(path)) {
|
||||
LOGI("open(\"%s\", flags=0x%x) -> fd=%d%s", path ? path : "(null)", flags, fd,
|
||||
fd < 0 ? " [FAILED]" : "");
|
||||
}
|
||||
return fd;
|
||||
}
|
||||
|
||||
int openat(int dirfd, const char* path, int flags, ...) {
|
||||
static auto real = RealSym<int (*)(int, const char*, int, ...)>("openat");
|
||||
mode_t mode = 0;
|
||||
if (flags & O_CREAT) {
|
||||
va_list ap;
|
||||
va_start(ap, flags);
|
||||
mode = (mode_t)va_arg(ap, int);
|
||||
va_end(ap);
|
||||
}
|
||||
int fd = real(dirfd, path, flags, mode);
|
||||
if (IsInteresting(path)) {
|
||||
LOGI("openat(%d, \"%s\", flags=0x%x) -> fd=%d%s", dirfd, path ? path : "(null)", flags,
|
||||
fd, fd < 0 ? " [FAILED]" : "");
|
||||
}
|
||||
return fd;
|
||||
}
|
||||
|
||||
FILE* fopen(const char* path, const char* mode) {
|
||||
static auto real = RealSym<FILE* (*)(const char*, const char*)>("fopen");
|
||||
FILE* f = real(path, mode);
|
||||
if (IsInteresting(path)) {
|
||||
LOGI("fopen(\"%s\", \"%s\") -> %p%s", path ? path : "(null)", mode ? mode : "(null)",
|
||||
(void*)f, f == nullptr ? " [FAILED]" : "");
|
||||
}
|
||||
return f;
|
||||
}
|
||||
|
||||
// Deliberately NOT interposing read() - by far the single highest-frequency
|
||||
// libc call during ART's own startup (every byte-buffer read of every dex/
|
||||
// oat/vdex file goes through it), and it doesn't carry a path - open/openat/
|
||||
// fopen already show WHICH file was touched, which is what this
|
||||
// investigation actually needs; logging read() too would reintroduce the
|
||||
// exact volume problem IsInteresting() above was added to avoid, for very
|
||||
// little extra information.
|
||||
|
||||
int stat(const char* path, struct stat* buf) {
|
||||
static auto real = RealSym<int (*)(const char*, struct stat*)>("stat");
|
||||
int r = real(path, buf);
|
||||
if (IsInteresting(path)) {
|
||||
LOGI("stat(\"%s\") -> %d%s", path ? path : "(null)", r, r != 0 ? " [FAILED]" : "");
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
int access(const char* path, int mode) {
|
||||
static auto real = RealSym<int (*)(const char*, int)>("access");
|
||||
int r = real(path, mode);
|
||||
if (IsInteresting(path)) {
|
||||
LOGI("access(\"%s\", 0x%x) -> %d%s", path ? path : "(null)", mode, r,
|
||||
r != 0 ? " [FAILED]" : "");
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
// opendir - added specifically to check whether the real game also polls
|
||||
// the (on the emulated engine, confirmed empty) external-files directory
|
||||
// the same way the emulated engine's own guest thread was found looping
|
||||
// on (ARM64_TRANSLATION_LAYER.md's file-I/O-path investigation) - "nfs13"
|
||||
// is already a substring of the package's own external-files path
|
||||
// (.../Android/data/com.ea.games.nfs13_arm/files), so IsInteresting()
|
||||
// already covers it once this call is actually interposed.
|
||||
DIR* opendir(const char* path) {
|
||||
static auto real = RealSym<DIR* (*)(const char*)>("opendir");
|
||||
DIR* d = real(path);
|
||||
if (IsInteresting(path)) {
|
||||
LOGI("opendir(\"%s\") -> %p%s", path ? path : "(null)", (void*)d,
|
||||
d == nullptr ? " [FAILED]" : "");
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
// Added 2026-09-16 (ARM64_TRANSLATION_LAYER.md - dword_AE00D8 reentrancy
|
||||
// investigation): ground-truth counterpart to Shim_pthread_once's own
|
||||
// register/memory-diff probes in the emulated engine, which found that
|
||||
// EVERY reentrant CallGuestFunction call it could find (including into the
|
||||
// real sub_87B968/sub_88CCD0 init routines this exact libc call reaches on
|
||||
// the emulated side) leaves both the caller's registers and this same
|
||||
// dword_AE00D8 memory window completely unchanged - a clean result that
|
||||
// doesn't explain the crash. This answers a different but related
|
||||
// question: does REAL, unmodified pthread_once() on REAL hardware ever
|
||||
// touch this memory window at all, for comparison against the emulated
|
||||
// engine's own (also clean) result.
|
||||
//
|
||||
// CONFIRMED LIVE this session: an earlier version of this wrapper that
|
||||
// unconditionally called LibappBase() (dl_iterate_phdr) on every
|
||||
// pthread_once() call crashed the WHOLE process with SIGSEGV inside
|
||||
// libtrace_agent.so itself, before Application.attachBaseContext even ran
|
||||
// (tombstone showed the fault inside our own .so, called from deep within
|
||||
// app_process32's own bootstrap). pthread_once() is called extremely early
|
||||
// by the dynamic linker/bionic itself (library static-initializer guards,
|
||||
// TLS setup) - calling dl_iterate_phdr from inside one of those calls means
|
||||
// re-entering the dynamic linker's own internal module-list lock while it
|
||||
// may already be held on the SAME thread by an ancestor frame (the linker
|
||||
// loading a library and running ITS constructors) - the exact "chicken and
|
||||
// egg" bootstrap hazard this file's mmap()-avoidance comment (below)
|
||||
// already documents for a different symbol. Fix: skip ALL of this
|
||||
// diagnostic's work (dl_iterate_phdr, string snapshots) until the FIRST
|
||||
// already-proven-safe hook (open(), which prior sessions confirmed fires
|
||||
// without incident) has fired at least once - by then bootstrap is long
|
||||
// over and dl_iterate_phdr is safe. Before that point, pthread_once() is
|
||||
// passed straight through with no diagnostics at all - purely linker/libc
|
||||
// bookkeeping this investigation was never after in the first place.
|
||||
//
|
||||
// SECOND, DEEPER hazard found once wrap.sh (real process-wide LD_PRELOAD,
|
||||
// active from exec() itself) replaced the earlier System.load()-based
|
||||
// partial activation: a real tombstone (llvm-addr2line confirmed) showed
|
||||
// THIS function's own `static auto real = RealSym<...>(...)` line crashing
|
||||
// via __emutls_get_address - this toolchain/ABI implements function-local
|
||||
// static thread-safe initialization using emulated TLS, which internally
|
||||
// calls pthread_once() ITSELF to lazily set up the TLS key. Since we're
|
||||
// LD_PRELOAD'd, that internal call gets intercepted by THIS SAME wrapper -
|
||||
// which then tries to initialize ITS OWN `real` static again -> infinite
|
||||
// recursion -> stack overflow -> SIGSEGV, on literally the first call, no
|
||||
// PastBootstrap() check even reached. Fixed by resolving `real` eagerly, at
|
||||
// LIBRARY LOAD time via a real ELF constructor (single-threaded by
|
||||
// construction, no lazy-static guard machinery involved at all) instead of
|
||||
// a lazy function-local static - sidesteps the whole class of hazard.
|
||||
using PthreadOnceFn = int (*)(pthread_once_t*, void (*)(void));
|
||||
PthreadOnceFn g_realPthreadOnce = nullptr;
|
||||
__attribute__((constructor)) void ResolveRealPthreadOnceEarly() {
|
||||
g_realPthreadOnce = RealSym<PthreadOnceFn>("pthread_once");
|
||||
}
|
||||
// THIRD hazard, one level deeper still: even with `real` resolved eagerly,
|
||||
// the diagnostic body's own LOGI(...) call touches TraceLog()'s
|
||||
// thread_local InTraceLog() reentrancy guard (trace_log.h) - a SEPARATE
|
||||
// thread_local variable that needs its OWN one-time emutls/pthread_once
|
||||
// setup, the first time ANY thread ever reaches it. That nested setup
|
||||
// calls back into THIS SAME pthread_once wrapper, which (bootstrap flag
|
||||
// already true by then) takes the logging branch again, touching
|
||||
// InTraceLog() again, recursing without end - a live tombstone confirmed
|
||||
// this exact chain (pthread_once -> TraceLog -> __emutls_get_address).
|
||||
// Fixed with a reentrancy guard that cannot itself need emutls: a plain
|
||||
// namespace-scope std::atomic<int>, constant-initialized (no runtime lazy-
|
||||
// static guard at all, unlike a thread_local or a guarded function-local
|
||||
// static) - if pthread_once is already executing on ANY thread when
|
||||
// re-entered, skip every bit of the diagnostic (dl_iterate_phdr AND
|
||||
// logging alike) and just pass straight through to the real
|
||||
// implementation. Slightly imprecise across threads (a global, not
|
||||
// per-thread, counter) but that's an acceptable tradeoff for a
|
||||
// diagnostic-only tool, and avoids needing anything TLS-shaped at all.
|
||||
std::atomic<int> g_pthreadOnceDepth{0};
|
||||
int pthread_once(pthread_once_t* onceCtl, void (*initRoutine)(void)) {
|
||||
PthreadOnceFn real = g_realPthreadOnce;
|
||||
if (!real) {
|
||||
// Constructor hasn't run yet (shouldn't happen once loaded, but a
|
||||
// defensive fallback beats calling through a null pointer) - resolve
|
||||
// directly via RTLD_NEXT this one time. Still safer than before:
|
||||
// this is a plain function call, not a lazy-static-guarded one.
|
||||
real = RealSym<PthreadOnceFn>("pthread_once");
|
||||
}
|
||||
int depthAtEntry = g_pthreadOnceDepth.fetch_add(1, std::memory_order_relaxed);
|
||||
struct DepthGuard {
|
||||
~DepthGuard() { g_pthreadOnceDepth.fetch_sub(1, std::memory_order_relaxed); }
|
||||
} depthGuard;
|
||||
if (depthAtEntry > 0 || !PastBootstrap().load(std::memory_order_relaxed)) {
|
||||
return real(onceCtl, initRoutine);
|
||||
}
|
||||
std::string before = SnapshotAE00D8Window();
|
||||
uintptr_t base = LibappBase();
|
||||
if (base && (uintptr_t)initRoutine >= base) {
|
||||
LOGI("pthread_once: initRoutine=%p (libapp.so+0x%lx)", (void*)initRoutine,
|
||||
(unsigned long)((uintptr_t)initRoutine - base));
|
||||
} else {
|
||||
LOGI("pthread_once: initRoutine=%p (not in libapp.so, or base unresolved)",
|
||||
(void*)initRoutine);
|
||||
}
|
||||
int r = real(onceCtl, initRoutine);
|
||||
std::string after = SnapshotAE00D8Window();
|
||||
LogAE00D8WindowDiff(before, after);
|
||||
return r;
|
||||
}
|
||||
|
||||
// Deliberately NOT interposing mmap() - confirmed live on-device that it
|
||||
// hangs the whole app process indefinitely (ActivityManager kills it after
|
||||
// a start-timeout, zero TRACE_AGENT log output the entire time - the hang
|
||||
// happens before our first successful call). The dynamic linker uses
|
||||
// mmap() itself to map every shared library into memory, including ours,
|
||||
// mid-load; interposing it risks exactly this kind of bootstrap
|
||||
// chicken-and-egg deadlock (our wrapper's own dlsym(RTLD_NEXT, "mmap")
|
||||
// requires the linker to already be in a stable state that a call arriving
|
||||
// mid-bootstrap may not have reached yet). Not needed for this
|
||||
// investigation's goal anyway - open/fopen/stat/access already show which
|
||||
// files get touched.
|
||||
|
||||
// ---- GLESv2 (same 4 entry points this session's own gles_shim.cpp counts,
|
||||
// for direct comparison against the emulated engine's counters) ----
|
||||
|
||||
// 2026-09-19: frame-structure ground truth (see glBindFramebuffer below).
|
||||
// The emulated engine has no eglSwapBuffers of its own to key off -
|
||||
// nativeOnResume never returns, so Android's automatic post-onDrawFrame swap
|
||||
// never fires (gles_shim.cpp's Shim_glClear) - and it synthesizes one per
|
||||
// default-framebuffer colour clear instead. That is a GUESS about how the
|
||||
// game structures a frame. Spelling out the mask bits and the bound
|
||||
// framebuffer here, against the already-traced eglSwapBuffers below, gives
|
||||
// the real ratio and ordering the emulated engine should be reproducing.
|
||||
GLuint g_tracedBoundFramebuffer = 0;
|
||||
|
||||
void glClear(GLbitfield mask) {
|
||||
static auto real = RealSym<void (*)(GLbitfield)>("glClear");
|
||||
LOGI("glClear(mask=0x%x%s%s%s) fb=%u", mask,
|
||||
(mask & GL_COLOR_BUFFER_BIT) ? " COLOR" : "",
|
||||
(mask & GL_DEPTH_BUFFER_BIT) ? " DEPTH" : "",
|
||||
(mask & GL_STENCIL_BUFFER_BIT) ? " STENCIL" : "",
|
||||
g_tracedBoundFramebuffer);
|
||||
real(mask);
|
||||
}
|
||||
|
||||
void glDrawArrays(GLenum mode, GLint first, GLsizei count) {
|
||||
static auto real = RealSym<void (*)(GLenum, GLint, GLsizei)>("glDrawArrays");
|
||||
LOGI("glDrawArrays(mode=0x%x, first=%d, count=%d)", mode, first, count);
|
||||
real(mode, first, count);
|
||||
}
|
||||
|
||||
// 2026-09-18 (ARM64_TRANSLATION_LAYER.md task #39 - missing text overlay):
|
||||
// ground-truth counterpart to gles_shim.cpp's own Shim_glTexImage2D/
|
||||
// Shim_glDrawElements instrumentation, which found a narrow 738x302
|
||||
// texture (real anti-aliased glyph data, matching the missing safety-
|
||||
// disclaimer/copyright text) uploads correctly on the emulated engine but
|
||||
// is NEVER referenced by any subsequent draw call. Question this answers:
|
||||
// on REAL hardware, does the equivalent texture get uploaded with similar
|
||||
// dimensions, and - critically - does a real draw call actually reference
|
||||
// it (proving the real game DOES intend to draw it, ruling out "the text
|
||||
// is legitimately decorative-only and never drawn as geometry").
|
||||
GLuint g_currentBoundTexture2D = 0;
|
||||
void glBindTexture(GLenum target, GLuint texture) {
|
||||
static auto real = RealSym<void (*)(GLenum, GLuint)>("glBindTexture");
|
||||
if (target == GL_TEXTURE_2D) g_currentBoundTexture2D = texture;
|
||||
real(target, texture);
|
||||
}
|
||||
|
||||
void glTexImage2D(GLenum target, GLint level, GLint internalformat, GLsizei width,
|
||||
GLsizei height, GLint border, GLenum format, GLenum type,
|
||||
const void* pixels) {
|
||||
static auto real = RealSym<void (*)(GLenum, GLint, GLint, GLsizei, GLsizei, GLint, GLenum,
|
||||
GLenum, const void*)>("glTexImage2D");
|
||||
LOGI("glTexImage2D tex=%u target=0x%x level=%d internalformat=0x%x %dx%d format=0x%x type=0x%x",
|
||||
g_currentBoundTexture2D, target, level, internalformat, width, height, format, type);
|
||||
real(target, level, internalformat, width, height, border, format, type, pixels);
|
||||
}
|
||||
|
||||
void glDrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) {
|
||||
static auto real =
|
||||
RealSym<void (*)(GLenum, GLsizei, GLenum, const void*)>("glDrawElements");
|
||||
LOGI("glDrawElements(mode=0x%x, count=%d, type=0x%x) tex0=%u", mode, count, type,
|
||||
g_currentBoundTexture2D);
|
||||
real(mode, count, type, indices);
|
||||
}
|
||||
|
||||
void glUseProgram(GLuint program) {
|
||||
static auto real = RealSym<void (*)(GLuint)>("glUseProgram");
|
||||
LOGI("glUseProgram(program=%u)", program);
|
||||
real(program);
|
||||
}
|
||||
|
||||
// 2026-09-19: frame-structure ground truth. The emulated engine has no
|
||||
// eglSwapBuffers of its own to key off (nativeOnResume never returns, so
|
||||
// Android's automatic post-onDrawFrame swap never fires - see gles_shim.cpp's
|
||||
// Shim_glClear), and synthesizes one per default-framebuffer colour clear
|
||||
// instead. That is a GUESS about how the game structures a frame. These three
|
||||
// interposers measure the real thing on hardware: which framebuffer is bound
|
||||
// at each clear, what the clear mask actually is, and exactly where the real
|
||||
// swaps fall relative to those clears. The ratio and ordering are what the
|
||||
// emulated engine needs to reproduce - the swap CALL itself can't be copied,
|
||||
// since on real hardware it is Android's framework making it, not the game.
|
||||
void glBindFramebuffer(GLenum target, GLuint framebuffer) {
|
||||
static auto real = RealSym<void (*)(GLenum, GLuint)>("glBindFramebuffer");
|
||||
g_tracedBoundFramebuffer = framebuffer;
|
||||
real(target, framebuffer);
|
||||
}
|
||||
|
||||
// 2026-09-18 (ARM64_TRANSLATION_LAYER.md - the "frozen splash, real
|
||||
// rendering never reaches the screen" chase): the emulated engine's own
|
||||
// logs show 0 calls to eglSwapBuffers/eglGetProcAddress across every
|
||||
// capture, and IDA static analysis of the exact same real armeabi-v7a
|
||||
// libapp.so found the IsisApp render-pipeline's own vtable+168/+116 slots
|
||||
// to be empty no-ops. Ground-truth check: does the REAL app, running
|
||||
// natively on the Galaxy A9, actually call eglSwapBuffers at all, and if
|
||||
// so how often relative to the GLES draw calls above?
|
||||
EGLBoolean eglSwapBuffers(EGLDisplay dpy, EGLSurface surface) {
|
||||
static auto real = RealSym<EGLBoolean (*)(EGLDisplay, EGLSurface)>("eglSwapBuffers");
|
||||
EGLBoolean result = real(dpy, surface);
|
||||
LOGI("eglSwapBuffers(dpy=%p, surface=%p) -> %d", dpy, surface, (int)result);
|
||||
return result;
|
||||
}
|
||||
|
||||
__eglMustCastToProperFunctionPointerType eglGetProcAddress(const char* procname) {
|
||||
static auto real =
|
||||
RealSym<__eglMustCastToProperFunctionPointerType (*)(const char*)>("eglGetProcAddress");
|
||||
__eglMustCastToProperFunctionPointerType result = real(procname);
|
||||
LOGI("eglGetProcAddress(\"%s\") -> %p", procname ? procname : "(null)", (void*)result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Added 2026-09-16 - ground-truth counterpart to this session's own
|
||||
// gles_shim.cpp investigation (ARM64_TRANSLATION_LAYER.md): our emulated
|
||||
// engine's shader-variant cache (sub_43FDE0/dword_ADBFB8) consistently
|
||||
// fails, producing an EMPTY shader source that the real host GLSL compiler
|
||||
// rejects with "Missing main() function". Question this trace answers: on
|
||||
// REAL native ARM32 execution (no CPU translation at all), does the same
|
||||
// shader-variant lookup ever produce an empty source too (and get silently
|
||||
// recovered from), or does it never happen at all? Logs the real source
|
||||
// text/length and real GL_COMPILE_STATUS/GL_LINK_STATUS, same as
|
||||
// Shim_glShaderSource/Shim_glCompileShader/Shim_glLinkProgram do in our own
|
||||
// gles_shim.cpp, for direct comparison.
|
||||
void glShaderSource(GLuint shader, GLsizei count, const GLchar* const* string, const GLint* length) {
|
||||
static auto real =
|
||||
RealSym<void (*)(GLuint, GLsizei, const GLchar* const*, const GLint*)>("glShaderSource");
|
||||
for (GLsizei i = 0; i < count; i++) {
|
||||
GLint len = length ? length[i] : (string[i] ? (GLint)strlen(string[i]) : 0);
|
||||
LOGI("glShaderSource(%u) fragment[%d/%d] len=%d lengths_provided=%d: \"%.300s\"",
|
||||
shader, i, count, len, length != nullptr, string[i] ? string[i] : "(null)");
|
||||
}
|
||||
real(shader, count, string, length);
|
||||
}
|
||||
|
||||
void glCompileShader(GLuint shader) {
|
||||
static auto real = RealSym<void (*)(GLuint)>("glCompileShader");
|
||||
real(shader);
|
||||
static auto realGetShaderiv = RealSym<void (*)(GLuint, GLenum, GLint*)>("glGetShaderiv");
|
||||
static auto realGetShaderInfoLog =
|
||||
RealSym<void (*)(GLuint, GLsizei, GLsizei*, GLchar*)>("glGetShaderInfoLog");
|
||||
GLint status = GL_FALSE;
|
||||
realGetShaderiv(shader, GL_COMPILE_STATUS, &status);
|
||||
LOGI("glCompileShader(%u) status=%s", shader, status == GL_TRUE ? "OK" : "FAILED");
|
||||
if (status != GL_TRUE) {
|
||||
char log[512] = {0};
|
||||
realGetShaderInfoLog(shader, sizeof(log), nullptr, log);
|
||||
LOGI("glCompileShader(%u) FAILED: %s", shader, log);
|
||||
}
|
||||
}
|
||||
|
||||
void glLinkProgram(GLuint program) {
|
||||
static auto real = RealSym<void (*)(GLuint)>("glLinkProgram");
|
||||
real(program);
|
||||
static auto realGetProgramiv = RealSym<void (*)(GLuint, GLenum, GLint*)>("glGetProgramiv");
|
||||
static auto realGetProgramInfoLog =
|
||||
RealSym<void (*)(GLuint, GLsizei, GLsizei*, GLchar*)>("glGetProgramInfoLog");
|
||||
GLint status = GL_FALSE;
|
||||
realGetProgramiv(program, GL_LINK_STATUS, &status);
|
||||
LOGI("glLinkProgram(%u) status=%s", program, status == GL_TRUE ? "OK" : "FAILED");
|
||||
if (status != GL_TRUE) {
|
||||
char log[512] = {0};
|
||||
realGetProgramInfoLog(program, sizeof(log), nullptr, log);
|
||||
LOGI("glLinkProgram(%u) FAILED: %s", program, log);
|
||||
}
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
@@ -0,0 +1,148 @@
|
||||
// Shared logging for trace_agent (see ARM64_TRANSLATION_LAYER.md's "Native
|
||||
// ARM32 tracing harness" section). Writes every trace line to BOTH logcat
|
||||
// (for live `adb logcat` monitoring) and an append-only file under the
|
||||
// app's own private files dir - logcat alone drops lines on long runs
|
||||
// (ring buffer wraps, and per-process rate limiting kicks in well before a
|
||||
// "full" trace's real call volume), so the file is the actual source of
|
||||
// truth for offline analysis.
|
||||
//
|
||||
// Also holds a jmethodID/jfieldID -> "Name sig" cache, populated by the
|
||||
// Get*MethodID/Get*FieldID wrappers in jni_trace.cpp, so the high-volume
|
||||
// Call*Method/Get*Field/Set*Field wrappers can log a real, readable name
|
||||
// instead of a bare pointer - the whole point of a "full" trace is being
|
||||
// able to read it after the fact without cross-referencing every ID by hand.
|
||||
#pragma once
|
||||
#include <android/log.h>
|
||||
#include <cstdio>
|
||||
#include <cstdarg>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace trace_agent {
|
||||
|
||||
inline FILE*& LogFile() {
|
||||
static FILE* f = nullptr;
|
||||
return f;
|
||||
}
|
||||
inline std::mutex& LogMutex() {
|
||||
static std::mutex m;
|
||||
return m;
|
||||
}
|
||||
|
||||
inline void InitFileLog(const char* path) {
|
||||
// fopen() called OUTSIDE the lock, deliberately: bionic's fopen()
|
||||
// internally calls the exported open() symbol, which - since this
|
||||
// library is LD_PRELOAD'd - gets re-intercepted by our own open()
|
||||
// wrapper in libc_gles_trace.cpp, which itself calls TraceLog(). If
|
||||
// LogMutex() were still held at that point, this second, same-thread
|
||||
// lock attempt on a non-reentrant std::mutex would deadlock. The
|
||||
// reentrancy guard in TraceLog() below is a second, independent layer
|
||||
// against the same class of self-interposition recursion.
|
||||
FILE* f = fopen(path, "a");
|
||||
std::lock_guard<std::mutex> lock(LogMutex());
|
||||
if (LogFile()) fclose(LogFile());
|
||||
LogFile() = f;
|
||||
if (LogFile()) {
|
||||
fprintf(LogFile(), "---- trace_agent file log opened: %s ----\n", path);
|
||||
fflush(LogFile());
|
||||
}
|
||||
__android_log_print(ANDROID_LOG_INFO, "TRACE_AGENT",
|
||||
"InitFileLog: %s -> %s", path, LogFile() ? "ok" : "FAILED (fopen)");
|
||||
}
|
||||
|
||||
inline long long NowMs() {
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (long long)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
|
||||
}
|
||||
|
||||
inline bool& InTraceLog() {
|
||||
static thread_local bool v = false;
|
||||
return v;
|
||||
}
|
||||
|
||||
// 2026-09-18 (ARM64_TRANSLATION_LAYER.md task #39, A9 ground-truth
|
||||
// comparison): this used to ALSO call __android_log_print() on every line,
|
||||
// same as the file write. Confirmed live that's actively harmful for a
|
||||
// "full" trace, not just redundant - the game's own per-frame draw-call
|
||||
// volume wraps logcat's small ring buffer in well under a second (a
|
||||
// 95,000+ line `adb logcat -d` dump had already lost every
|
||||
// glTexImage2D/glDrawElements line from the exact moment under
|
||||
// investigation) and Android additionally rate-limits/drops a single
|
||||
// process's log output past a threshold, silently. Neither of those
|
||||
// problems touch a plain flushed file write. File-only now - the file is
|
||||
// the one and only source of truth for anything beyond the first instant;
|
||||
// see InitFileLogOnceSafe (libc_gles_trace.cpp) for where the output path
|
||||
// comes from. `adb logcat` is still useful for confirming the agent loaded
|
||||
// at all (see InitFileLog's own one-time __android_log_print), just not
|
||||
// for the high-volume trace itself anymore.
|
||||
inline void TraceLog(const char* fmt, ...) {
|
||||
// Reentrancy guard: a handful of libc calls this agent itself makes
|
||||
// (fopen -> open, etc.) get re-intercepted by our own LD_PRELOAD
|
||||
// wrappers (self-interposition), which would otherwise call back into
|
||||
// TraceLog from inside TraceLog on the same thread. Drop the nested
|
||||
// call rather than log it (or, worse, deadlock on LogMutex() below) -
|
||||
// matches the project's general "never intercept indiscriminately"
|
||||
// principle: this agent's own bookkeeping calls aren't part of the
|
||||
// game's execution trace anyway.
|
||||
if (InTraceLog()) return;
|
||||
InTraceLog() = true;
|
||||
|
||||
char buf[1024];
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
vsnprintf(buf, sizeof(buf), fmt, ap);
|
||||
va_end(ap);
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(LogMutex());
|
||||
if (LogFile()) {
|
||||
// Flushed every line, not buffered - these investigations
|
||||
// routinely end in a crash or an ActivityManager kill, and a
|
||||
// buffered-but-unflushed tail is exactly the data most worth
|
||||
// having at that point.
|
||||
fprintf(LogFile(), "[%lld] [tid=%d] %s\n", NowMs(), gettid(), buf);
|
||||
fflush(LogFile());
|
||||
}
|
||||
}
|
||||
|
||||
InTraceLog() = false;
|
||||
}
|
||||
|
||||
inline std::unordered_map<const void*, std::string>& NameCache() {
|
||||
static std::unordered_map<const void*, std::string> m;
|
||||
return m;
|
||||
}
|
||||
inline std::mutex& NameCacheMutex() {
|
||||
static std::mutex m;
|
||||
return m;
|
||||
}
|
||||
|
||||
inline void RememberName(const void* id, const char* name, const char* sig) {
|
||||
if (!id) return;
|
||||
std::string full = name ? name : "?";
|
||||
if (sig) {
|
||||
full += " ";
|
||||
full += sig;
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(NameCacheMutex());
|
||||
NameCache()[id] = std::move(full);
|
||||
}
|
||||
|
||||
inline std::string LookupName(const void* id) {
|
||||
if (!id) return "(null)";
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(NameCacheMutex());
|
||||
auto it = NameCache().find(id);
|
||||
if (it != NameCache().end()) return it->second;
|
||||
}
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), "%p", id);
|
||||
return buf;
|
||||
}
|
||||
|
||||
} // namespace trace_agent
|
||||
Reference in New Issue
Block a user