# 用 REA 分析 Spine.app — 首轮结果 日期:2026-10-15 工具:REA (rea-agents 6.5.0),仓库来源 `https://github.com/morluto/rea.git` 目标:`/Users/tianmokeji/Desktop/临时/spine加功能/Spine.app`(Spine 4.3.06 启动器 / 4.3.26 Professional 编辑器) ## 0. 怎么跑起来的 | 步骤 | 结果 | | --- | --- | | `git clone https://github.com/morluto/rea.git` | 直连 **卡死在 72KB**;换 `ghfast.top` 镜像也卡在 4.1MB | | `git -c http.version=HTTP/1.1 clone` | **成功**,68MB / 约 4 秒。沙箱代理下 HTTP/2 大包传输会中断,强制 HTTP/1.1 可绕过 | | `npm install -g rea-agents` | 安装 rea 6.5.0,CLI 可用 | | `rea doctor --client codex` | MCP 未注册;Hopper/Ghidra/IDA 均缺失(深度反汇编不可用) | REA 的 MCP 注册需要重启客户端才生效,本轮全部走 **CLI**(REA 技能文档认可的同源路径)。 **踩坑**:REA 的 `inspect_plist` / `inspect_signature` / `observe-native-ui` 会现场用 `xcrun` 编译辅助进程,沙箱内会失败,必须放沙箱外执行。 ## 1. 包结构(REA `inspect-artifact`) - 图规模:185 节点 / 239 次出现 / 238 条边,`coverage.status = complete` - Info.plist:`CFBundleShortVersionString 4.3.06`、`LSMinimumSystemVersion 10.13`、文档类型 `.spine` - 签名(`inspect-signature`):**ad-hoc 签名**,`authorities: []`、`team_identifier: null`、`hardened_runtime: false`、无 entitlements `/Applications/Spine.app` 与工作区副本的 `Contents/MacOS/Spine` SHA-256 完全一致(`fea6e69a…4340f2`),两个 libjvm 大小也一致 → 工作区副本即真实安装的等价副本。 ## 2. 启动链路(REA `inspect-macho` + `trace-dylib-resolution`) `Contents/MacOS/Spine` 只有 88KB,是纯原生启动器: - 链接:Foundation / AppKit / Cocoa / libobjc / libSystem(**不含 libjvm**,JVM 是运行期 `dlopen` 进来的) - 内置字符串给出完整启动参数: ``` -Djava.class.path=%@/Contents/MacOS/launcher/1/launcher.jar %@/Contents/MacOS/launcher/1/lib/jli/libjli.dylib → 失败则 %@/Contents/MacOS/launcher/2/lib/libjli.dylib %@/Contents/MacOS/launcher/1/lib/server/libjvm.dylib → 失败则 %@/Contents/MacOS/launcher/2/lib/server/libjvm.dylib JNI_CreateJavaVM 主类候选:com/esotericsoftware/spine/editor/launcher/Launcher、com/esotericsoftware/spine/launcher/Launcher ``` `launcher.jar` 只有 72 个条目 / 2MB(`s/*.class` 混淆包 + 一个 374KB 的 `Launcher.class`)——**它只是引导壳,不是编辑器本体**。 ## 3. 真正的加固点在 launcher/2 的 libjvm(已实测确认) REA 对两个 libjvm 的段对比: | | launcher/1/libjvm.dylib | launcher/2/libjvm.dylib | | --- | --- | --- | | 文件大小 | 13.3 MB | **58.4 MB** | | `__TEXT` | 8.5 MB | 8.1 MB | | `__DATA` | 313 KB | **48,896,800 B(≈48.9MB,文件偏移 8,933,376)** | | HotSpot 调试段 `__dof_hotspot*` | 有 | **无** | | `lib/modules`(jimage) | **46,549,519 B 存在** | **不存在** | `launcher/2/libjvm` 的 `__DATA` 前 1MiB 熵 **7.59 bit/byte**,且扫描不到 `PK\x03\x04` / `PK\x05\x06` / jimage magic → 是**加密载荷**,不是明文 jar。 配合缺 `modules` 仍能正常启动这一事实,结论是: > launcher/2 把 Java 模块镜像(jimage)连同编辑器类一起搬进了 libjvm 的 `__DATA` 并加密, > 由打过补丁的 JVM 在加载时解密 —— 这就是「类不在 jar 里」的真正原因。 ### 运行期到底加载了哪个?(零干扰实测) 对正在运行的 Spine(PID 91687)做 `lsof` / `vmmap` 只读采样: ``` …/launcher/2/lib/libjli.dylib …/launcher/2/lib/server/libjvm.dylib ← 实际使用 launcher/2,不是 launcher/1 ``` 即:**`launcher/1` 是引导尝试路径,真正的运行时是 `launcher/2`**,与第 3 节吻合。 ## 4. 新发现:编辑器原生核心在临时目录里 同一份 `lsof` 显示编辑器真正的原生库并不是从 app 包里加载的,而是运行期解包到临时目录: ``` $TMPDIR/.spine/tianmokeji/4.2.43/{libspine-editorarm64.dylib, libgdxarm64.dylib, libgdx-freetypearm64.dylib, libjglfwarm64.dylib, liblwjgl.dylib, openal.dylib} $TMPDIR/.spine/tianmokeji/L4.3.06/libjglfwarm64.dylib $TMPDIR/.spine/tianmokeji/4.3.26/libspine-editorarm64.dylib ``` REA `inspect-macho` 对 `libspine-editorarm64.dylib`(3,047,440 B)的结果: - install name `target/libspine-editorarm64.dylib`,依赖仅 Cocoa/AppKit/libSystem/libobjc/libc++ - 导出符号(REA 导出列表)里最有价值的几条: ``` _Java_com_esotericsoftware_spine_editor_Editor_licCheck _Java_com_esotericsoftware_spine_editor_Editor_register _Java_com_esotericsoftware_spine_editor_Editor_expire _Java_com_esotericsoftware_spine_editor_audio_Audio_pauseDeviceJni / resumeDeviceJni ``` 另外导出大量 **Eigen** 符号(`Eigen::Matrix`、`squared_edge_lengths`),说明网格/几何解算是 Eigen 在跑。 **这条信息对「在编辑器内部加粒子面板」很关键**:编辑器原生核心是**磁盘上、未加密、可直接分析的 dylib**, 比拿 libjvm 里那 49MB 加密数据段要现实得多。 日志侧证据:`~/Library/Application Support/Spine/spine.log` → `Spine Launcher 4.3.06 (macOS Apple Silicon)` / `Starting: Spine 4.3.26 Professional`。 ## 5. REA 运行时 UI 观测:被一个具体阻塞点挡住 REA 有 `observe-native-ui` / `capture-native-ui-scenario`(读取窗口的无障碍树 + 截图), 本来最适合你现在的面板研究。实测报错: ``` process-identity-unavailable: Selected application does not expose a launch time ``` 定位(读源码 + 实测):`rea/bridge/native/ReaNativeUI.swift` 用 `NSRunningApplication.launchDate` 做 PID 复用校验,而当前实例由命令行直接 exec 启动, LaunchServices 没有登记它: ``` pid=91687 bundleIdentifier=nil bundleURL=…/spine-lab/Spine.app launchDate=nil ``` **解决办法**:让 Spine 由 LaunchServices 启动(`open -na --args …` 或访达双击), 新进程才有 `launchDate`,REA 的 UI 树观测即可通过。 ## 6. 本轮结论清单 | 问题 | 状态 | 依据 | | --- | --- | --- | | 编辑器类为什么不在 jar 里 | 已答 | launcher.jar 仅 72 条目;launcher/2 无 `lib/modules`,libjvm `__DATA` 48.9MB 高熵 | | 实际运行哪个 libjvm | 已答(实测) | `lsof`/`vmmap`:launcher/2 | | 编辑器原生代码在哪 | 已答 | `$TMPDIR/.spine///libspine-editorarm64.dylib` | | 编辑器原生库的 JNI 面 | 已答 | REA 导出:`Editor_licCheck/register/expire`、`Audio_*` | | 包内清单 / 签名 / plist | 已答 | REA inventory / signature / plist | | 深度反汇编(伪代码、函数、xrefs) | **未答** | 缺 Hopper/Ghidra/IDA 提供商 | | 编辑器窗口内面板运行时结构 | **未答** | 需要先解决 launchDate(见第 5 节) | ## 7. 建议的下一步 1. **装 Ghidra(免费)**:`GHIDRA_INSTALL_DIR` 指过去,REA 立刻能对 `libspine-editorarm64.dylib`、`launcher/2/libjvm.dylib`、`libgdxarm64.dylib` 出伪代码与 xrefs。 2. **用 LaunchServices 启动 Spine**,然后跑 `rea capture-native-ui-scenario`,把编辑器面板/工具栏的无障碍树和可点击路径抓出来(可直接验证「窗口内面板是否可见、能否点」)。 3. 面板注入目标从 libjvm 改为 **`libspine-editorarm64.dylib`**:磁盘未加密、有完整 JNI 导出、可被 LLDB/Frida 附着。 4. 若要 REA 的 MCP 工具而非 CLI:`rea setup --client codex --dry-run --json` 看变更清单, 确认后 `--yes` 并重启 Codex(注册写入 `~/.codex/config.toml`)。 ## 8. 证据文件 | 文件 | 内容 | | --- | --- | | `rea-analysis/01-inspect-artifact.json` | 应用清单图(185 节点,含 SHA-256) | | `rea-analysis/02-macho-Spine.json` | 原生启动器 Mach-O(依赖、段、入口) | | `rea-analysis/03-signature.json` | ad-hoc 签名详情 | | `rea-analysis/04-dylib-resolution.json` | dyld 解析(8 个可执行根,含 ffmpeg/oxipng) | | `rea-analysis/05-macho-libjvm2.json` / `06-macho-libjvm1.json` | 两个 libjvm 段对比 | | `rea-analysis/07-native-ui.json` | 运行时 UI 观测尝试(记录阻塞原因) | | `rea-analysis/08-macho-spine-editor.json` | 编辑器原生库(约 1MB 导出/导入明细) | | `rea-analysis/tools/list-windows.swift` | 列出窗口 ID(配合 REA) | | `rea-analysis/tools/check-app-identity.swift` | 检查启动时间/LaunchServices 身份(定位第 5 节问题) |