Files
tianmo 2e7ab7c33c 阶段四:Spine 3.8.99 编辑器里挂上自建视图面板
目标:在 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 副本、从载荷派生的批量产物。
2026-10-16 13:34:18 +08:00

482 lines
22 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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 = &sections[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();
}