文件系统-从 vmdk 镜像解析与导出文件
平淡无奇的周一, 老板突然说 7z 怎么无法在 esxi host 解析 vmdk 呢? 于是有这个这个工具的探索. 实现起来倒是不复杂, 只因为 AI 可以闭环验证, 很快就完成了
一、问题背景
在日常运维中,我们经常会遇到以下场景:
- 虚拟机无法启动:操作系统损坏,但数据还在磁盘上
- ESXi 环境受限:不能安装第三方工具
- 快照数据恢复:需要理解 VMware 快照中的增量数据
传统方案需要挂载虚拟磁盘,这要求 root 权限和特定的文件系统驱动。但实际上,文件系统的本质就是按照特定规则组织的二进制数据。理解这些规则,是进行数据恢复、取证分析、存储系统开发的基础。
本文将详细讲解:
- 分区表结构:MBR 和 GPT 的二进制布局
- 文件系统原理:ext4、NTFS、XFS 的核心数据结构
- LVM 机制:逻辑卷管理器的元数据组织
- VMware SESparse:快照格式的增量存储原理
二、磁盘数据的层次结构
要理解磁盘数据恢复,首先需要理解数据的层次组织:
┌───────────────────────────────────────────────────────────┐
│ 磁盘镜像 (Raw Disk Image) │
└────────────────────────────┬──────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────┐
│ 分区表层 (MBR / GPT) │
│ → 定义分区的起始偏移和大小 │
└────────────────────────────┬──────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────┐
│ LVM 层 (可选) │
│ → 物理卷 → 卷组 → 逻辑卷 │
└────────────────────────────┬──────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────┐
│ 文件系统层 (NTFS / ext4 / XFS) │
│ → 元数据结构 → 目录树 → 文件数据 │
└───────────────────────────────────────────────────────────┘
关键魔数(Magic Numbers)
文件系统检测依赖特定位置的魔数签名:
| 类型 | 魔数 | 位置 | 说明 |
|---|---|---|---|
| MBR | 0x55AA |
偏移 510 | 引导扇区签名 |
| GPT | EFI PART |
LBA 1 | GPT 头部签名 |
| LVM | LABELONE |
分区偏移 +512 | LVM2 物理卷标签 |
| ext4 | 0xEF53 |
分区偏移 +1080 | Superblock 魔数 |
| XFS | 0x58465342 |
分区偏移 +0 | ‘XFSB’ 大端序 |
| NTFS | NTFS |
分区偏移 +3 | OEM ID |
三、分区表结构
3.1 MBR (Master Boot Record)
MBR 位于磁盘第一个扇区(512 字节),包含 4 个分区条目。
核心信息:每个分区条目记录分区类型和起始 LBA
分区偏移 = 起始 LBA × 512
常见分区类型:0x07 NTFS、0x83 Linux、0x8E LVM、0xEE GPT 保护分区
3.2 GPT (GUID Partition Table)
GPT 是现代标准,支持超过 2TB 磁盘和 128+ 个分区。
核心信息:每个分区条目记录类型 GUID、起始/结束 LBA、分区名称
分区偏移 = 起始 LBA × 512(64 位 LBA,支持超大磁盘)
四、ext4 文件系统结构
ext4 是 Linux 最常用的文件系统,采用块组 + inode + extents三层结构。
4.1 整体架构
Superblock (偏移 1024) → 块组描述符表 → 块组 0/1/2...
│
┌─────────────┴─────────────┐
▼ ▼
Inode Table Data Blocks
│
▼
Extents (连续块映射)
4.2 核心概念
| 概念 | 作用 | 关键点 |
|---|---|---|
| Superblock | 文件系统全局信息 | 魔数 0xEF53,块大小 = 1024 << log值 |
| 块组 | 将磁盘分区管理 | 每组有独立的 inode 表和数据块 |
| Inode | 文件元数据 | 根目录固定为 inode 2 |
| Extents | 数据块映射 | 魔数 0xF30A,B+ 树结构 |
4.3 Inode 定位
块组号 = (inode_num - 1) / inodes_per_group
组内索引 = (inode_num - 1) % inodes_per_group
4.4 Extents 树
ext4 用 extents 描述文件数据的物理位置,每个 extent 表示一段连续的物理块:
Extent = (逻辑块号, 连续块数, 物理起始块)
物理偏移 = 物理起始块 × 块大小
大文件使用 B+ 树组织多个 extents(depth > 0 时有索引节点)。
五、NTFS 文件系统结构
NTFS 是 Windows 文件系统,核心设计是一切皆属性——文件名、时间戳、数据都是属性。
5.1 整体架构
Boot Sector → MFT (Master File Table) → 属性列表 → 数据
│
┌───────────┴───────────┐
▼ ▼
MFT 记录 0: $MFT MFT 记录 5: 根目录
(MFT 自身) (目录索引)
5.2 核心概念
| 概念 | 作用 | 关键点 |
|---|---|---|
| Boot Sector | 分区元信息 | OEM ID NTFS,记录 MFT 位置 |
| MFT | 文件索引表 | 每条记录 1024 字节,签名 FILE |
| 属性 | 存储一切信息 | $DATA 存数据,$FILE_NAME 存文件名 |
| 簇 | 分配单位 | MFT 偏移 = MFT_LCN × 簇大小 |
5.3 MFT 记录
每个文件/目录对应一条 MFT 记录,包含多个属性:
| 属性类型 | 名称 | 说明 |
|---|---|---|
| 0x10 | $STANDARD_INFORMATION | 时间戳、权限 |
| 0x30 | $FILE_NAME | 文件名 |
| 0x80 | $DATA | 文件数据 |
| 0x90 | $INDEX_ROOT | 目录索引 |
5.4 驻留 vs 非驻留
NTFS 的独特优化:小文件数据直接存在 MFT 记录内(约 700-900 字节以内)。
$DATA 属性
│
├── 驻留 (flag=0): 数据在 MFT 记录内,无需额外 I/O
│
└── 非驻留 (flag=1): 数据在外部簇,通过 Data Runs 定位
5.5 Data Runs(数据运行)
Data Runs 用变长编码描述非驻留数据的物理位置:
编码格式: [Header][Length][Offset] ...
Header = (offset_size << 4) | length_size
Offset 是累积偏移: LCN_n = LCN_{n-1} + offset_n
Offset = 0 表示稀疏区域(虚拟空间)
示例:31 01 02 03 → 1 个簇,起始于 LCN 0x030201
六、XFS 文件系统结构
XFS 是 CentOS/RHEL 默认文件系统,最大特点是大端序和AG 分区管理。
6.1 整体架构
Superblock (偏移 0) → AG 0 / AG 1 / AG 2 ...
│
┌────────────┴────────────┐
▼ ▼
Inode Table Data Blocks
│
▼
128-bit Extents (大端序)
6.2 核心概念
| 概念 | 作用 | 关键点 |
|---|---|---|
| Superblock | 文件系统元信息 | 魔数 XFSB,偏移 0(不是 1024) |
| AG | 分配组 | 每个 AG 独立管理,支持并行 I/O |
| Inode 编码 | inode 号含 AG 信息 | inode = (ag_num << ag_ino_log) | offset |
| Extents | 128 位结构 | 全部大端序 |
6.3 fsblock 的 AG 编码(最大陷阱)
XFS 的 fsblock 高位编码了 AG 号,不能直接乘块大小:
错误:disk_offset = fsblock × block_size
正确:
ag_num = fsblock >> ag_blk_log
ag_blk = fsblock & ((1 << ag_blk_log) - 1)
actual_block = ag_num × ag_blocks + ag_blk
disk_offset = actual_block × block_size
为什么? 因为 ag_blocks 不一定等于 2^ag_blk_log(最后一个 AG 可能不完整)
七、LVM 逻辑卷管理
Linux 常在分区之上使用 LVM,需要先解析 LVM 元数据找到逻辑卷位置。
7.1 LVM 层次结构
物理分区 → 物理卷 (PV) → 卷组 (VG) → 逻辑卷 (LV) → 文件系统
7.2 核心概念
| 概念 | 魔数/签名 | 说明 |
|---|---|---|
| PV 标签 | LABELONE |
位于分区偏移 512 字节 |
| 元数据头 | LVM2 |
包含文本格式的配置 |
| extent_size | - | 每个 extent 的扇区数 |
| pe_start | - | 物理 extent 起始扇区 |
7.3 逻辑卷偏移计算
LVM 元数据是人类可读的文本格式,关键信息:
lv_offset = (pe_start + stripe_offset × extent_size) × 512
找到 LV 偏移后,就可以在该位置解析 ext4/XFS 文件系统。
八、VMware SESparse 快照格式
VMware 在 VMFS6 上使用 SESparse 格式存储快照,这是一种增量存储格式。
8.1 基本原理
Base Disk: [sector 0][sector 1][sector 2]...[sector N]
↑ ↑
SESparse: [modified] [unallocated → 读 base]
只有被修改的扇区存储在 SESparse 文件中,未修改的数据从 base disk 读取。
8.2 核心概念
| 概念 | 说明 |
|---|---|
| 魔数 | 0xCAFEBABE |
| 粒度 | grain_size,数据分配的最小单位 |
| L1/L2 表 | 两级索引定位数据 |
| 状态位 | 0x0=未分配,0x2=零,0x3=已分配 |
8.3 透明合并
通过封装类提供统一接口,上层文件系统工具无需关心快照逻辑:
读取请求 → 查 L1/L2 表
├── 已分配 → 从 SESparse 读取
├── 零粒度 → 返回全零
└── 未分配 → 从 Base Disk 读取
九、文件数据索引机制
三大文件系统采用不同策略定位文件数据的物理位置。
9.1 三者对比
| 特性 | ext4 | NTFS | XFS |
|---|---|---|---|
| 索引结构 | Extents B+ 树 | Data Runs 链表 | 128-bit Extents |
| 小文件优化 | 无 | 驻留存储(≤900B) | inline 存储 |
| 稀疏文件 | 未初始化 extent | offset=0 | 未分配 extent |
| 字节序 | 小端 | 小端 | 大端 |
9.2 ext4 - Extents B+ 树
inode → Extent Header (magic=0xF30A)
│
┌─────────┴─────────┐
▼ ▼
depth=0 (叶子) depth>0 (索引)
│ │
▼ ▼
Extent Entry Index Entry → 递归
(逻辑块, 块数,
物理起始块)
查找逻辑块 N:遍历 extents,找到包含 N 的 extent,计算物理块
9.3 NTFS - Data Runs
MFT Record → $DATA 属性
│
┌──────────┴──────────┐
▼ ▼
驻留 (flag=0) 非驻留 (flag=1)
数据在 MFT 内 Data Runs 链表
(≤900 bytes) (累积偏移编码)
关键:Data Runs 使用累积偏移,LCN_n = LCN_{n-1} + offset_n
9.4 XFS - 128 位 Extent
inode → 128-bit Extent (大端序)
├── 逻辑块偏移 (54 bits)
├── 物理起始块 (52 bits, 含 AG 编码)
└── 块数 (21 bits)
关键陷阱:fsblock 高位是 AG 号,必须先 AG 解码再计算物理偏移
十、目录遍历与文件查找
以查找 /etc/hosts 为例,看三大文件系统如何定位文件。
10.1 通用流程
根目录 → 查找 "etc" → 获取 etc 的 inode → 查找 "hosts" → 读取数据
10.2 三者对比
| 步骤 | ext4 | NTFS | XFS |
|---|---|---|---|
| 根目录 | 固定 inode 2 | 固定 MFT 记录 5 | Superblock 中读取 |
| 目录结构 | dirent 链表 | B+ 树索引 | shortform/block/btree |
| 文件名匹配 | 顺序遍历 | B+ 树查找 | 顺序或 hash |
| inode 定位 | 块组+组内索引 | MFT 记录号×1024 | AG 解码 |
10.3 目录格式
ext4:目录是特殊文件,内容是 dirent 链表(inode + 文件名)
NTFS:使用 B+ 树索引,小目录在 $INDEX_ROOT,大目录在 $INDEX_ALLOCATION
XFS:根据大小自动选择格式
- shortform:小目录,数据在 inode 内
- block:中等目录,单个数据块
- leaf/node:大目录,B+ 树结构
10.4 安全考虑
从磁盘读取的文件名可能包含危险字符,必须过滤:
- 路径分隔符:
\/ - Windows 特殊字符:
: * ? " < > | - Windows 保留名:
CON,PRN,AUX,NUL,COM1-9,LPT1-9
十一、技术挑战与经验总结
11.1 字节序陷阱
| 文件系统 | 字节序 | Python struct |
|---|---|---|
| ext4 | 小端 | < |
| NTFS | 小端 | < |
| XFS | 大端 | > |
11.2 常见坑点
| 问题 | 错误做法 | 正确做法 |
|---|---|---|
| XFS fsblock | 直接乘 block_size | 先 AG 解码 |
| NTFS Data Runs | 当作绝对偏移 | 累积偏移计算 |
| ext4 inode | 从 0 开始 | 从 1 开始(inode_num - 1) |
11.3 实践建议
- 用 hexdump 验证:不要盲目相信文档,用真实数据验证
- 内存管理:大文件分块读取,使用 LRU 缓存
- 边界检查:所有读取操作都要检查长度
- 安全处理:文件名必须过滤危险字符
十二、高级开发者应知的文件系统常识
以下是从实际数据恢复项目中提炼的核心认知,适合技术面试或架构讨论时展示底层理解。
12.1 文件系统的本质
一句话:文件系统 = 元数据索引 + 数据块管理
所有文件系统都在解决同一个问题:如何从文件名找到磁盘上的字节。差异只在于索引结构和空间分配策略。
用户视角: /etc/hosts
↓
文件系统: 目录索引 → inode/MFT → 数据块映射 → 物理偏移
↓
磁盘视角: sector 123456, offset 0x1E240000
12.2 三大设计哲学
| 文件系统 | 设计哲学 | 典型场景 |
|---|---|---|
| ext4 | 块组隔离,元数据分散 | 通用 Linux,平衡性能与可靠性 |
| NTFS | 一切皆属性,小文件内联 | Windows,元数据密集型 |
| XFS | AG 并行,大文件优化 | 企业存储,高吞吐场景 |
面试点:
- ext4 的 inode 2 是根目录,这是硬编码的
- NTFS 小于 900B 的文件直接存在 MFT 记录里(驻留属性),零额外 I/O
- XFS 全大端序,这是 SGI IRIX 的历史遗留
12.3 数据定位的两种范式
连续映射(Extent-based):ext4、XFS
"文件的第 0-99 块在磁盘的第 1000-1099 块"
优点:大文件高效,元数据紧凑
链式映射(Run-based):NTFS Data Runs
"从簇 1000 开始连续 50 簇,然后跳到簇 2000 连续 30 簇..."
优点:碎片化场景灵活,支持稀疏文件
12.4 必须知道的"坑"
| 坑 | 现象 | 根因 |
|---|---|---|
| XFS 地址计算错误 | 读出来全是垃圾 | fsblock 高位是 AG 号,不能直接乘块大小 |
| NTFS 偏移累加错误 | 只能读第一段数据 | Data Runs 是累积偏移,不是绝对偏移 |
| ext4 inode 越界 | inode 2 读成 inode 3 | inode 从 1 开始编号,计算时要 -1 |
| LVM 找不到文件系统 | 分区检测失败 | 要先解析 LVM 元数据找到 LV 偏移 |
12.5 架构师视角
为什么理解文件系统底层很重要?
- 性能调优:知道 ext4 的 extent 树深度影响大文件随机读性能
- 故障诊断:磁盘坏块时能判断影响范围(元数据区 vs 数据区)
- 系统设计:设计存储系统时理解 COW、日志、快照的实现代价
- 安全意识:知道"删除"只是解除索引,数据仍在磁盘上
12.6 一句话总结
| 层次 | 核心问题 | 关键数据结构 |
|---|---|---|
| 分区表 | 磁盘怎么分割? | MBR/GPT 分区条目 |
| LVM | 物理卷怎么组合? | PV → VG → LV 映射 |
| 文件系统 | 文件名怎么找到数据? | 目录索引 + inode + extent/run |
| 快照 | 增量数据怎么存? | COW + 两级索引表 |
终极认知:文件系统没有魔法,只有规则。掌握规则,就能在没有操作系统的情况下直接读取任何数据。