diff --git a/docs/16-MemoryViewer设计文档.md b/docs/16-MemoryViewer设计文档.md new file mode 100644 index 0000000..0d39739 --- /dev/null +++ b/docs/16-MemoryViewer设计文档.md @@ -0,0 +1,391 @@ +# Memory Viewer 内存视图设计文档 + +## 1. 概述 + +Memory Viewer 是一个嵌入式面板视图,为用户提供实时内存数据查看、导出、导入和填充功能。作为 VS Code 面板容器中的视图(与问题、输出、终端同级),通过 DAP (Debug Adapter Protocol) 自定义请求与 DSS 调试引擎通信。 + +### 1.1 核心能力 + +| 功能 | 说明 | +|------|------| +| 内存读取 | 通过 DAP `readMemory` 从调试目标读取指定地址范围的内存数据 | +| 数据展示 | 支持十六进制、有符号/无符号整数、单精度/双精度浮点多种格式 | +| 滚动加载 | 向下滚动自动追加数据,向上滚动自动回溯历史数据 | +| 内存导出 | 支持 TI HEX / 整数文本 / 长整数文本 / 浮点文本 / COFF / 原始二进制 六种格式 | +| 内存导入 | 支持自动检测格式(COFF / TI-TXT / Intel HEX)和原始二进制导入 | +| 内存填充 | 用指定值填充连续内存区域 | +| 双击编辑 | 双击值单元格可修改内存数据,Enter 确认写入,Esc 取消 | +| 自动刷新 | 可配置间隔的定时自动刷新 | + +## 2. 架构 + +### 2.1 组件关系 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ VS Code 面板容器 │ +│ ┌───────────────────────────────────────────────────────┐ │ +│ │ Memory Viewer (WebviewView) │ │ +│ │ ┌─────────┐ ┌──────────┐ ┌──────────┐ ┌───────┐ │ │ +│ │ │ 工具栏 │ │ 数据表格 │ │ 状态栏 │ │ 对话框 │ │ │ +│ │ │(HTML/JS) │ │(渲染引擎) │ │ │ │ │ │ │ +│ │ └────┬────┘ └────┬─────┘ └────┬─────┘ └───┬───┘ │ │ +│ │ │ │ │ │ │ │ +│ │ └────────────┴─────────────┴─────────────┘ │ │ +│ │ postMessage │ │ +│ └───────────────────────┬───────────────────────────────┘ │ +│ │ │ +│ ┌───────────────────────┴───────────────────────────────┐ │ +│ │ MemoryViewerProvider (Extension) │ │ +│ │ - 消息路由与业务逻辑 │ │ +│ │ - 文件对话框交互 │ │ +│ │ - 自动刷新定时器管理 │ │ +│ └───────────────────────┬───────────────────────────────┘ │ +│ │ customRequest │ +│ ┌───────────────────────┴───────────────────────────────┐ │ +│ │ DSS Bridge (Adapter) │ │ +│ │ - DAP 协议封装 │ │ +│ │ - stdin/stdout JSON 通信 │ │ +│ └───────────────────────┬───────────────────────────────┘ │ +│ │ stdin/stdout │ +│ ┌───────────────────────┴───────────────────────────────┐ │ +│ │ dss-wrapper.js (Rhino) │ │ +│ │ - DSS API 调用 │ │ +│ │ - 中文路径处理 │ │ +│ │ - UTF-8 编码转换 │ │ +│ └───────────────────────┬───────────────────────────────┘ │ +│ │ │ +│ ┌───────────────────────┴───────────────────────────────┐ │ +│ │ DSS Debug Engine (Java) │ │ +│ │ - Memory.readData / writeData │ │ +│ │ - Memory.saveData / saveRaw │ │ +│ │ - Memory.load / loadRaw │ │ +│ │ - Memory.fill │ │ +│ └───────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 2.2 数据流 + +#### 读取流程 +``` +Webview.scroll/go → postMessage('read') → Provider._read() + → session.customRequest('readMemory') → Bridge → DSS + → DSS readData() → 返回 base64 → 解码为 byte[] + → postMessage('data') → Webview.render() +``` + +#### 导出流程 +``` +Webview.btnExport → showSaveDialog() → postMessage('export') + → Provider._export() → session.customRequest('exportMemory') + → Bridge → DSS saveData()/saveRaw() + → [中文路径: 写入临时文件 → renameTo/复制到目标路径] +``` + +#### 导入流程 +``` +Webview.btnImport → showOpenDialog() → postMessage('import') + → Provider._import() → session.customRequest('importMemory') + → Bridge → DSS load()/loadRaw() + → [中文路径: 复制到临时文件 → DSS 从临时文件加载 → 删除临时文件] +``` + +#### 写入流程(双击编辑) +``` +Webview commit() → postMessage('write', { address, hexData, callbackId }) + → Provider._write(address, hexData, callbackId) + → Buffer.from(hexData, "hex") → base64 编码 + → session.customRequest('writeMemory', { memoryReference, data: base64 }) + → adapter.writeMemoryRequest() → bridge.writeMemory() + → DSS writeMemory() → writeDataArray() → 逐字节写入 + → postMessage('writeResult', { callbackId, success }) → Webview 刷新 +``` + +## 3. 通信协议 + +### 3.1 Webview ↔ Extension(postMessage) + +#### Webview → Extension + +| 命令 | 参数 | 说明 | +|------|------|------| +| `read` | `address, count, prepend?, append?` | 读取内存 | +| `autoRefresh` | `enabled, interval` | 设置自动刷新 | +| `export` | `address, length, format` | 导出内存 | +| `import` | `address, format` | 导入内存 | +| `fill` | `address, length, value` | 填充内存 | +| `write` | `address, hexData, callbackId` | 写入单个值(双击编辑) | + +#### Extension → Webview + +| 命令 | 参数 | 说明 | +|------|------|------| +| `data` | `address, bytes, prepend?, append?` | 内存数据返回 | +| `error` | `text` | 错误信息 | +| `info` | `text` | 成功信息 | +| `loading` | `text` | 操作进行中(带脉冲动画) | +| `sessionEnded` | — | 调试会话结束,禁用按钮 | +| `sessionActive` | — | 调试会话启动,启用按钮 | +| `autoRead` | — | 触发自动读取 | +| `autoRefreshTick` | — | 自动刷新定时器触发 | +| `writeResult` | `callbackId, success, error?` | 写入结果返回 | + +### 3.2 Extension ↔ DSS(DAP Custom Request) + +| 请求名 | 参数 | 返回 | +|--------|------|------| +| `readMemory` | `memoryReference, count` | `{ data: base64 }` | +| `writeMemory` | `memoryReference, data (base64)` | `{ bytesWritten }` | +| `exportMemory` | `address, length, format, filePath` | `{ success, message/error }` | +| `importMemory` | `address, filePath, format` | `{ success, message/error }` | +| `fillMemory` | `address, length, value` | `{ success, message/error }` | + +## 4. 导出格式 + +| 格式标识 | 名称 | DSS API | IOFormat | 文件扩展名 | +|----------|------|---------|----------|-----------| +| `hex` | TI HEX (TI-DAT) | `saveData` | `IOFormat.HEX (1)` | .txt | +| `int` | 整数文本 | `saveData` | `IOFormat.INT (2)` | .txt | +| `long` | 长整数文本 | `saveData` | `IOFormat.LONG (3)` | .txt | +| `float` | 浮点文本 | `saveData` | `IOFormat.FLOAT (4)` | .txt | +| `coff` | COFF | `saveData` | `IOFormat.COFF (5)` | .out | +| `raw` | 原始二进制 | `saveRaw` | — | .bin | + +### 4.1 saveRaw 参数说明 + +``` +saveRaw(nPage, nAddress, sFilename, nLength, nTypeSize, bByteSwap) +``` + +- **nLength**: 单位是 target memory words(C66xx 32 位 = 4 字节),前端传字节数需除以 4 +- **nTypeSize = 32**: 按 32 位字处理字节序(C66xx 大端 → 主机小端自动转换) +- **bByteSwap = false**: 启用自动字节序转换 + +## 5. 导入格式 + +### 5.1 格式检测策略(优先级从高到低) + +1. 用户显式选择 `raw` 格式 → 使用 `loadRaw` 加载 +2. 文件扩展名为 `.bin` / `.dat` / `.raw` → 使用 `loadRaw` 加载 +3. 其他格式 → 使用 `load()` 自动检测(根据文件头识别 COFF / TI-TXT / Intel HEX) + +### 5.2 loadRaw 参数说明 + +``` +loadRaw(nPage, nAddress, sFilename, nTypeSize, bByteSwap) +``` + +- **nTypeSize = 32**: 与 saveRaw 的 nTypeSize=32 匹配,确保字节序一致 +- **文件大小**: DSS 自动确定,无需手动指定长度 + +## 6. 中文路径处理 + +### 6.1 问题根因 + +中文 Windows 下,DSS 的 Rhino 引擎和 Java 文件 I/O 存在两层编码问题: + +| 层级 | 问题 | 影响 | +|------|------|------| +| stdin/stdout 通信 | `InputStreamReader(System.in)` 默认使用 GBK 编码 | JSON 中的中文路径乱码 | +| 文件 I/O | `saveData`/`load` 内部用 GBK 编码文件名 | 创建的文件名乱码,或找不到文件 | + +### 6.2 解决方案 + +#### stdin/stdout 编码修复 + +```javascript +// stdin: 指定 UTF-8 编码 +var reader = new BufferedReader(new InputStreamReader(System.in, "UTF-8")); + +// stdout: 用 _writeUtf8() 直接写 UTF-8 字节 +function _writeUtf8(msg) { + var bytes = new java.lang.String(str).getBytes('UTF-8'); + System.out['write(byte[])'](bytes); + System.out['write(int)'](0x0A); + System.out.flush(); +} +``` + +#### 文件 I/O 中文路径 workaround + +**导出**: 先写入 ASCII 临时文件 → `File.renameTo` 或手动复制到目标路径 + +**导入**: 先复制源文件到 ASCII 临时文件 → DSS API 从临时文件加载 → 删除临时文件 + +```javascript +// 检测是否需要 workaround +var needsWorkaround = /[^\x00-\x7F]/.test(filePath); + +if (needsWorkaround) { + var tempFile = new java.io.File(tempDir, "dss_export_" + nanoTime + ".tmp"); + // ... DSS 写入临时文件 ... + // 然后移动到目标路径 + tempFile.renameTo(targetFile); // 或手动复制 +} +``` + +## 7. 前端交互设计 + +### 7.1 双击编辑机制 + +#### 交互流程 + +``` +用户双击 c-val 单元格 + → 创建 input 输入框,显示当前格式下的原始值 + → 用户修改值,按 Enter 或失焦触发 commit() + → commit() 处理: + 1. committed 标志防止 Enter 和 blur 双重触发 + 2. 根据当前格式(hex/uint/int/float/double)将输入值转换为字节数组 + 3. NaN 检查:十六进制输入无效时回退并提示 + 4. hex 格式特殊处理:字节序反转(hex 显示时已反转,编辑时需再反转回来) + 5. 构造 hexData 字符串 + 6. 通过 postMessage('write') 发送写入请求 + → 按 Esc 取消编辑,恢复原始文本 +``` + +#### 值格式转换规则 + +| 格式 | 输入示例 | 转换逻辑 | 字节序 | +|------|---------|---------|--------| +| hex | `000003E8` | 直接解析为字节 | 显示时反转,写入时再反转 | +| uint | `1000` | parseInt → 逐字节提取 | 小端序 | +| int | `-123` | parseInt → 补码转换 → 逐字节提取 | 小端序 | +| float | `3.14` | Float32Array 编码(4 字节) | 小端序 | +| double | `3.14159265` | Float64Array 编码(8 字节) | 小端序 | + +#### 数据流 + +``` +commit() → valueToHexBytes(value, format, sizeBytes) + → hexStr = hex字节拼接(hex 格式需反转字节序) + → postMessage('write', { address, hexData, callbackId }) + → Provider._write() + → Buffer.from(hexData, "hex") → base64 编码 + → session.customRequest('writeMemory', { memoryReference, data: base64 }) + → adapter.writeMemoryRequest() → bridge.writeMemory() + → DSS writeMemory() → writeDataArray() → 逐字节写入 + → 返回 writeResult { success, callbackId } + → Webview 更新状态栏 + autoRead 刷新 +``` + +### 7.2 callbackId 机制 + +双击编辑使用 `callbackId` 匹配异步写入结果: + +```javascript +var writeCallbackId = 0; +var writeCallbacks = {}; // { [cbId]: { addr, td, origBytes, newHex } } + +// commit() 时: +var cbId = ++writeCallbackId; +writeCallbacks[cbId] = { addr, td, origBytes, newHex }; +V.postMessage({ command: 'write', address, hexData, callbackId: cbId }); + +// 收到 writeResult 时: +var cb = writeCallbacks[m.callbackId]; +if (cb) { + if (m.success) { st.textContent = '写入成功: ' + cb.addr; } + else { st.textContent = '写入失败: ' + m.error; cb.td.textContent = cb.origBytes; } + delete writeCallbacks[m.callbackId]; +} +``` + +### 7.3 按钮状态管理 + +| 状态 | 读取/翻页 | 导出/导入/填充 | +|------|----------|---------------| +| 无调试会话 | 禁用 | 禁用 | +| 有调试会话 | 启用 | 启用 | +| 操作进行中 | 启用 | 禁用 | + +### 7.4 状态栏反馈 + +| 状态 | 样式 | 文本示例 | +|------|------|----------| +| 就绪 | 默认 | `就绪` | +| 加载中 | 默认 | `加载中...` | +| 导出中 | 脉冲动画 | `正在导出...` | +| 导入中 | 脉冲动画 | `正在导入...` | +| 提交修改中 | 脉冲动画 | `正在提交修改...` | +| 成功 | 绿色 | `导出成功: test.bin` | +| 写入成功 | 绿色 | `写入成功: 0x0C000000` | +| 错误 | 红色 | `导入失败: ...` | +| 写入失败 | 红色 | `写入失败: ...` | +| 会话结束 | 红色 | `会话结束` | + +### 7.5 滚动加载机制 + +- **向下滚动**: 当 `scrollTop + clientHeight >= scrollHeight - 2*ROW_H` 时,请求追加下一个 CHUNK(4096 字节) +- **向上滚动**: 当 `scrollTop <= 2*ROW_H` 且 `curAddr > 0` 时,请求回溯上一个 CHUNK +- **防抖**: `busy` 和 `scrollBusy` 标志防止并发请求 + +## 8. writeMemory 写入机制 + +### 8.1 调用链路 + +``` +Webview commit() + → postMessage('write', { address, hexData, callbackId }) + → Provider._write(address, hexData, callbackId) + → Buffer.from(hexData, "hex") → base64 编码 + → session.customRequest('writeMemory', { memoryReference: address, data: base64 }) + → adapter.writeMemoryRequest() → bridge.writeMemory(address, hexData) + → DSS writeMemory() → 将 hexData 字符串解析为 byteArray + → writeDataArray() 逐字节调用 writeData(page, addr, value, 8) + → 返回 { success, bytesWritten } + → Provider postMessage('writeResult', { callbackId, success, error? }) +``` + +### 8.2 writeDataArray 实现 + +```javascript +function writeDataArray(page, address, byteArr, typeSize) { + for (var i = 0; i < byteArr.length; i++) { + session.memory.writeData(page, address + i, byteArr[i], typeSize); + } +} +``` + +使用单值 `writeData(page, address, value, typeSize)` 逐字节写入,避免 Rhino 方法重载歧义(`write(byte[])` vs `write(int)`)。 + +### 8.3 错误处理 + +`writeMemory` 函数先尝试写入 `PROGRAM` 页面,失败后自动回退到 `DATA` 页面。 + +## 9. 安全策略 + +### 9.1 Content Security Policy (CSP) + +```html + +``` + +- 所有 `