目标:在 Spine 编辑器窗口内部加一个自己的视图面板(按钮 + 文字显示), 点击读取场景对象,并像原生面板一样出现在 Views 菜单、可停靠可折叠。 本轮打通: 1. 编辑器在我们自己的进程里完整启动 - 类加载器必须是 URLClassLoader 本身(自检要求) - Editor.main 必须在 AWT EDT 上调用 - jvm.cfg 第 21 行要以两个空格结尾 + -DSpineJava - Editor.D:I 版本号、fonts/、/tzax 资源、Java8 版 launcher.Launcher 替身 2. 3.8.99 全量解密 → 可读源码 - 字符串 17251/17461(伪造同名调用帧对齐 key) - 混淆动态派发还原 53871 处(调引导方法取 MemberName) 3. 自建视图 - agent 在内存里给 us 枚举加 spineView 常量(不改磁盘,避开完整性自检) - 面板 Actor 由外部反射填进 us.spineView.D - 复用已有图标做别名(不新建图片资源) 最终 "Spine View" 出现在 Views 菜单,可与 Tree 并排停靠、折叠展开 另外: - ViewPatcher/ViewAgent:视图枚举与面板挂载的字节码插桩 - DecAll/ObMap/rewrite_source:字符串解密、派发还原、可读源码生成 - 阶段四-视图面板挂载.md:完整记录做法与踩过的坑 不纳入版本管理:rea/(独立仓库)、tools 下的 JDK/Ghidra 工具链、 Spine.app 与 spine-lab 副本、从载荷派生的批量产物。
482 lines
22 KiB
C
482 lines
22 KiB
C
/* 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 <dirent.h>
|
||
#include <dlfcn.h>
|
||
#include <jni.h>
|
||
#include <mach-o/dyld.h>
|
||
#include <mach-o/loader.h>
|
||
#include <mach-o/nlist.h>
|
||
#include <pthread.h>
|
||
#include <stdarg.h>
|
||
#include <stdint.h>
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <sys/mman.h>
|
||
#include <sys/stat.h>
|
||
#include <unistd.h>
|
||
|
||
#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();
|
||
}
|