/* ViewPanel —— 最小注入器 * * 依据(REA 对 Contents/MacOS/Spine 的实测): * · 该二进制导入 _dlopen/_dlsym/_dlerror,__la_symbol_ptr 里有 _dlsym 槽位, * 且没有直接链接 _JNI_CreateJavaVM —— JNI 入口是运行期 dlopen+dlsym 拿的, * 所以重绑定 _dlsym 是有效钩子点。 * · ad-hoc 签名、无 hardened runtime、无 entitlements —— DYLD_INSERT_LIBRARIES 可用。 * * 本文件只做三件事: * 1) 重绑定 _dlsym(含后加载的 libjvm) * 2) 拦 JNI_CreateJavaVM * 3) JVM 起来后,把我们的类 DefineClass 进编辑器自己的类加载器,再调用它的 main * * 这里没有任何 UI 代码:面板是纯 Java / Scene2D 实现的。 */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define VP_LOG "/tmp/spine-viewpanel.log" #define VP_DEFAULT_CLASSES "/tmp/spine-viewpanel/classes" #define VP_DEFAULT_MAIN "com/spineview/SceneScan" static void vp_log(const char *format, ...) { FILE *file = fopen(VP_LOG, "a"); if (!file) return; fprintf(file, "[pid %d] ", (int)getpid()); va_list arguments; va_start(arguments, format); vfprintf(file, format, arguments); va_end(arguments); fputc('\n', file); fclose(file); } typedef void *(*dlsym_fn)(void *, const char *); typedef jint (*create_vm_fn)(JavaVM **, void **, void *); static dlsym_fn real_dlsym = NULL; static create_vm_fn real_create_vm = NULL; static JavaVM *g_vm = NULL; static jobject g_editor_loader = NULL; /* 编辑器自己的 ClassLoader 实例 */ static jclass g_panel_class = NULL; /* com.spineview.SceneScan 的全局引用 */ static jmethodID g_on_render = NULL; /* SceneScan.onRenderThread() */ static char g_classes_dir[4096]; static char g_main_class[512]; static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER; typedef void (*swap_fn)(JNIEnv *, jclass, jlong); static swap_fn real_swap = NULL; static int g_swap_calls = 0; /* 编辑器用它自己的类加载器加载了这些保名的 libGDX 控件类(REA 运行时证据), * 拿它们的 Class 对象就能反推出编辑器类加载器。 */ static const char *kEditorAnchorClasses[] = { "com/badlogic/gdx/scenes/scene2d/ui/TextButton", "com/badlogic/gdx/scenes/scene2d/ui/Label", "com/badlogic/gdx/scenes/scene2d/ui/ImageTextButton", "com/badlogic/gdx/scenes/scene2d/ui/Window", "com/badlogic/jglfw/Glfw", }; /* ---------- 读取我们的 .class ---------- */ static jclass vp_define_file(JNIEnv *env, jobject loader, const char *path) { FILE *file = fopen(path, "rb"); if (!file) return NULL; fseek(file, 0, SEEK_END); long size = ftell(file); fseek(file, 0, SEEK_SET); if (size <= 0) { fclose(file); return NULL; } unsigned char *buffer = malloc((size_t)size); if (!buffer) { fclose(file); return NULL; } size_t read = fread(buffer, 1, (size_t)size, file); fclose(file); if (read != (size_t)size) { free(buffer); return NULL; } jclass defined = (*env)->DefineClass(env, NULL, loader, (const jbyte *)buffer, (jsize)size); free(buffer); if ((*env)->ExceptionCheck(env)) { vp_log("DefineClass 抛出异常: %s", path); (*env)->ExceptionDescribe(env); (*env)->ExceptionClear(env); return NULL; } return defined; } static jclass vp_define_directory(JNIEnv *env, jobject loader, const char *root, const char *relative, const char *main_internal) { jclass found = NULL; char path[4096]; if (relative[0]) snprintf(path, sizeof(path), "%s/%s", root, relative); else snprintf(path, sizeof(path), "%s", root); DIR *directory = opendir(path); if (!directory) return NULL; struct dirent *entry; while ((entry = readdir(directory)) != NULL) { if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue; char child_relative[4096]; if (relative[0]) snprintf(child_relative, sizeof(child_relative), "%s/%s", relative, entry->d_name); else snprintf(child_relative, sizeof(child_relative), "%s", entry->d_name); char child_path[4096]; snprintf(child_path, sizeof(child_path), "%s/%s", root, child_relative); struct stat info; if (stat(child_path, &info) != 0) continue; if (S_ISDIR(info.st_mode)) { jclass nested = vp_define_directory(env, loader, root, child_relative, main_internal); if (nested) found = nested; continue; } size_t length = strlen(child_relative); if (length <= 6 || strcmp(child_relative + length - 6, ".class") != 0) continue; char internal_name[4096]; snprintf(internal_name, sizeof(internal_name), "%s", child_relative); internal_name[length - 6] = '\0'; jclass defined = vp_define_file(env, loader, child_path); vp_log("define %s -> %s", internal_name, defined ? "ok" : "failed"); if (defined && main_internal && strcmp(internal_name, main_internal) == 0) found = defined; } closedir(directory); return found; } /* ---------- 在编辑器自己的类加载器里加载并执行我们的类 ---------- */ /* 把我们的类定义进调用方传入的加载器(也就是编辑器自己的那个),返回入口类。 * 目的是让我们的代码和编辑器处在同一个类命名空间里,能直接按名字找它的类。 */ static jclass VPC_defineIntoLoader(JNIEnv *env, jclass clazz, jobject loader) { (void)clazz; if (!env || !loader) return NULL; jclass defined = vp_define_directory(env, loader, g_classes_dir, "", g_main_class); vp_log("defineIntoLoader: %s", defined ? "已定义" : "失败"); return defined; } static void *vp_run_thread(void *unused) { (void)unused; /* 等编辑器把保名的控件类加载进来(启动后几百毫秒到几秒)。 */ jobject loader = NULL; for (int attempt = 0; attempt < 120 && !loader; attempt++) { pthread_mutex_lock(&g_lock); loader = g_editor_loader; pthread_mutex_unlock(&g_lock); if (!loader) usleep(200000); } JNIEnv *env = NULL; if (!g_vm || (*g_vm)->AttachCurrentThread(g_vm, (void **)&env, NULL) != JNI_OK || !env) { vp_log("run: AttachCurrentThread 失败"); return NULL; } if (!loader) { /* 有些进程不触发 dlsym("Java_..."),抓不到编辑器加载器。 * 退一步用系统类加载器:我们的类定义进去照样能跑,只是看不到编辑器的私有类。 */ jclass class_loader_class = (*env)->FindClass(env, "java/lang/ClassLoader"); jmethodID get_system = class_loader_class ? (*env)->GetStaticMethodID(env, class_loader_class, "getSystemClassLoader", "()Ljava/lang/ClassLoader;") : NULL; if (get_system) loader = (*env)->CallStaticObjectMethod(env, class_loader_class, get_system); if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionDescribe(env); (*env)->ExceptionClear(env); loader = NULL; } vp_log("run: 没抓到编辑器加载器,改用系统类加载器 -> %p", loader); } if (!loader) { vp_log("run: 拿不到任何类加载器,放弃"); return NULL; } { jclass object_class = (*env)->FindClass(env, "java/lang/Object"); jmethodID to_string = object_class ? (*env)->GetMethodID(env, object_class, "toString", "()Ljava/lang/String;") : NULL; jstring text = to_string ? (jstring)(*env)->CallObjectMethod(env, loader, to_string) : NULL; const char *chars = text ? (*env)->GetStringUTFChars(env, text, NULL) : NULL; vp_log("run: 编辑器类加载器 = %s", chars ? chars : "(未知)"); if (chars) (*env)->ReleaseStringUTFChars(env, text, chars); } jclass main_class = vp_define_directory(env, loader, g_classes_dir, "", g_main_class); if (!main_class) { vp_log("run: 没有定义到入口类 %s", g_main_class); return NULL; } { JNINativeMethod methods[] = { { "defineIntoLoader", "(Ljava/lang/ClassLoader;)Ljava/lang/Class;", (void *)VPC_defineIntoLoader }, }; if ((*env)->RegisterNatives(env, main_class, methods, 1) != JNI_OK) { vp_log("run: 注册 defineIntoLoader 失败"); if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionDescribe(env); (*env)->ExceptionClear(env); } } else { vp_log("run: 已注册 defineIntoLoader"); } } g_panel_class = (jclass)(*env)->NewGlobalRef(env, main_class); g_on_render = (*env)->GetStaticMethodID(env, main_class, "onRenderThread", "()V"); if (!g_on_render) { vp_log("run: 找不到 onRenderThread"); if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionDescribe(env); (*env)->ExceptionClear(env); } } /* 把进程号写进去:日志里要能区分启动器进程和编辑器进程。 */ { jfieldID pid_field = (*env)->GetStaticFieldID(env, main_class, "pid", "J"); if (pid_field) { (*env)->SetStaticLongField(env, main_class, pid_field, (jlong)getpid()); vp_log("run: 已把 pid=%d 写进 Java", (int)getpid()); } else { if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); vp_log("run: 没找到 pid 静态字段"); } } jclass string_class = (*env)->FindClass(env, "java/lang/String"); jobjectArray arguments = (*env)->NewObjectArray(env, 0, string_class, NULL); jmethodID main_method = (*env)->GetStaticMethodID(env, main_class, "main", "([Ljava/lang/String;)V"); if (!main_method) { vp_log("run: 找不到 main([Ljava/lang/String;)V"); if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionDescribe(env); (*env)->ExceptionClear(env); } return NULL; } vp_log("run: 调用 %s.main", g_main_class); (*env)->CallStaticVoidMethod(env, main_class, main_method, arguments); if ((*env)->ExceptionCheck(env)) { vp_log("run: main 抛出异常"); (*env)->ExceptionDescribe(env); (*env)->ExceptionClear(env); } return NULL; } static void vp_start_thread(void) { pthread_t thread; if (pthread_create(&thread, NULL, vp_run_thread, NULL) == 0) { pthread_detach(thread); vp_log("已启动注入线程"); } else { vp_log("线程创建失败"); } } typedef jint (*get_created_vms_fn)(JavaVM **, jsize, jsize *); /* 有些进程(例如 3.8.99 的 x86 编辑器)是从启动器进程继承 JVM 的, * 根本不会调用 JNI_CreateJavaVM —— 钩子自然不会触发。 * 这里改用 JNI_GetCreatedJavaVMs 去取已经存在的 VM。 */ static void *vp_find_existing_vm_thread(void *unused) { (void)unused; for (int attempt = 0; attempt < 1200; attempt++) { if (g_vm) return NULL; get_created_vms_fn get_vms = real_dlsym ? (get_created_vms_fn)real_dlsym(RTLD_DEFAULT, "JNI_GetCreatedJavaVMs") : NULL; if (get_vms) { JavaVM *vm = NULL; jsize count = 0; if (get_vms(&vm, 1, &count) == JNI_OK && count > 0 && vm) { g_vm = vm; vp_log("用 JNI_GetCreatedJavaVMs 拿到已存在的 JVM,开始注入"); vp_start_thread(); return NULL; } } usleep(500000); } return NULL; } static void vp_start_vm_watch(void) { pthread_t thread; if (pthread_create(&thread, NULL, vp_find_existing_vm_thread, NULL) == 0) pthread_detach(thread); } /* ---------- 抓编辑器类加载器 ---------- */ /* 编辑器在给自己绑定原生方法时会 dlsym("Java_...");此刻调用 FindClass, * 正好落在编辑器自己的类加载上下文里,用它反推编辑器类加载器。只观察,不改返回值。 */ static void vp_try_capture(const char *symbol) { pthread_mutex_lock(&g_lock); int need = (g_editor_loader == NULL); pthread_mutex_unlock(&g_lock); if (!need || !g_vm) return; JNIEnv *env = NULL; if ((*g_vm)->GetEnv(g_vm, (void **)&env, JNI_VERSION_1_6) != JNI_OK || !env) return; jclass class_class = (*env)->FindClass(env, "java/lang/Class"); jmethodID get_class_loader = class_class ? (*env)->GetMethodID(env, class_class, "getClassLoader", "()Ljava/lang/ClassLoader;") : NULL; for (size_t index = 0; index < sizeof(kEditorAnchorClasses) / sizeof(kEditorAnchorClasses[0]); index++) { jclass found = (*env)->FindClass(env, kEditorAnchorClasses[index]); if (!found) { (*env)->ExceptionClear(env); continue; } jobject loader = get_class_loader ? (*env)->CallObjectMethod(env, found, get_class_loader) : NULL; if (!loader || (*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); /* FindClass 落在 bootstrap/系统加载器上时 getClassLoader() 会是 null, * 这时改用当前 Java 线程的上下文加载器(编辑器线程的上下文就是编辑器加载器)。 */ jclass thread_class = (*env)->FindClass(env, "java/lang/Thread"); jmethodID current = thread_class ? (*env)->GetStaticMethodID(env, thread_class, "currentThread", "()Ljava/lang/Thread;") : NULL; jmethodID context = thread_class ? (*env)->GetMethodID(env, thread_class, "getContextClassLoader", "()Ljava/lang/ClassLoader;") : NULL; if (current && context) { jobject thread = (*env)->CallStaticObjectMethod(env, thread_class, current); if (thread) loader = (*env)->CallObjectMethod(env, thread, context); if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); loader = NULL; } } } if (!loader) { vp_log("锚点 %s 的加载器为空,继续试下一个", kEditorAnchorClasses[index]); continue; } pthread_mutex_lock(&g_lock); if (!g_editor_loader) { g_editor_loader = (*env)->NewGlobalRef(env, loader); char message[512]; snprintf(message, sizeof(message), "捕获编辑器类加载器(锚点 %s,触发符号 %s)", kEditorAnchorClasses[index], symbol ? symbol : "(null)"); pthread_mutex_unlock(&g_lock); vp_log("%s", message); return; } pthread_mutex_unlock(&g_lock); return; } } /* ---------- JVM 创建拦截 ---------- */ static jint vp_create_vm(JavaVM **pvm, void **penv, void *args) { vp_log("拦截到 JNI_CreateJavaVM"); jint result = real_create_vm ? real_create_vm(pvm, penv, args) : JNI_ERR; if (result != JNI_OK || !pvm || !*pvm) { vp_log("JNI_CreateJavaVM 失败: %d", (int)result); return result; } g_vm = *pvm; vp_log("JVM 已创建,等编辑器类加载器"); vp_start_thread(); return result; } /* ---------- _dlsym 重绑定 ---------- */ /* 渲染线程上的钩子:编辑器每帧调 glfwSwapBuffers 时,先问一下 Java 侧要不要装配面板。 * 只在前若干帧回调,之后彻底安静。 */ static void vp_swap(JNIEnv *env, jclass clazz, jlong window) { const char *no_call = getenv("SPINE_VIEWPANEL_NO_JCALL"); if (g_panel_class && g_on_render && env && g_swap_calls < 20000 && !(no_call && no_call[0] == '1')) { g_swap_calls++; (*env)->CallStaticVoidMethod(env, g_panel_class, g_on_render); if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionDescribe(env); (*env)->ExceptionClear(env); } } if (real_swap) real_swap(env, clazz, window); } static void *vp_dlsym(void *handle, const char *name) { if (name && strcmp(name, "JNI_CreateJavaVM") == 0) { if (!real_create_vm && real_dlsym) real_create_vm = (create_vm_fn)real_dlsym(handle, name); vp_log("dlsym: 返回注入版 JNI_CreateJavaVM"); return (void *)vp_create_vm; } /* 只截 JNI 方法,不碰 C 函数 glfwSwapBuffers。 * libjglfw 里两个符号都有:C 函数的签名是 (GLFWwindow*),JNI 方法是 (JNIEnv*,jclass,jlong)。 * 截错那个会让包装函数用错签名,直接把渲染搞坏(实测窗口全黑)。 */ if (name && strncmp(name, "Java_", 5) == 0 && strstr(name, "glfwSwapBuffers") != NULL) { void *real = real_dlsym ? real_dlsym(handle, name) : NULL; if (!real) { vp_log("dlsym: %s 拿不到真实实现,不挂钩(否则会没有交换缓冲 → 黑屏)", name); return NULL; } /* 实测:替换这个 JNI 方法会让编辑器整窗黑屏(启动器没事),所以默认不挂。 * 需要时用 SPINE_VIEWPANEL_SWAP=1 显式打开。 */ const char *enable = getenv("SPINE_VIEWPANEL_SWAP"); if (!enable || enable[0] != '1') return real; if (getenv("SPINE_VIEWPANEL_NO_SWAP")) return real; if (!real_swap) { real_swap = (swap_fn)real; void *via_default = real_dlsym(RTLD_DEFAULT, name); void *via_next = real_dlsym(RTLD_NEXT, name); vp_log("dlsym: 截住 %s 真实=%p RTLD_DEFAULT=%p RTLD_NEXT=%p", name, real, via_default, via_next); } return (void *)vp_swap; } if (name && strncmp(name, "Java_", 5) == 0) vp_try_capture(name); return real_dlsym ? real_dlsym(handle, name) : NULL; } static void vp_make_writable(void *address, size_t size) { uintptr_t page = (uintptr_t)address & ~((uintptr_t)getpagesize() - 1); uintptr_t end = ((uintptr_t)address + size + getpagesize() - 1) & ~((uintptr_t)getpagesize() - 1); mprotect((void *)page, end - page, PROT_READ | PROT_WRITE); } static void vp_rebind(const struct mach_header *header, intptr_t slide) { if (!header || header->magic != MH_MAGIC_64) return; const uint8_t *image = (const uint8_t *)header; const struct load_command *cmd = (const struct load_command *)(image + sizeof(struct mach_header_64)); const struct symtab_command *symtab = NULL; const struct dysymtab_command *dysymtab = NULL; const struct segment_command_64 *linkedit = NULL; for (uint32_t index = 0; index < header->ncmds; index++) { if (cmd->cmd == LC_SYMTAB) symtab = (const struct symtab_command *)cmd; else if (cmd->cmd == LC_DYSYMTAB) dysymtab = (const struct dysymtab_command *)cmd; else if (cmd->cmd == LC_SEGMENT_64) { const struct segment_command_64 *segment = (const struct segment_command_64 *)cmd; if (strcmp(segment->segname, "__LINKEDIT") == 0) linkedit = segment; } cmd = (const struct load_command *)((const uint8_t *)cmd + cmd->cmdsize); } if (!symtab || !dysymtab || !linkedit) return; const uintptr_t linkedit_base = (uintptr_t)(linkedit->vmaddr - linkedit->fileoff + slide); const struct nlist_64 *symbols = (const struct nlist_64 *)(linkedit_base + symtab->symoff); const char *strings = (const char *)(linkedit_base + symtab->stroff); const uint32_t *indirect = (const uint32_t *)(linkedit_base + dysymtab->indirectsymoff); cmd = (const struct load_command *)(image + sizeof(struct mach_header_64)); for (uint32_t index = 0; index < header->ncmds; index++) { if (cmd->cmd != LC_SEGMENT_64) { cmd = (const struct load_command *)((const uint8_t *)cmd + cmd->cmdsize); continue; } const struct segment_command_64 *segment = (const struct segment_command_64 *)cmd; const struct section_64 *sections = (const struct section_64 *)((const uint8_t *)segment + sizeof(struct segment_command_64)); for (uint32_t section_index = 0; section_index < segment->nsects; section_index++) { const struct section_64 *section = §ions[section_index]; if (strcmp(section->sectname, "__la_symbol_ptr") != 0 && strcmp(section->sectname, "__got") != 0) continue; if (section->reserved1 == 0) continue; const size_t count = section->size / sizeof(void *); for (size_t entry = 0; entry < count; entry++) { uint32_t symbol_index = indirect[section->reserved1 + entry]; if (symbol_index >= symtab->nsyms) continue; const char *name = strings + symbols[symbol_index].n_un.n_strx; if (name && strcmp(name, "_dlsym") == 0) { void **slot = (void **)((uintptr_t)section->addr + slide + entry * sizeof(void *)); vp_make_writable(slot, sizeof(void *)); *slot = vp_dlsym; } } } cmd = (const struct load_command *)((const uint8_t *)cmd + cmd->cmdsize); } } static void vp_image_added(const struct mach_header *header, intptr_t slide) { vp_rebind(header, slide); } __attribute__((constructor)) static void vp_install(void) { real_dlsym = (dlsym_fn)dlsym(RTLD_DEFAULT, "dlsym"); const char *dir = getenv("SPINE_VIEWPANEL_CLASSES"); snprintf(g_classes_dir, sizeof(g_classes_dir), "%s", (dir && dir[0]) ? dir : VP_DEFAULT_CLASSES); const char *main_class = getenv("SPINE_VIEWPANEL_MAIN"); snprintf(g_main_class, sizeof(g_main_class), "%s", (main_class && main_class[0]) ? main_class : VP_DEFAULT_MAIN); vp_log("=== inject.dylib 已载入 pid=%d classes=%s main=%s ===", (int)getpid(), g_classes_dir, g_main_class); uint32_t count = _dyld_image_count(); for (uint32_t index = 0; index < count; index++) { vp_rebind(_dyld_get_image_header(index), _dyld_get_image_vmaddr_slide(index)); } _dyld_register_func_for_add_image(vp_image_added); /* 万一这个进程不创建 JVM(继承来的),也要能注入。 */ vp_start_vm_watch(); }