# JMemoryEdit **Repository Path**: SnailcatMall/JMemoryEdit ## Basic Information - **Project Name**: JMemoryEdit - **Description**: 这个是基于jni技术,实现炫彩gui+d2d+d3d自绘组件 - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 1 - **Created**: 2026-09-30 - **Last Updated**: 2026-10-01 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # JMemoryEdit 项目说明文档 > 基于 **炫彩界面库 XCGUI(Jscm.dll / JNI)+ Java 8** 的 Windows 记忆体搜索与修改工具。 > 本文档覆盖:项目结构、依赖、编译运行方式、功能清单,以及开发过程中踩到的**每一个坑与处理办法**,最后是**注意事项**。 --- ## 目录 1. [项目简介](#1-项目简介) 2. [技术栈与依赖](#2-技术栈与依赖) 3. [目录结构与模块职责](#3-目录结构与模块职责) 4. [功能清单](#4-功能清单) 5. [编译与运行](#5-编译与运行) 6. [架构要点](#6-架构要点) 7. [遇到的问题与处理方法(重点)](#7-遇到的问题与处理方法重点) 8. [注意事项](#8-注意事项) 9. [附录 A:菜单 ID 分配表](#附录-a菜单-id-分配表) 10. [附录 B:窗口尺寸与标题](#附录-b窗口尺寸与标题) --- ## 1. 项目简介 JMemoryEdit 是一个仿 Cheat Engine 风格的 Windows 记忆体(内存)编辑工具: - **搜索**:按数据类型(字节/短整/整型/长整/浮点/双精度/字符串等)在目标进程内存中扫描数值; - **筛选**:对上一轮结果再做「增大/减小/未变/已变」二次过滤,逐步收敛; - **监视与修改**:把命中的地址加入「地址列表」,实时读取、锁定、改写目标进程内存; - **断点**:对地址下 VEH 访问 / 写入 / 执行断点; - **反汇编**:内置基于 CapStone 的反汇编查看器,上「反汇编」下「十六进制」,实时刷新,可直接改内存值; - **工具**:自动扫描坐标矩阵地址、人物地址深度解析(遍历所有人物结构)。 界面全部由炫彩界面库(XCGUI,Direct2D 渲染)绘制,**没有使用 Swing/JavaFX**。 --- ## 2. 技术栈与依赖 | 项目 | 说明 | |---|---| | 语言 | Java 8(`maven.compiler.release=8`,class 版本 52) | | 构建 | Maven(`pom.xml`),另有 PowerShell 直编脚本 `tools/build_tw.ps1` | | 界面 | 炫彩界面库 XCGUI,通过 `Jscm-Core.jar` 的 JNI 封装调用 | | 反汇编 | CapStone(`com.scm.all.export.capstone`,同样在 `Jscm-Core.jar` 内) | | 原生库 | `Jscm.dll` / `Jscm-Core.dll` 等,由 `PathFileJSCM` 指定目录加载 | `pom.xml` 中的关键依赖: ```xml com.scm Jscm-Core 1.0 system C:/Users/www10/IdeaProjects/Jscm-Core/out/artifacts/Jscm_Core_jar/Jscm-Core.jar org.apache.commons commons-lang3 3.14.0 ``` > `Jscm-Core` 是 `system` scope 的本地 jar,**换机器/换目录必须同步改 `pom.xml` 里的 `` 属性**,同时更新 `tools/cp.txt`(直编脚本用的 classpath 清单)。 --- ## 3. 目录结构与模块职责 ``` JMemoryEdit/ ├── pom.xml Maven 配置(release=8,Jscm-Core system 依赖) ├── README.md 本文件 ├── src/main/java/org/example/ │ ├── App.java ★ 程序入口:原生库注册、提权、建主窗口、居中 │ ├── core/ ★ 框架层(与业务无关的通用能力) │ │ ├── Ui.java 界面工具类(200KB+):建窗、建控件、列表、菜单、定时器、几何 │ │ ├── Theme.java 版式与配色令牌(字号、行高、间距、颜色) │ │ └── GpuDiagnostics.java Direct2D/GPU 环境诊断输出 │ └── jscm/ ★ 业务层 │ ├── MenuBar.java 顶部菜单栏(Tools 下拉 + 快捷键按钮 + 关于按钮) │ ├── ToolBar.java 工具条(进程 / 模块 / 搜索 / 属性 / 遍历) │ ├── WindowShow.java 主窗口内容装配(把所有业务面板拼进主窗口) │ ├── Shortcuts.java 全局快捷键(F1 快捷键说明 / Ctrl+U 关于) │ ├── AboutBtn.java 「关于」按钮 │ ├── openprocess/ 进程选择窗口(列表 + 右键菜单 + 附加) │ ├── openmodule/ 模块选择窗口(列表 + 右键菜单 + 反汇编模块) │ ├── openselect/ 条件搜索窗口(搜索条件、偏移列表、类型选择) │ ├── showdata/ ★ 数据核心:搜索结果的读取/显示/锁定/改写 │ │ ├── SelectDataView.java 结果表 + 锁定列表(右键菜单最全的一处) │ │ ├── LockRow.java 一条锁定地址的数据模型 │ │ ├── LockAddressWindow.java 新增/编辑锁定地址 │ │ ├── LockValueWindow.java 修改锁定地址的当前数据 │ │ ├── ModuleMap.java 模块名 + 偏移 的地址标注 │ │ └── TagMomeory.java 数据类型的枚举定义 │ ├── showVeh/ VEH 断点窗口(命中记录) │ ├── showHexBool/ 特征分析工具(十六进制/布尔视图) │ ├── showAttribute/ 地址属性列表窗口 │ ├── selfDraw/ 内存遍历工具(自绘表格 + 分页) │ ├── matrix/ 自动扫描坐标矩阵地址 │ ├── person/ ★ 人物地址深度解析(本次新增) │ └── disasm/ ★ 内存反汇编查看器(本次新增) └── tools/ 开发期脚本与产物(不影响运行) ├── cp.txt 直编用 classpath 清单 ├── build_tw.ps1 编译脚本 ├── *_tw.ps1 各种端到端验证脚本 └── probe/ 离线探针(验证原生库行为) ``` **分层约定**: - `core/` 只放"怎么画界面、怎么调度",不含任何内存搜索业务; - `jscm/` 每个子包 = 一个窗口或一个功能块,窗口类统一是 `XxxWindow`(窗口壳)+ `XxxPaneShow`(内容区)+ 若干 `XxxBtn` / `XxxText`(控件)。 - **新增窗口请在 `core/Ui.java` 里找现成的建控件方法复用,不要直接调原生 API。** --- ## 4. 功能清单 > 以下截图均为软件实际运行画面,统一存放于 `docs/images/`。 ### 4.1 主窗口 ![主窗口](docs/images/01-main.png) - 标题 `JMemory`,内容区 **500 × 730**;启动时按屏幕居中。 - 顶部:菜单栏(高 30px,`Tools` 下拉 + `快捷键` + `关于`)。 - 工具条按钮:**进程 / 模块 / 搜索 / 属性 / 遍历**。 - 中部:搜索结果表(可拖拽多选)。 - 下部:锁定地址列表(可勾选「锁定」,右键调出完整菜单)。 ### 4.2 工具条功能(进程 / 模块 / 搜索 / 属性 / 遍历) **选择进程**(`openprocess/`)——列出系统进程,左键双击附加;右键可复制进程名 / PID、刷新进程列表: ![选择进程](docs/images/04-process.png) **查看模块地址**(`openmodule/`)——列出目标进程已加载模块(句柄 + 路径),双击复制模块地址,右键可反汇编该模块: ![查看模块地址](docs/images/05-module.png) **条件搜索**(`openselect/`)——设置搜索数据 / 类型 / 参数配置 / 偏移条件,支持首次搜索、再次搜索、过滤列表、开启列表刷新: ![条件搜索](docs/images/06-search.png) **地址属性列表**(`showAttribute/`)——显示所选内存地址的区域地址、区域偏移、保护属性、内存大小、类型与原始数据: ![地址属性列表](docs/images/07-attribute.png) **内存遍历工具**(`selfDraw/`)——填写内存地址 / 偏移层数 / 各级偏移 / 遍历数量后「开始遍历」,右侧自绘表格 + 分页显示结果,支持导入 / 导出数据: ![内存遍历工具](docs/images/08-traverse.png) ### 4.3 Tools 下拉菜单(3 项) ![Tools 下拉菜单](docs/images/02-tools-menu.png) | 菜单项 | 打开的窗口 | |---|---| | 自动扫描坐标矩阵地址 | 自动扫描坐标矩阵地址(640×412) | | 人物地址深度解析(遍历所有人物结构) | 人物地址深度解析(900×620) | | 内存反汇编查看器 | 内存反汇编查看器(880×790) | > 这三个窗口**按主窗口中心弹出一次,之后自由拖动**,不再跟随主窗口。 **自动扫描坐标矩阵地址**(`matrix/`)——扫描候选矩阵数组,给出类型 / 分数 / 变稳噪 / 平移 XYZ,可加入地址列表: ![自动扫描坐标矩阵地址](docs/images/09-matrix.png) **人物地址深度解析**(`person/`): - **上表**:候选人物数组(`数组地址/模块 / 类型 / 人物数 / 分数 / 状态`); - **下表**:展开某个候选后,逐个列出角色(`# / 槽位地址 / 人物地址 / 模块 / 结构字段(前 32 字节)`); - 扫描可随时开始/停止(大内存扫描在工作线程跑,结果回投 UI); - 候选表右键 4 项、展开表右键 3 项。 ![人物地址深度解析](docs/images/10-person.png) **内存反汇编查看器**(`disasm/`): - 第一行:起始地址 + `GO` + `实时刷新` 勾选框 + `长度(字节)` + 状态文本。 - **上方**反汇编表:`地址 / 字节 / 指令`; - **下方**十六进制表:`地址 / 十六进制数据 / 文本`,行地址**按真实内存地址向下对齐 16 字节**,所以每行左端地址都是 `...0 / ...10 / ...20` 这样的真实地址; - **实时刷新**:500ms 定时器重新读取并刷新(可关闭); - **修改内存值**:输入地址 + 新字节,直接写目标进程内存; - **VEH 下断**:访问 / 写入 / 执行三种; - **加入地址列表**:直接进主窗口的锁定列表; - 右键菜单 7 项:`反汇编转到此地址 / 加入地址列表 / VEH访问该地址断点 / VEH写入该地址断点 / VEH执行该地址断点 / 修改该地址内存值 / 复制地址`。 ![内存反汇编查看器](docs/images/11-disasm.png) ### 4.4 全列表右键菜单 所有列表框(进程、模块、偏移、矩阵候选、人物候选、人物展开、反汇编、结果表、锁定列表、遍历表)**均支持右键菜单**,菜单样式与主题一致。以锁定列表为例(共 13 项): ![锁定列表右键菜单](docs/images/17-menu.png) ### 4.5 锁定地址相关对话框 **添加锁定地址**(手工新增一条监视项): ![添加锁定地址](docs/images/15-add-lock.png) **修改锁定地址数据**(改当前值 / 勾选「锁定」后持续写回目标进程): ![修改锁定地址数据](docs/images/16-edit-value.png) **VEH 断点窗口**(命中后展示异常地址与命中瞬间的寄存器信息;下断失败时给出原因提示,例如宿主 JVM 位宽与目标进程不一致、缺少 `Jscm.dll` 依赖、权限不足): ![VEH 访问断点](docs/images/13-veh.png) **特征分析工具**(以当前地址为起点做字节特征对比): ![特征分析工具](docs/images/14-feature.png) ### 4.6 快捷键 ![快捷键说明](docs/images/12-shortcuts.png) - `F1`:打开「快捷键说明」窗口(等价于点菜单栏的「快捷键」按钮)。 - `Ctrl+U`:打开「关于」——用系统默认浏览器打开项目主页(等价于点菜单栏的「关于」按钮)。 --- ## 5. 编译与运行 ### 5.1 环境 | 用途 | 路径 | |---|---| | 运行 JDK(Java 8) | `C:\Program Files\Java\jdk1.8.0_201\bin\java.exe` | | 编译 JDK(任意 ≥8) | `C:\Program Files\Java\dragonwell-17.0.11.0.12+9-GA\bin\javac.exe`,加 `--release 8` | ### 5.2 Maven 编译 ```powershell mvn -q clean compile ``` ### 5.3 直编(不依赖 Maven,开发期最常用) `tools/cp.txt` 里是一行分号分隔的 classpath(`Jscm-Core.jar` + `commons-lang3` + junit)。 ```powershell $cp = (Get-Content tools\cp.txt -Raw).Trim() if (Test-Path 'tools\build\b3jscmcore') { $cp = 'tools\build\b3jscmcore;' + $cp } & 'C:\Program Files\Java\dragonwell-17.0.11.0.12+9-GA\bin\javac.exe' ` --release 8 -encoding UTF-8 -nowarn ` -d tools\build\classes -cp $cp ` (Get-ChildItem -Recurse src\main\java\*.java | ForEach-Object FullName) ``` ### 5.4 运行 ```powershell & 'C:\Program Files\Java\jdk1.8.0_201\bin\java.exe' -cp "tools\build\classes;$cp" org.example.App ``` - 原生库目录默认写死在 `App.java` 的 `DEFAULT_SDK_X86 / DEFAULT_SDK_X64`,可用系统属性覆盖: ```powershell java -Djscm.sdk.x86="D:\sdk\x86\" -Djscm.sdk.x64="D:\sdk\x64\" -cp ... org.example.App ``` ### 5.5 启动链路(`App.java`) ``` System.loadLibrary("jawt") ← 必须最先,见 8.1 → PathFileJSCM.setDebugFile32/64(sdkDir) ← 告诉原生层去哪找 Jscm 库 → PathFileJSCM.setIsDebug(false) → ProcessAndThreadUtils.elevatePrivileges(..) ← 提权(管理员),失败直接退出 → XCChainedUI.app(true).init() ← 初始化炫彩引擎 → GpuDiagnostics.init() ← D2D/GPU 诊断 → Ui.createWindow(500, 730, "JMemory") → WindowShow.init(Ui.body(mainWindow)) ← 装配所有面板 → Ui.installUiTimer(mainWindow, 9101, 50) ← 启动 UI 回调泵(50ms) → Shortcuts.install(...) → mainFrame.show() → Ui.centerOnScreen(mainWindow, W, H) ← 按尺寸算中心,不量窗口 ``` --- ## 6. 架构要点 ### 6.1 线程模型 - **所有界面操作必须在 UI 线程**。原生引擎没有 `SwingUtilities.invokeLater` 那种机制。 - 后台线程(如搜索结果、人物扫描)要回投 UI,**唯一通道是 `Ui.post(Runnable)`**,它把任务塞进队列,由主窗口的 `9101` 定时器(50ms 一次)在 UI 线程里 `drainUiQueue()` 取出来执行。 - 跨线程共享的可变状态(列表、结果集合)统一在 UI 线程里改,或在后台线程改完只 `post` 一个"刷新"动作。 ### 6.2 版式令牌 所有尺寸/颜色都走 `core/Theme.java`,**不要在业务代码里写魔法数字**: ``` FONT_SIZE=13 FONT_SIZE_SMALL=12 FONT_SIZE_LARGE=16 FONT_SIZE_TITLE=13 CONTROL_H=30 HEADER_H=32 ROW_H=26 TOOLBAR_H=52 GAP_X=10 GAP_Y=10 SCROLL_W=12 TITLE_H=38 ``` 窗口边框补偿(算居中/贴边时用):`Ui.FRAME_SIDE=8`、`FRAME_TOP=Theme.TITLE_H=38`、`FRAME_BOTTOM=8`。 ### 6.3 窗口定位 API | 方法 | 语义 | |---|---| | `Ui.centerOnScreen(hWnd, w, h)` | 按**传入尺寸**算屏幕居中(不量窗口,避免量到未定型的尺寸) | | `Ui.centerOnMain(hWnd)` | 居中于主窗口;主窗未建则退回屏幕居中;允许溢出屏幕 | | `Ui.dockToMain(hWnd, dx, dy)` | 把窗口钉在主窗口左上角 + (dx, dy) —— **"吸附/跟随"就是它** | | `Ui.showWindow(hWnd)` | 显示(带淡入 + 上滑动画) | | `Ui.installWindowTimer(h, id, ms, r)` | 窗口级定时器(每窗口一个 ID 空间) | ### 6.4 列表刷新两种方式(差别很大) | 方法 | 行为 | |---|---| | `Ui.setRowCells(list, row, cells...)` | **只改文本**(内部 `XAdTable_SetItemText` + `XList_RefreshRow`),**不动选中行、不动滚动位置** | | `Ui.clearRows(list)` | 清空全部行 → **会重置选中行与滚动位置** | | `Ui.addRow` + `Ui.refreshRows` | 建表/整表重建 | > **实时刷新循环里只能用 `setRowCells`**,用 `clearRows` 会让用户的选中行和滚动条每 500ms 跳一次。 --- ## 7. 遇到的问题与处理方法(重点) > 下面每一条都是开发中真实踩到的,按"现象 → 根因 → 处理办法"记录。 ### 7.1 CapStone 原生封装有四个必踩的坑 反汇编用的是 `com.scm.all.export.capstone`(在 `Jscm-Core.jar` 内)。这是**薄封装**,原生层的实现细节和 Java 侧的直觉不一致,直接照签名用必定踩雷。 #### 坑① 回调对象不是传进去的那一个 - **现象**:用匿名内部类做回调,结果全为 null,**点一下就 `NullPointerException`**。 - **根因**:原生层拿到传入的 Java 对象后,是 `GetObjectClass(obj)` + `AllocObject(cls)` **造了一个新实例**再回调的。所以: ```java // ✗ 错误:匿名内部类捕获的 result 在原生回调的那个"新实例"里是 null final List result = new ArrayList<>(); CapStone.CapStone_cs_disasm(code, addr, new Tag_cs_insnCallBack() { @Override public void callback(int a, long b, String op) { result.add(op); } // NPE }); ``` - **处理办法**:回调实现必须是 **`public static` 的具名类**,只用 **`static` 字段**收集结果。见 [Disasm.java](file:///c:/Users/www10/IdeaProjects/JMemoryEdit/src/main/java/org/example/jscm/disasm/Disasm.java#L61-L78): ```java public static final class Collector extends Tag_cs_insnCallBack { static long[] addresses = new long[0]; static String[] texts = new String[0]; static int count; @Override public void callback(int addressX86, long addressX64, String op_str) { int i = count; if (i < addresses.length) { addresses[i] = addressX64; texts[i] = op_str; } count = i + 1; } } ``` > 这不是代码风格问题,是**能不能拿到数据**的问题。 #### 坑② 全局 handle 没有线程保护 - **现象**:多线程同时反汇编会互相踩,指令错乱。 - **根因**:`CapStone_cs_open` 写的是**原生全局变量**,不是"每个调用各持一个句柄"。 - **处理办法**:把「open → disasm → 取结果」整段用 `synchronized` 串行化: ```java private static final Object LOCK = new Object(); public static List disasm(long address, byte[] code, boolean x64) { ... synchronized (LOCK) { ... } } ``` 另外用 `openedMode` 记住当前位数,**位数变了必须 `cs_free` 后重开**,否则 32/64 位混用会反出错误指令。 #### 坑③ `cs_disasm` 分配的 `insn` 数组从不 `cs_free`(内存泄漏) - **现象**:长时间开实时刷新,进程内存持续增长。 - **根因**:原生 `cs_disasm` 内部为指令数组分配了内存,但封装层**从不调用 `cs_free`**,每次调用固定漏「指令数 × ~250 字节」。 - **处理办法**: 1. 刷新频率压到 **500ms**(`DisasmWindow.TIMER_MS = 500`),不要每帧调; 2. 单次反汇编**限制字节数**(`MAX_DISASM_BYTES = 96`),控制单次指令条数; 3. 不要把它当成可以循环狂调的高频接口。 #### 坑④ 返回值是"指令条数",单条指令长度回调里拿不到 - **现象**:以为返回值是字节数;以为回调会带 `size`。 - **根因**:`cs_disasm` 返回**指令条数**;回调只给 `(addressX86, addressX64, op_str)`,**没有 size**。 - **处理办法**:单条长度用**相邻两条地址相减**推出;**最后一条**没有下一条可比,按 x86 指令长度上限 **15 字节**封顶(否则会把剩余整段都算成它的字节): ```java int end = (i + 1 < got) ? (int) Math.min(code.length, Collector.addresses[i + 1] - address) : Math.min(code.length, offset + 15); int size = Math.max(1, Math.min(end - offset, code.length - offset)); ``` 另外 `op_str` **已经带助记符**(如 `mov rax, qword ptr [rbx + 8]`),不需要自己拼。 #### 附:离线探针验证(必须做) 原生库跟 UI 无关,写个 `main` 就能验证,**不要去 UI 里猜**。探针 [tools/probe/DisasmProbe.java](file:///c:/Users/www10/IdeaProjects/JMemoryEdit/tools/probe/DisasmProbe.java) 的实机输出(x64 6 条 + x86 5 条,字节/长度/文本全对): ``` --- x64 bytes=14 insns=6 0x140001000 size=5 [48 89 5C 24 08] mov qword ptr [rsp + 8], rbx 0x140001005 size=4 [48 8B 43 08] mov rax, qword ptr [rbx + 8] 0x140001009 size=2 [FF D0] call rax 0x14000100B size=1 [90] nop 0x14000100C size=1 [90] nop 0x14000100D size=1 [C3] ret covered=14/14 --- x86 bytes=8 insns=5 (covered=8/8) --- x64 复跑 ... covered=14/14 ← 重复调用稳定 empty -> 0 ← 空输入不抛异常 ``` **探针必须做原生初始化**,否则 `cs_open` 找不到库、返回 0 条指令(见 7.9)。 ### 7.2 引擎自绘菜单通路在本版引擎里一次都不派发 - **现象**:用引擎自带 `XMenu_Create/Popup` + 自绘回调(`XE_MENU_DRAW_BACKGROUND / DRAWITEM / SELECT`)做下拉菜单,**回调一次都不进**,菜单弹不出来或弹出来是空白。 - **根因**:本版引擎的菜单自绘通路未派发(`Ui.onRowMenu` 注释里写明)。 - **处理办法**:改用 `XCContextMenuComponentUI`——一个**自绘的 XLayout 面板 + 透明按钮捕获点击**,把它绑到**顶级弹出窗口**上(否则会被客户区裁剪): ```java menu = XCContextMenuComponentUI.init(App.mainWindow, MENU_TEXTS, MENU_IDS); XCContextMenuComponentUI.setOnSelect(menu, (id, label) -> performMenuAction(id)); XCContextMenuComponentUI.show(menu, rc.left, rc.bottom + 2); ``` - 顶部菜单栏 [MenuBar.java](file:///c:/Users/www10/IdeaProjects/JMemoryEdit/src/main/java/org/example/jscm/MenuBar.java) 也**没用引擎自带 `XCMenuBarComponentUI`**(它不对外开放选择回调),而是沿用同一套自绘下拉:把弹出时机从「右键」换成「左键点按钮」(组件本身只挂右键事件,左键要自己调 `show`)。 ### 7.3 弹出菜单点外面不关(要自己盯) - **现象**:菜单弹出后,点菜单外面关不掉。 - **根因**:这个弹出窗口**形状透明 + `WS_EX_NOACTIVATE`**,导致 `WM_LBUTTONDOWN` / `WM_RBUTTONDOWN` / `WM_KILLFOCUS` **都走不到**。 - **处理办法**:主窗口装一个 **50ms 定时器**轮询,自己判断"点没点在菜单外面": 1. `GetAsyncKeyState` 看左右键是否刚按下; 2. `GetCursorPos` 取光标位置; 3. 与菜单面板的客户区矩形比,不在里面就关菜单。 实现见 `Ui.watchPopupMenu` / `Ui.dismissMenuIfClickedOutside`(`core/Ui.java`)。 ### 7.4 弹窗的标题栏按钮不派发点击事件 - **现象**:主窗口的标题栏关闭/最小化按钮正常,但**弹出窗口**上的按钮点了没反应。 - **根因**:`XE_BNCLICK` 在弹出窗口上不派发。 - **处理办法**:`redrawOnMouse` —— 自己记鼠标**按下/松开**的配对,在松开时判定为一次点击。 ### 7.5 右键菜单拿不到"右键点的那一行" - **现象**:菜单项回调执行时,`XList_GetSelectRow` 拿到的行不对甚至为 -1。 - **根因**: 1. 菜单项回调发生时**鼠标早就移走了**,实时命中测试没意义; 2. **列表框(XListBox)不接受 `XList_GetSelectRow`**,必须走 `XListBox_HitTest`。 - **处理办法**: 1. 在 `XE_RBUTTONUP` 里**提前记住命中行**(`Ui.rememberMenuHit`,存进 `MENU_HIT`); 2. 菜单回调里用 `Ui.menuRow(list)` / `Ui.menuColumn(list)` 取(内部对列表框走 `XListBox_HitTest`); 3. 兜底:`menuRow` 返回 -1 时退回当前选中行。 ### 7.6 列表框必须用 `XListBox_CreateEx` - **现象**:自己 `Create` + `BindAdapter` 建出来的列表框**整列空白**。 - **处理办法**:统一用 `Ui.createListBox` / `Ui.listBox`,内部走 `XListBox_CreateEx`;列表创建后要调 `Ui.refreshRows` / `Ui.refreshListBox` 补套主题——**项模板要渲染过首行之后才建立**,不刷新第一屏会缺样式。 ### 7.7 高频刷新把用户的选中行/滚动条冲掉 见 [6.4](#64-列表刷新两种方式差别很大):实时刷新循环里**只用 `Ui.setRowCells`**,不要用 `clearRows`。反汇编窗口/人物窗口的做法是:**建表时一次性建满固定行数**,之后每次刷新只改文本。 ### 7.8 工具窗口"吸附"在主窗口旁边 - **现象**:从 Tools 弹出的窗口会**粘着主窗口跑**,主窗口一拖动它就跟着动,且无法自由摆放。 - **根因**:每个窗口 `init()` 里装了 `Ui.installWindowTimer(window, id, 100, () -> Ui.dockToMain(window, dx, dy))`——100ms 一次把窗口钉回主窗口左下角。 - **处理办法**: 1. **删掉跟随定时器**(连同 `OFFSET_X / OFFSET_Y / TIMER_ID / TIMER_MS` 常量); 2. `show()` 里把 `dockToMain` 换成 **`Ui.centerOnMain(window)`**(按主窗口中心弹出一次); 3. 之后窗口自由拖动,不再跟随。 > 现状:Tools 下拉三项(矩阵扫描 / 人物解析 / 反汇编)已完成去吸附。 > **`showAttribute/AttributeWindow.java` 与 `openselect/selectShowWindow.java` 仍是 `dockToMain` 跟随**(`OFFSET_X=515`、`OFFSET_Y=481/0`、`TIMER_ID=9201/9203`、`TIMER_MS=100`),如需一致体验,按同样三步处理。 ### 7.9 探针/工具类未初始化原生库 → 反汇编返回 0 条 - **现象**:`DisasmProbe` 首跑 `insns=0`(x64/x86 都是 0,`covered=0`)。 - **根因**:探针没做原生初始化,`CapStone_cs_open` 找不到库。 - **处理办法**:照 `App.java` 的启动前几步补上(**顺序不能变**): ```java System.loadLibrary("jawt"); PathFileJSCM.setDebugFile32("...\\Jscm-Core\\src\\main\\resources\\init_\\x86\\"); PathFileJSCM.setDebugFile64("...\\Jscm-Core\\src\\main\\resources\\init_\\x64\\"); PathFileJSCM.setIsDebug(false); ``` ### 7.10 主窗口居中偏上 - **现象**:开窗后按窗口实际高度居中,窗口偏上。 - **根因**:刚 `show()` 出来的窗口,**量到的高度还不是最终值**。 - **处理办法**:`Ui.centerOnScreen(hWnd, w, h)` 这个重载**按传入尺寸算**,不要量窗口;主窗口传 `CONTENT_W + FRAME_SIDE*2`、`CONTENT_H + FRAME_TOP + FRAME_BOTTOM`。 ### 7.11 编译脚本产物文件被占用 - **现象**:`tools\build_tw.ps1` 报 `The process cannot access the file 'tools\build_tw.out' because it is being used by another process`,且 `JAVAC_EXIT=` 为空(拿不到结果)。 - **根因**:脚本把 stdout/stderr 重定向到固定文件,上一次运行的句柄没释放(或 IDE 占着)。 - **处理办法**:直接按 [5.3](#53-直编不依赖-maven开发期最常用) 的命令调 `javac`,输出到控制台即可(已验证 `sources=82 / JAVAC_EXIT=0`)。 ### 7.12 PowerShell 验证脚本的几个坑 写端到端 UI 验证脚本时踩到: - **硬编码坐标点菜单必然失败**:主窗口位置会被 IDE/其它窗口改变(`rect` 从 `1022,308` 变成 `696,409`),点到空处,报 `NO WINDOW AFTER ITEM k`。 **处理办法**:脚本开头先 `[MX]::Move($main, 900, 300)` 把主窗口固定到确定位置;菜单面板位置不硬编码,改用「点击前后对某区域做**像素采样比对求 diff bbox**」自动推出面板顶边与各项 Y 坐标。 - **`Add-Type` 报「`mouse_event` 方法没有任何重载采用 6 个参数」**:C# 里 `mouse_event` 声明是 **5 参**,滚轮分支误写了 6 参。 改为 `mouse_event(0x0800, 0, 0, unchecked((uint)(-120)), IntPtr.Zero);`。 - **`[MX]::Escape` 被 PowerShell 当成方法组打印**:改成 `[MX]::Escape()`(带括号)。 ### 7.13 反汇编窗口状态文案被裁切 - **现象**:状态行 `"尚未打开目标进程:请先在工具条点「进程」选一个进程"` 太长,右端被裁掉。 - **处理办法**:缩短为 `"未打开目标进程:先点工具条「进程」"`。 --- ## 8. 注意事项 ### 8.1 必须最先 `System.loadLibrary("jawt")` `jawt` 必须在**任何炫彩原生调用之前**加载,否则后面 `XCChainedUI.app(true).init()` 会失败。`App.java` 把它放在 `main` 的第一句。 ### 8.2 必须管理员权限 `ProcessAndThreadUtils.elevatePrivileges(...)` 失败会**直接退出**。读写别的进程内存、下 VEH 断点都需要管理员权限,**不要用受限账户运行**。 ### 8.3 原生库路径 - `PathFileJSCM.setDebugFile32/64` 必须在 `XCChainedUI.app(true).init()` **之前**调用; - 换机器要改 `App.java` 里的默认 SDK 目录,或用 `-Djscm.sdk.x86=` / `-Djscm.sdk.x64=` 覆盖; - 32 位 / 64 位目标要选对应的 SDK 目录,**不匹配会导致搜索/读取异常**。 ### 8.4 线程 - **禁止在后台线程直接操作界面控件**,一律 `Ui.post(Runnable)` 回投; - UI 回调泵是主窗口的 `9101` 定时器(**50ms**),`Ui.post` 的任务最快也要等下一拍; - `Ui.keep(...)` 用来保活回调对象,**不要省**,否则可能被 GC 掉。 ### 8.5 自绘三条铁律 1. `hDraw` **不可跨帧缓存** —— 每帧重新取; 2. 回调里**不要自己 `beginDraw/endDraw`**(引擎已在绘制中); 3. **不要释放引擎的渲染目标**。 ### 8.6 CapStone - **不要把 `Disasm.disasm` 当高频接口用**(见 7.1 坑③,会泄漏); - 32/64 位切换会重开引擎,**别在循环里混着调**; - 反汇编结果是「一次性、整段」语义,读不到就返回空列表,**不抛异常**。 ### 8.7 需要先附加目标进程的功能 以下功能**必须先通过工具条「进程」附加一个目标进程**才有意义: - 内存搜索 / 二次筛选; - 反汇编实时刷新、修改内存值、VEH 下断; - 人物地址扫描; - 矩阵地址扫描。 未附加进程时,反汇编窗口会在状态行提示 `未打开目标进程:先点工具条「进程」`。 ### 8.8 构建 - `maven.compiler.release=8`(class 52)与 IDEA 工程的 `languageLevel=JDK_1_8`、`Jscm-Core.jar` 保持一致,**不要擅自升到 11/17**; - `Jscm-Core` 是 `system` scope,**不会进 Maven 依赖树也不会被打进产物**,打包发布时要手动带上 jar 和原生库; - 用高版本 JDK 编译时必须写 `--release 8`,否则产物 class 版本过高、Java 8 运行不了。 ### 8.9 新增窗口的惯性检查清单 新增一个工具窗口时,照这份清单过一遍,能避开上面大部分坑: - [ ] 窗口尺寸/标题常量定义在窗口类里(`CONTENT_W / CONTENT_H / TITLE`); - [ ] 需要"按主窗口中心弹一次"就用 `Ui.centerOnMain(window)`,**不要** `installWindowTimer + dockToMain`; - [ ] 关闭请求是"隐藏"还是"退出"要明确(多数工具窗口用 `Ui.onWindowClose → Ui.hideWindow`); - [ ] 列表用 `Ui.listBox` / `Ui.createListBox`,建完调 `refreshRows`; - [ ] 加右键菜单用 `Ui.onRowMenu`,需要的行内数据提前在列表里存好(**不要靠回调时去命中测试**); - [ ] 实时刷新循环里只 `setRowCells`,不 `clearRows`; - [ ] 需要后台计算就用 `Ui.post` 回投,不在后台线程碰控件; - [ ] 菜单项 ID 不要和已有的撞(见[附录 A](#附录-a菜单-id-分配表))。 --- ## 附录 A:菜单 ID 分配表 | 所属 | 常量 | ID | 文案 | |---|---|---|---| | 顶部 Tools 下拉 | `MENU_MATRIX_SCAN` | 1 | 自动扫描坐标矩阵地址 | | | `MENU_PERSON_SCAN` | 2 | 人物地址深度解析(遍历所有人物结构) | | | `MENU_DISASM` | 3 | 内存反汇编查看器 | | 锁定/结果列表 | `MENU_REMOVE` 等 | 2001–2024 | 见 `showdata/SelectDataView.java` | | 遍历表 | `MENU_ADD` | 2010 | 加入地址列表(复用结果表一份菜单) | | 进程列表 | `MENU_ATTACH` / `COPY_NAME` / `COPY_PID` / `REFRESH` | 9011–9014 | 附加到该进程 / 复制进程名 / 复制 PID / 刷新进程列表 | | 模块列表 | `MENU_DISASM` / `COPY_HANDLE` / `COPY_PATH` / `REFRESH` | 9015–9018 | 反汇编查看该模块 / 复制模块句柄 / 复制模块路径 / 刷新模块列表 | | 偏移列表 | `MENU_COPY` / `MENU_DELETE` / `MENU_CLEAR` | 9019–9021 | 复制偏移地址 / 删除该偏移 / 清空偏移列表 | | 矩阵候选表 | `MENU_ADD` / `MENU_DISASM` / `MENU_COPY` | 9201–9203 | 加入地址列表 / 反汇编查看该地址 / 复制地址 | | 反汇编窗口 | `M_GOTO` … `M_COPY_ADDR` | 9301–9307 | 反汇编转到此地址 / 加入地址列表 / VEH访问 / VEH写入 / VEH执行 / 修改该地址内存值 / 复制地址 | | 人物候选表 | `M_EXPAND` / `M_ADD_LOCK` / `M_DISASM` / `M_COPY` | 9401–9404 | 解析该候选结构 / 加入地址列表 / 反汇编查看该数组 / 复制数组地址 | | 人物展开表 | `M_DISASM` / `M_ADD_LOCK` / `M_COPY` | 9403 / 9402 / 9404 | 反汇编查看该人物 / 加入地址列表 / 复制人物地址 | **定时器 ID 分配**(避免撞车):UI 主泵 `9101`(50ms);窗口跟随定时器 `9201`(属性)/ `9203`(条件搜索);反汇编实时刷新 `9217`(500ms)。 --- ## 附录 B:窗口尺寸与标题 | 窗口 | 内容区尺寸 | 标题 | 定位方式 | |---|---|---|---| | 主窗口 | 500 × 730 | `JMemory` | 屏幕居中 | | 进程选择 | 300 × 500 | `选择进程- 左键双击确认` | 弹出 | | 模块选择 | 800 × 500 | `查看模块地址 双击复制模块地址` | 弹出 | | 条件搜索 | 500 × 动态 | `条件搜索`(标题随结果条数改写) | `dockToMain` 跟随 | | 地址属性列表 | 与条件搜索同宽 × 250 | `地址属性列表` | `dockToMain` 跟随 | | 内存遍历工具 | 1168 × 572 | `内存遍历工具` | 弹出 | | VEH 断点 | 560 × 430 | `VEH 访问断点 [ 断点地址:… ]` | 弹出 | | 特征分析 | 560 × 460 | `特征分析工具[ 当前地址:… 当前长度:… ]` | 弹出 | | 自动扫描坐标矩阵地址 | 640 × 412 | `自动扫描坐标矩阵地址` | `centerOnMain` | | 人物地址深度解析 | 900 × 620 | `人物地址深度解析` | `centerOnMain` | | 内存反汇编查看器 | 880 × 790 | `内存反汇编查看器` | `centerOnMain` | | 修改锁定地址数据 | 见 `LockValueWindow` | `修改锁定地址数据` | 弹出 | | 添加锁定地址 | 见 `LockAddressWindow` | `添加锁定地址` | 弹出 | | 快捷键说明 | 见 `Shortcuts` | `快捷键说明` | 弹出 | --- *文档最后更新:2026-10-01*