外观
从 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 的恢复结果不是一个可以直接顺序拼接的大镜像,而是一组稀疏的 DataBank:ImageLBAInfo.idx 告诉我们每个 4 KiB payload 属于哪个逻辑 LBA,BankImage.bin 按 record 顺序保存 payload。V1/V2 的官方导出流程差异很大,尤其 ROMDEBUG 会改变固件侧的读路径、cache/status 状态和 host 后处理;但导出完成后,离线重建的核心规则反而很简单:ImageLBAInfo.idx 的 record 序号对应 BankImage.bin 的 record_index * 4096 偏移。
smi-recovery 的设计就是围绕这个事实建立一条只读恢复链:先把碎片化的 idx 编译成本地 .smiidx 索引,再用同一个 core reader 支撑 source、read-lba、export --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.idx | 0x20 字节 header 后面是一组 u64 little-endian logical sector LBA record | 权威 LBA map |
BankImage.bin | 按 append 顺序保存 4 KiB payload chunk | 权威 payload |
LBAIndexTable.bin | V2.x 的 bank-local slot 到 idx record offset 的校验表 | 诊断/校验,不替代 idx |
DebugLBAInfo.idx | V2.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 的导出路径可以拆成四步:
- 读取
ImageInfo.ini的 bank-chain hint,包括TotalBank、StartBank、LastBank和NextBank_<id>。这条链适合做诊断,但离线 reader 不应该只相信它;目录里的Image/DataBank_*才是实际输入。 - 对每个 bank 读取
H2F/H2F_<bank>.bin,每条 32-bit entry 对应一个 bank-local 4 KiB slot。entry & 0xc0000000 != 0、entry == 0x3fffffff等状态会让 host 跳过该 slot。 - 对可接受的 H2F entry,host 解出
CurrBlk、PG、CK、PN、CE、CH,再通过F0 0Avendor read command 读取 9 个 sector。前 0x1000 字节成为最终 payload,尾部额外 sector 用于状态/metadata。 - 成功读到一个 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 的主链路大致是:
- 进入 MPISP/ROMDEBUG 相关 code mode,采集 DBS,生成
TempBlockType.bin,选择 cache/index block。 - 采集 DPS,生成
CacheBlockDPSType.bin,回退到 LastValidPage,并读取NewestPageInfo.bin。 - 从
NewestPageInfo + 0x10000解析 block-type word,生成 corrected-block-type table 和 DataBlock 候选列表。 - 再跑一轮 DBS,归档为
CorrectedBlockType.bin,对候选 block 做二次筛选。 - 对每个 DataBlock 生成
DataBlockInfoDPS_%d.bin和DataBlockDPS_%d.txt。其中.bin是 0x40-byte record 流,包含CurrBlk、CurrPG、CurrPN、CurrCE、CurrCH等 physical coordinate。 - backup path 反向扫描这些 0x40-byte record,读取 flash payload page,再从 page footer 解析
(DataBank id, bank-local 4K slot)。 - footer entry 通过 range、duplicate 和 table 校验后,才会 append
ImageLBAInfo.idx、BankImage.bin、LBAIndexTable.bin和DebugLBAInfo.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 RAMParamTable 和 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 会消费同一段状态区:
| 地址/区域 | 已确认作用 |
|---|---|
0x40070000 | cache/status runtime 区,包含 per-entry 状态/映射表 |
0x4007D318 | 1 KiB 2-bit state bitmap,对应 TempCacheInfo.bin tail64 内的 +0xD318 |
0x40068000 | entry/list 区,参与 cache-status remap |
0x10005DF4 | 4 个 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 和 $MFT 的 FILE0 记录。这个差异说明问题不只是“镜像助手怎么拼”,而是 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,删除后随时重建。之后 source、read-lba、export、export-plan 和 target 都可以复用这个索引,只在真正读取 payload 时打开对应 BankImage.bin。
export-plan 还会把 EasyTool 源 metadata 一起纳入诊断:V1 会统计 Info.ini、H2FTable.bin、H2F/H2F_*.bin、H2F candidate/status/sentinel;V2 会统计 TempBlockType、CacheBlockDPSType、NewestPageInfo、CorrectedBlockType、DataBlockInfoDPS、DPS pair 和 raw DataBlock 文件夹。这样用户看到的不只是“能不能导出”,还包括“为什么这个导出覆盖少”。
sparse 导出为什么快
经典镜像助手通常给恢复软件一个 dense raw image:逻辑磁盘多大,就把目标文件写成多大;缺失区域也会写 0。这对恢复软件最兼容,但在大面积 sparse 的恢复场景里非常浪费时间,尤其是源在 HDD/SMB、目标在 SSD 时,瓶颈常常变成大量无意义的零写入。
smi-recovery export --sparse 的策略不同:
- 对缺失 extent,不写零,只移动文件 offset。
- 对已经读到的全零 payload chunk,也跳过写入。
- 对连续的非零 chunk 分组写入,减少小块写。
- 最后用
set_len(image_size)保持逻辑镜像大小。 - 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 本地索引,可复用 |
| 诊断 | 结果导向,难以解释覆盖缩水 | source、probe、export-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 恢复研究的主结论不是某个命令,而是一条证据链:
- V1/V2 EasyTool 官方导出流程不同,但最终离线重建原语相同:idx record ordinal 到 BankImage 4 KiB payload ordinal。
- ROMDEBUG 对 V1 样本非常关键,因为它不只是 mode switch,而是 Param/Seed 加载、system index rebuild、cache/status 回写和固件侧 request normalize 的组合。
- 大小不能投票。普通 V1 MPISP、V2/HY16、V1 ROMDEBUG 必须按 payload 质量和同 LBA 一致性分级;当前 primary source 是 V1 ROMDEBUG。
smi-recovery把恢复链拆成 core reader、compiled index、diagnostic plan、sparse export 和只读 iSCSI target,避免 CLI 或协议层复制解析逻辑。- sparse 导出是产品体验上的决定性差异:同样逻辑 447G 镜像,本次实际占用约 104G,导出耗时约 13 分钟;旧助手在同硬件上约 4-5 小时。
对恢复工具来说,真正的“快”不是跳过验证,而是少做没有信息量的 IO;真正的“准”也不是把多个坏导出混在一起投票,而是明确每个来源的证据级别,把可读的 primary source 只读地暴露出来。