Skip to content

从 EasyTool 到 ROMDEBUG:SM2258XT 恢复镜像的重建路径

  • 数据时间:2026-06-22 Asia/Shanghai
  • 研究对象:SMI SM2258XT EasyTool / EZTool 1.x 与 2.x 恢复导出、ROMDEBUG 路径、smi-recovery 恢复工具
  • 报告类型:存储芯片研究与恢复工程博客
  • 资料来源:smi-recovery 仓库文档、IDA 静态分析记录、真实样本验证记录、Rust core/CLI/iSCSI 源码

样本边界:

  • 本文主测试盘为 SM2258XT + SK hynix 16nm TLC,固件版本 Q1215B。
  • V2 格式阳性参照为 SM2258XT + Intel B16A TLC。
  • 因此本文的 ROMDEBUG 和恢复质量结论只对上述样本链路负责;其他 SMI 主控、其他 NAND 制程或其他 EasyTool 家族需要重新建立 profile 和样本验证。

一句话

SM2258XT EasyTool 的恢复结果不是一个可以直接顺序拼接的大镜像,而是一组稀疏的 DataBankImageLBAInfo.idx 告诉我们每个 4 KiB payload 属于哪个逻辑 LBA,BankImage.bin 按 record 顺序保存 payload。V1/V2 的官方导出流程差异很大,尤其 ROMDEBUG 会改变固件侧的读路径、cache/status 状态和 host 后处理;但导出完成后,离线重建的核心规则反而很简单:ImageLBAInfo.idx 的 record 序号对应 BankImage.binrecord_index * 4096 偏移。

smi-recovery 的设计就是围绕这个事实建立一条只读恢复链:先把碎片化的 idx 编译成本地 .smiidx 索引,再用同一个 core reader 支撑 sourceread-lbaexport --sparse 和只读 iSCSI target。与经典 SM2258XT 镜像助手相比,它最大的实际优势不是“会拼”,而是不会把缺失区和全零 chunk 实打实写满目标盘。在同一套 HDD -> SSD 硬件上,经典镜像助手写同类镜像需要约 4-5 小时;smi-recovery --sparse 在本次样本上生成逻辑 447G、实际占用约 104G 的镜像只用了约 13 分钟。

EasyTool 官方恢复导出的共同底层

EasyTool 的恢复目录通常长这样:

text
RecoveryMode/.../
  Info.ini
  Image/
    ImageInfo.ini
    DataBank_0/
      ImageLBAInfo.idx
      BankImage.bin
      LBAIndexTable.bin      # V2.x 常见
      DebugLBAInfo.idx       # V2.x 常见
    DataBank_1/
      ...

真正可用于离线重建的是每个 DataBank_N 里的两类文件:

文件作用离线重建地位
ImageLBAInfo.idx0x20 字节 header 后面是一组 u64 little-endian logical sector LBA record权威 LBA map
BankImage.bin按 append 顺序保存 4 KiB payload chunk权威 payload
LBAIndexTable.binV2.x 的 bank-local slot 到 idx record offset 的校验表诊断/校验,不替代 idx
DebugLBAInfo.idxV2.x 每条 record 的 DataBlock/page/CE/CH/PN 来源诊断,不改变读取规则

核心公式只有一条:

text
ImageLBAInfo.records[i] = 该 4 KiB chunk 的起始逻辑 512B sector LBA
BankImage.bin offset    = i * 4096
chunk size              = 4096 bytes = 8 sectors

这个规则很容易被写错:不能用逻辑 LBA 值反推 BankImage.bin 偏移。V1 的 record 在单 bank 内通常按 LBA 单调递增,V2 的 record 经常按 DataBlock discovery 顺序乱序出现;但两者都一样,payload 偏移来自 record ordinal,而不是 LBA 数值。

V1:H2F 驱动的 bank/local-slot 导出

V1.2.x 的恢复流程更像“按 bank-local slot 扫描”。EasyTool 先读取或生成 H2F 中间表,再用每个 32-bit H2F entry 决定这个 slot 是否能读、从哪个 physical block/page/plane/channel 读取。

V1 的导出路径可以拆成四步:

  1. 读取 ImageInfo.ini 的 bank-chain hint,包括 TotalBankStartBankLastBankNextBank_<id>。这条链适合做诊断,但离线 reader 不应该只相信它;目录里的 Image/DataBank_* 才是实际输入。
  2. 对每个 bank 读取 H2F/H2F_<bank>.bin,每条 32-bit entry 对应一个 bank-local 4 KiB slot。entry & 0xc0000000 != 0entry == 0x3fffffff 等状态会让 host 跳过该 slot。
  3. 对可接受的 H2F entry,host 解出 CurrBlkPGCKPNCECH,再通过 F0 0A vendor read command 读取 9 个 sector。前 0x1000 字节成为最终 payload,尾部额外 sector 用于状态/metadata。
  4. 成功读到一个 chunk 后,EasyTool append 4 KiB 到 BankImage.bin,并把对应 logical LBA 写入内存 idx;完成当前 bank 后一次性写出 ImageLBAInfo.idx

V1 的 ImageLBAInfo.idx header 中 0x10/0x18 通常是实际 first/last valid LBA。由于扫描顺序是 bank-local slot 递增,单个 bank 内的记录大多单调递增,中间会有 gap。

这里有一个很重要的恢复经验:V1 目录里 H2F 文件数量少于 DataBank 数量不一定是 Rust parser 的问题,也不一定是导出目录坏了。V1 host 在 H2F table pointer 是 0xffff 时会直接跳过该 bank,不创建 H2F_<bank>.bin。所以这类缺口要被记录为 EasyTool 导出阶段没有拿到有效 H2F source,而不是在离线阶段胡乱补洞。

V2:DataBlock/DPS 驱动的导出

V2.x 的恢复流程明显更复杂。它不是简单按 bank-local slot 走,而是先通过 DBS/DPS、DataBlock candidate 和 footer 逐层筛选,然后再 append 到最终 ImageLBAInfo.idx + BankImage.bin

V2 的主链路大致是:

  1. 进入 MPISP/ROMDEBUG 相关 code mode,采集 DBS,生成 TempBlockType.bin,选择 cache/index block。
  2. 采集 DPS,生成 CacheBlockDPSType.bin,回退到 LastValidPage,并读取 NewestPageInfo.bin
  3. NewestPageInfo + 0x10000 解析 block-type word,生成 corrected-block-type table 和 DataBlock 候选列表。
  4. 再跑一轮 DBS,归档为 CorrectedBlockType.bin,对候选 block 做二次筛选。
  5. 对每个 DataBlock 生成 DataBlockInfoDPS_%d.binDataBlockDPS_%d.txt。其中 .bin 是 0x40-byte record 流,包含 CurrBlkCurrPGCurrPNCurrCECurrCH 等 physical coordinate。
  6. backup path 反向扫描这些 0x40-byte record,读取 flash payload page,再从 page footer 解析 (DataBank id, bank-local 4K slot)
  7. footer entry 通过 range、duplicate 和 table 校验后,才会 append ImageLBAInfo.idxBankImage.binLBAIndexTable.binDebugLBAInfo.idx

V2 的 ImageLBAInfo.idx header 中 0x10/0x18 通常是完整 bank range:bank_id << 17(bank_id + 1) << 17,不是实际 first/last valid record。它还会维护 LBAIndexTable.bin,把 bank-local 4K slot 指回 ImageLBAInfo.idx 内的 record offset,用于防止 duplicate。

所以 V2 的低覆盖不能简单归因成“拼接器错了”。DataBlockInfoDPS 只是候选源,最终是否进入 image 还要经过 flash read、footer 解析、duplicate/range 过滤。静态分析里没有证据表明 host 会在大量 flash read 失败后静默继续 append;真正会让 image 变小的,往往是 candidate 不足、footer 无效、duplicate slot 或 range guard。

ROMDEBUG 为什么关键

ROMDEBUG 最容易被误解成“另一个 mode switch”。对 V1.2.6 来说,这个理解不够。V1 的 ROMDEBUG button 做了完整的 host-to-firmware runtime 初始化和状态回写。

V1 host 的 ROMDEBUG 顺序可以概括为:

text
load ParamTable + SeedTable + MPISP2258_ROMDEBUG.bin
  -> 写 5 sector Param/Seed payload 到 firmware runtime
  -> 下载 ROMDEBUG body
  -> 读取 system-block status pages 并回写 2-bit 状态图
  -> 调用 SWPtest.dll 重建 system index
  -> 扫描 cache info,写 TempCacheInfo.bin
  -> 把 TempCacheInfo.bin 尾部 64 KiB 回写到 firmware runtime RAM

ParamTable 和 SeedTable 不是 UI 上的形式要求。host 会把包含 Param/Seed 的 payload 写入 runtime 地址,ROMDEBUG firmware init 再把 Param 开头配置搬到 runtime config,把 SeedTable 搬到 seed/state 区。如果设备侧能从 system area 读到更新数据,ROMDEBUG 还会再刷新 seed 区。

固件侧更关键的是 cache/status 闭环。host 生成的 TempCacheInfo.bin 不只是诊断文件,尾部 64 KiB 会被写回到 ROMDEBUG firmware 的 runtime RAM。后续 firmware init 和 NAND read request 会消费同一段状态区:

地址/区域已确认作用
0x40070000cache/status runtime 区,包含 per-entry 状态/映射表
0x4007D3181 KiB 2-bit state bitmap,对应 TempCacheInfo.bin tail64 内的 +0xD318
0x40068000entry/list 区,参与 cache-status remap
0x10005DF44 个 state handler:direct/no-op、lower lookup、upper lookup、split lookup

这套表不是只在启动时展开一次。内部 NAND read helper 会先经过 cache/status remap normalize,再真正下发读请求。state 0 是 no-remap 快速路径,state 1/2/3 会走不同的 remap lookup。也就是说,V1 ROMDEBUG 的 post-load cache rebuild 会改变后续读取 H2F/BankImage payload 的条件。

真实样本也支持这一点:普通 V1 MPISP 导出虽然有大量 BankImage.bin 字节,但关键 payload 表现为高熵、不可识别;手动进入 V1 ROMDEBUG 后,早期 DataBank 已能读出 MBR、NTFS boot sector 和 $MFTFILE0 记录。这个差异说明问题不只是“镜像助手怎么拼”,而是 EasyTool 导出阶段是否让设备处在可用的 ROMDEBUG/cache/status 状态。

需要保留边界:当前分析确认 ROMDEBUG firmware 里存在 read-retry/read-threshold 参数扫描,也确认 cache/status remap 会进入读请求;但没有把 ECC/LDPC soft decode 的完整算法落名。所以文章不能把它写成“已证明 LDPC soft decode”,只能写成“ROMDEBUG 改变了固件侧读路径和状态修正路径”。

为什么不能按导出总量投票

恢复现场里很容易被总字节数误导。一个导出看起来有 200G+,不代表它是更好的恢复源;另一个导出只有几十 G,也不代表它完全没有真实文件系统数据。

真实样本里出现过三类现象:

导出表面现象当前判定
V1 MPISP总量大,payload 高熵,关键 LBA 不像 MBR/NTFS结构可组织,但 payload 需要复核,不作为 primary source
V1 ROMDEBUG早期 DataBank 可读出 MBR、NTFS boot sector、FILE0当前 primary source
V2 ROMDEBUG/HY16局部能看到 NTFS/MFT 结构,但 LBA0 错、LBA2048 缺,与 V1 ROMDEBUG 同 LBA 大量不一致错误/低覆盖导出,只作负面对照

这也是 smi-recovery 的一个原则:工具不做“投票补洞”。如果一个来源已经被判为错误导出,就不拿它覆盖 primary source,也不拿它补 sparse gap。不同导出的价值首先来自证据级别,而不是文件夹大小。

smi-recovery 的设计

smi-recovery 是 Rust 写的 SM2258XT EasyTool 恢复工具,当前范围只声明 SM2258XT EzTool 1.x 和 2.x。它的架构很刻意:

text
recovery-core   = 唯一事实来源,解析 idx、BankImage、profile、compiled index、reader、export
recovery-cli    = 薄命令行接口,负责参数和报告
recovery-iscsi  = 薄只读块设备前端,负责 iSCSI/SCSI 协议

CLI 和 iSCSI 都不能实现第二套 BankImage 查找算法。所有入口最终都走 core 的 VirtualBlockDevice

text
read_sectors(lba, count) -> present/missing report + bytes

缺失扇区默认读为 0。这个行为符合 sparse image,也能让文件系统扫描工具继续往下走。需要更严格的场景可以用 --strict-missing 让导出在缺口处失败。

常用命令可以按恢复流程分层:

阶段命令作用
预检inspect / index-summary / compare-index不急着读 payload,先看 idx、覆盖、重复、版本差异
建索引compile-index / compiled-index-info把几千个碎片 idx 合并成本地 .smiidx
可用性判断source / probe / validate检查 LBA0、LBA2048、MBR/GPT、NTFS BPB、MFT hint、zero/entropy
导出规划export-plan只生成计划、缺失区、source metadata 诊断,不写 image
输出export --sparse写出 sparse raw image
交互恢复target暴露只读 iSCSI LUN 给 Windows/R-Studio 等工具

compile-index 是大样本体验的关键。真实导出经常在 SMB 或机械盘上,直接反复遍历几千个 ImageLBAInfo.idx 小文件很慢。.smiidx 只保存 index 和 bank metadata,不复制 BankImage.bin payload;它可以放在本地 SSD,删除后随时重建。之后 sourceread-lbaexportexport-plantarget 都可以复用这个索引,只在真正读取 payload 时打开对应 BankImage.bin

export-plan 还会把 EasyTool 源 metadata 一起纳入诊断:V1 会统计 Info.iniH2FTable.binH2F/H2F_*.bin、H2F candidate/status/sentinel;V2 会统计 TempBlockTypeCacheBlockDPSTypeNewestPageInfoCorrectedBlockTypeDataBlockInfoDPS、DPS pair 和 raw DataBlock 文件夹。这样用户看到的不只是“能不能导出”,还包括“为什么这个导出覆盖少”。

sparse 导出为什么快

经典镜像助手通常给恢复软件一个 dense raw image:逻辑磁盘多大,就把目标文件写成多大;缺失区域也会写 0。这对恢复软件最兼容,但在大面积 sparse 的恢复场景里非常浪费时间,尤其是源在 HDD/SMB、目标在 SSD 时,瓶颈常常变成大量无意义的零写入。

smi-recovery export --sparse 的策略不同:

  1. 对缺失 extent,不写零,只移动文件 offset。
  2. 对已经读到的全零 payload chunk,也跳过写入。
  3. 对连续的非零 chunk 分组写入,减少小块写。
  4. 最后用 set_len(image_size) 保持逻辑镜像大小。
  5. Windows 上显式调用 sparse file 标记;非 Windows 文件系统依靠 seek hole 行为。

因此输出文件可以同时满足两个条件:

text
逻辑大小:仍是原盘/目标镜像大小,例如 447G
实际占用:只接近非零、可恢复 payload,例如本次约 104G

这个差异解释了实测时间:经典 SM2258XT 镜像助手在同样 HDD -> SSD 硬件上写同类镜像需要约 4-5 小时;smi-recovery --sparse 本次约 13 分钟完成。它不是神奇地“读取更多数据”,而是少写了大量不携带信息的 0,并且通过 compiled index 避免反复扫小文件。

性能结论要带两个限制:

  • 13 分钟是本次样本和本次硬件/文件系统上的实测,不是所有恢复场景的固定承诺。
  • sparse 文件需要目标文件系统和后续工具正确支持 hole;如果某些工具或拷贝方式把 sparse 文件重新实体化,耗时和占用会回到 dense image 的级别。

与经典 SM2258XT 镜像助手的边界

经典镜像助手仍有参照价值,尤其是在它支持的 2.x 导出格式上,可以作为黑盒对照。但它不应该成为格式事实来源。

关键区别是:

维度经典 SM2258XT 镜像助手smi-recovery
事实来源黑盒导入/合并结果直接读取 ImageLBAInfo.idx + BankImage.bin
V1 支持旧文档口径偏 2.x,V1 失败不能证明 V1 bank 坏明确支持 SM2258XT EzTool 1.x/2.x profile
缺失区处理倾向 dense raw image,缺失区写零--sparse seek 过缺失区和全零 chunk
大样本索引反复扫源目录,进度和可诊断性弱.smiidx 本地索引,可复用
诊断结果导向,难以解释覆盖缩水sourceprobeexport-plan 输出覆盖、缺失、metadata 和限制
交互恢复主要产出 raw image可产出 sparse image,也可直接暴露只读 iSCSI LUN

更重要的是证据边界:如果旧助手把 V1.2.x 输出拼成空白或不可扫描的大 BIN,这不能直接证明 V1.2.x 的 DataBank 无效。它可能只是用 2.x 假设解释了 V1 header 或 companion files。smi-recovery 的路径是直接读 idx record 和 BankImage payload,再用小范围 LBA 检查确认 MBR/NTFS/MFT 是否真的存在。

当前结论

这轮 SM2258XT 恢复研究的主结论不是某个命令,而是一条证据链:

  1. V1/V2 EasyTool 官方导出流程不同,但最终离线重建原语相同:idx record ordinal 到 BankImage 4 KiB payload ordinal。
  2. ROMDEBUG 对 V1 样本非常关键,因为它不只是 mode switch,而是 Param/Seed 加载、system index rebuild、cache/status 回写和固件侧 request normalize 的组合。
  3. 大小不能投票。普通 V1 MPISP、V2/HY16、V1 ROMDEBUG 必须按 payload 质量和同 LBA 一致性分级;当前 primary source 是 V1 ROMDEBUG。
  4. smi-recovery 把恢复链拆成 core reader、compiled index、diagnostic plan、sparse export 和只读 iSCSI target,避免 CLI 或协议层复制解析逻辑。
  5. sparse 导出是产品体验上的决定性差异:同样逻辑 447G 镜像,本次实际占用约 104G,导出耗时约 13 分钟;旧助手在同硬件上约 4-5 小时。

对恢复工具来说,真正的“快”不是跳过验证,而是少做没有信息量的 IO;真正的“准”也不是把多个坏导出混在一起投票,而是明确每个来源的证据级别,把可读的 primary source 只读地暴露出来。