本文将深入解析 NTFS 文件系统的底层结构,帮助你理解 Windows 是如何组织和访问磁盘数据的。
概述
NTFS 是什么
NTFS(New Technology File System)是 Windows 的原生文件系统,自 Windows NT 3.1 引入以来,一直作为 Windows 的默认文件系统。相比 FAT32,NTFS 支持更大的文件和分区、提供更好的安全性(ACL)、支持文件压缩和加密等特性。
四大核心组件
NTFS 可以抽象为四个主要部分:
NTFS 卷
│
├── Boot Sector(引导扇区)
│ └── 提供卷参数,告诉系统 $MFT 在哪里
│
├── $MFT(主文件表)
│ └── 记录卷上每个文件和目录的"档案卡"
│
├── Data Area(数据区)
│ └── 承载非驻留数据
│
└── $MFTMirr(主文件表镜像)
└── MFT 局部损坏时提供恢复能力
用一句话概括:
Boot Sector 负责定位,$MFT 负责编目,Data Area 负责承载实际非驻留数据。
引导扇区(Boot Sector)
引导扇区位于卷的第一个扇区,包含了卷的基本参数和 $MFT 的位置信息。
关键字段
| 偏移 | 字段名 | 说明 |
|---|---|---|
| 0x0B | bytes_per_sector | 每扇区字节数(通常为 512) |
| 0x0D | sectors_per_cluster | 每簇扇区数 |
| 0x30 | mft_lcn | $MFT 起始簇号 |
C 语言结构定义
typedef struct NTFS_BOOT_SECTOR_512 {
uint8_t jump[3]; // 0x00: 跳转指令
char oem_id[8]; // 0x03: OEM ID,通常为 "NTFS "
uint16_t bytes_per_sector; // 0x0B: 每扇区字节数
uint8_t sectors_per_cluster; // 0x0D: 每簇扇区数
uint16_t reserved_sectors; // 0x0E: 保留扇区数
// ... 其他字段 ...
uint64_t total_sectors; // 0x28: 总扇区数
uint64_t mft_lcn; // 0x30: $MFT 起始簇号
uint64_t mftmirr_lcn; // 0x38: $MFTMirr 起始簇号
int8_t clusters_per_file_record; // 0x40: 每 Record 簇数
int8_t clusters_per_index_block; // 0x44: 每索引块簇数
uint64_t volume_serial_number; // 0x48: 卷序列号
uint32_t checksum; // 0x50: 校验和
uint8_t boot_code[426]; // 0x54: 引导代码
uint16_t end_marker; // 0x1FE: 结束标记 0xAA55
} NTFS_BOOT_SECTOR_512;
定位 $MFT 的公式
$$\text{MFT_Offset} = \text{mft_lcn} \times \text{sectors_per_cluster} \times \text{bytes_per_sector}$$
其中:
- mft_lcn:$MFT 起始簇号
- sectors_per_cluster:每簇扇区数
- bytes_per_sector:每扇区字节数
主文件表 $MFT
$MFT(Master File Table)是 NTFS 的核心,它是一张”总索引表”,每条 Record 描述一个文件、目录或元数据文件。
16 个系统元数据文件
$MFT 的前 16 条 Record 保留给系统元数据文件:
| Record | 文件名 | 用途 |
|---|---|---|
| 0 | $Mft | 主文件表本身 |
| 1 | $MftMirr | MFT 前 4 条记录的镜像,用于恢复 |
| 2 | $LogFile | 日志文件,用于恢复一致性 |
| 3 | $Volume | 卷信息(卷标、版本等) |
| 4 | $AttrDef | 属性定义表 |
| 5 | .(根目录) | 根文件夹 |
| 6 | $Bitmap | 簇位图,标记空闲/已用簇 |
| 7 | $Boot | 引导扇区 |
| 8 | $BadClus | 坏簇记录 |
| 9 | $Secure | 安全描述符 |
| 10 | $Upcase | 大写转换表 |
| 11 | $Extend | 扩展元数据目录 |
| 12-15 | (保留) | 供将来使用 |
Record 的角色
每条 Record 可以理解为一张”档案卡”:
- 对于文件:记录文件的数据位置、大小、时间戳等
- 对于目录:记录目录下的子项索引
- 对于元数据:记录文件系统自身的信息
Record 结构详解
$MFT 由许多固定大小的 Record 槽位组成。理解 Record 的结构是解析 NTFS 的关键。
结构概览
Record
│
├── 固定头字段区(Header)
│
├── USA / Fixup 区
│
├── Attribute 区
│
└── 未使用尾部
Header 固定字段
typedef struct NTFS_FILE_RECORD_HEADER {
char magic[4]; // 固定为 "FILE"
uint16_t usa_offset; // USA 区偏移
uint16_t usa_count; // USA 项数量
uint64_t lsn; // 日志序列号
uint16_t sequence_number; // Record 复用次数
uint16_t hard_link_count; // 硬链接数
uint16_t first_attr_offset; // 第一个 Attribute 偏移
uint16_t flags; // 0x0001=使用中,0x0002=目录
uint32_t used_size; // 已使用字节数
uint32_t allocated_size; // 分配的总字节数
uint64_t base_file_record; // 基础记录引用(扩展记录用)
uint16_t next_attr_id; // 下一个属性 ID
uint16_t reserved; // 保留
uint32_t mft_record_number; // Record 编号
} NTFS_FILE_RECORD_HEADER;
关键字段说明:
- magic:固定为
"FILE",用于验证 Record 有效性 - first_attr_offset:定位 Attribute 区域的起点
- flags:判断是文件还是目录
- used_size:有效内容的结束位置
USA 完整性保护机制
USA(Update Sequence Array)用于保护 Record 的完整性。
typedef struct NTFS_USA {
uint16_t usn; // 更新序列号
uint16_t sector_end_data[]; // 每个扇区原末尾 2 字节的备份
} NTFS_USA;
工作原理:
-
写入时:
- 把 Record 覆盖的每个扇区末尾 2 字节替换为
usn - 原值保存到
sector_end_data[]
- 把 Record 覆盖的每个扇区末尾 2 字节替换为
-
读取时:
- 检查每个扇区末尾是否等于
usn - 如果一致,恢复原值
- 如果不一致,说明该扇区数据损坏
- 检查每个扇区末尾是否等于
Attribute 区域
Attribute 区域通过 first_attr_offset 定位,存放了多个 Attribute 块。
Attribute 体系
每个 Attribute 描述文件的一个”方面”,如数据内容、文件名、时间戳等。
公共头部
typedef struct NTFS_ATTR_HEADER_COMMON {
uint32_t type; // 属性类型
uint32_t length; // 整个 Attribute 长度
uint8_t non_resident; // 0=驻留,1=非驻留
uint8_t name_length; // 属性名长度
uint16_t name_offset; // 属性名偏移
uint16_t flags; // 压缩、加密等标志
uint16_t attr_id; // 属性 ID
} NTFS_ATTR_HEADER_COMMON;
常见属性类型:
| 类型值 | 名称 | 说明 |
|---|---|---|
| 0x10 | $STANDARD_INFORMATION | 时间戳、只读/隐藏等标志 |
| 0x30 | $FILE_NAME | 文件名、父目录引用 |
| 0x80 | $DATA | 文件内容 |
Resident vs NonResident
根据数据大小,Attribute 分为两种类型:
Resident(驻留)
数据直接存放在 Record 内:
typedef struct NTFS_ATTR_HEADER_RESIDENT {
NTFS_ATTR_HEADER_COMMON common;
uint32_t value_length; // 数据长度
uint16_t value_offset; // 数据偏移
uint8_t indexed_flag; // 是否可索引
uint8_t reserved;
} NTFS_ATTR_HEADER_RESIDENT;
适用场景:小文件、短文件名、时间戳等
NonResident(非驻留)
数据存放在 Record 外,Record 内只保留映射信息:
typedef struct NTFS_ATTR_HEADER_NONRESIDENT {
NTFS_ATTR_HEADER_COMMON common;
uint64_t lowest_vcn; // 起始 VCN
uint64_t highest_vcn; // 结束 VCN
uint16_t mapping_pairs_offset; // Runlist 偏移
uint8_t compression_unit; // 压缩单位
uint8_t reserved[5];
uint64_t allocated_size; // 分配大小
uint64_t data_size; // 逻辑大小
uint64_t initialized_size; // 已初始化大小
uint64_t compressed_size; // 压缩后大小(可选)
} NTFS_ATTR_HEADER_NONRESIDENT;
适用场景:大文件、目录索引等
Runlist 与 VCN→LCN 映射
对于 NonResident 属性,需要通过 Runlist 找到数据的真实位置。
概念:
- VCN(Virtual Cluster Number):属性流内部的逻辑簇号
- LCN(Logical Cluster Number):簇在整个卷中的真实簇号
- Runlist:记录 VCN → LCN 映射关系的数据结构
数据读取步骤:
- 通过
mapping_pairs_offset找到 Runlist - 解析 Runlist,建立 VCN → LCN 映射
- 根据 LCN 计算真实磁盘地址: $$\text{DataOffset} = \text{LCN} \times \text{ClusterSize}$$
Runlist 解析示例:
若某条 Run 表示 RunLength = 8, LCN = 36,则:
$$\text{VCN } 0 \sim 7 \rightarrow \text{LCN } 36 \sim 43$$
即这个属性流的前 8 个逻辑簇,实际存放在卷上的第 36 到 43 号簇。
目录索引结构
要遍历目录,需要理解目录索引的数据结构。
INDEX_ENTRY_HEADER
typedef struct NTFS_INDEX_ENTRY_HEADER {
NTFS_FILE_REFERENCE file_reference; // 指向目标 MFT Record
uint16_t entry_length; // 当前 Entry 总长度
uint16_t key_length; // Key 长度(通常是 FILE_NAME 属性)
uint16_t flags; // 0x0001=有子节点, 0x0002=最后一项
uint16_t reserved;
} NTFS_INDEX_ENTRY_HEADER;
关键字段:file_reference 指向子文件/子目录的 MFT Record。
FILE_REFERENCE(文件引用号)
这是一个 64 位值,需要拆分为两部分:
typedef uint64_t NTFS_FILE_REFERENCE;
// 低 48 位 = MFT Record 编号
static inline uint64_t ntfs_ref_record_number(NTFS_FILE_REFERENCE ref) {
return ref & 0x0000FFFFFFFFFFFFULL;
}
// 高 16 位 = 序列号
static inline uint16_t ntfs_ref_sequence_number(NTFS_FILE_REFERENCE ref) {
return (uint16_t)(ref >> 48);
}
| 部分 | 位数 | 含义 |
|---|---|---|
| FileRecordNumber | 低 48 位 | MFT Record 编号 |
| SequenceNumber | 高 16 位 | 序列号(用于验证) |
序列号的作用:确保引用的有效性。如果 Record 被复用,序列号会变化,旧引用将失效。
FILE_NAME_ATTR
索引项中携带的文件名属性:
typedef struct NTFS_FILE_NAME_ATTR {
NTFS_FILE_REFERENCE parent_directory; // 父目录引用
uint64_t creation_time; // 创建时间
uint64_t modified_time; // 修改时间
uint64_t mft_changed_time; // MFT 修改时间
uint64_t read_time; // 访问时间
uint64_t allocated_size; // 已分配大小
uint64_t real_size; // 实际大小
uint32_t flags; // 文件/目录标志
uint32_t reparse; // 重解析值
uint8_t name_length; // 文件名长度(Unicode 字符数)
uint8_t name_namespace; // 名字空间
// 后面紧跟: uint16_t name[name_length]
} NTFS_FILE_NAME_ATTR;
文件操作实现原理
理解了 NTFS 的数据结构后,我们来看看常见文件操作的底层实现。
路径遍历
所有操作的第一步都是解析路径。路径遍历的本质是:从根目录开始,一层一层在目录索引中按名字查找。
路径: C:\Users\test.txt
1. 从根目录 mft[5] 开始
2. 在索引中找 "Users" → 得到 file_reference
3. 跳转到对应 Record
4. 在索引中找 "test.txt" → 得到 file_reference
5. 跳转到目标 Record
核心步骤:
- 在当前目录索引中找名字匹配的 Entry
- 从 Entry 中取出
file_reference - 验证
sequence_number是否一致 - 跳到对应的 MFT Record
浏览目录(ls)
找到目标目录的 Record 后,读取 INDEX_ROOT / INDEX_ALLOCATION 属性,枚举所有 Entry,输出文件名。
读取文件(cat)
找到目标文件的 Record 后,读取 $DATA 属性:
- Resident:直接从 Record 内取值
- NonResident:解析 Runlist,读取真实数据
创建文件(touch)
本质:创建一个新的 Record,并插入父目录索引。
1. 找到父目录
2. 分配新的 MFT Record
3. 初始化属性:
- $STANDARD_INFORMATION
- $FILE_NAME
- 空的 $DATA
4. 在父目录索引里插入新 Entry
5. 更新日志和元数据
创建目录(mkdir)
本质:创建一个新的目录 Record,并插入父目录索引。
1. 定位父目录 Record
2. 分配新的 MFT Record
3. 初始化属性:
- $STANDARD_INFORMATION
- $FILE_NAME
- $INDEX_ROOT
4. 在父目录索引中插入新 Entry
5. 更新位图、MFT、日志
删除文件(rm)
本质:从父目录中移除名字,并回收资源。
1. 找到目标文件 Record
2. 在父目录索引里删除对应 Entry
3. 减少硬链接计数
4. 如果硬链接计数 = 0:
- 标记 Record 为未使用
- 回收 $DATA 占用的簇
- 更新 $Bitmap
5. 记录日志
删除目录(rmdir)
本质:删除一个空目录。
与 rm 的区别:
- 必须先检查目录是否为空
- 如果不空,操作失败
移动/重命名(mv)
本质:改目录索引 + 改 $FILE_NAME。
同目录改名:
- 在父目录索引中修改 Entry 的名字
- 更新 Record 中的
$FILE_NAME属性
跨目录移动:
- 在旧父目录索引中删除 Entry
- 在新父目录索引中插入 Entry
- 更新
$FILE_NAME里的父目录引用
同一卷内移动,通常不需要搬实际
$DATA簇。
复制文件(cp)
本质:创建新的 Record + 复制数据。
与 mv 的区别:cp 一定会创建新的 Record 和新的数据分配。
1. 读取源文件 $DATA
2. 创建新的 Record
3. 分配新的 $DATA
4. 写入数据
5. 在目标目录插入新 Entry
写文件
本质:修改目标文件的 $DATA 属性。
1. 找到目标 Record
2. 找到 $DATA
3. 如果写入变大,可能 Resident → NonResident
4. 为新数据分配簇
5. 更新 Runlist
6. 更新 allocated_size、data_size
7. 更新 $Bitmap 和日志
查看文件信息(stat)
本质:读取 Record 里的元数据属性。
不是读数据内容,而是读:
$STANDARD_INFORMATION:时间戳、属性标志$FILE_NAME:文件名、父目录引用
操作总结
| 操作 | 本质 |
|---|---|
| ls | 读取 INDEX_ROOT/ALLOCATION,枚举 Entry |
| cat | 读取 $DATA(Resident 直接取,NonResident 解 Runlist) |
| touch | 新建 Record + 插入父目录索引 |
| mkdir | 新建目录 Record + 初始化索引属性 |
| rm | 删除 Entry + 硬链接为 0 时回收资源 |
| rmdir | 删除空目录的 Entry + 回收 Record |
| mv | 改目录索引 + 改 $FILE_NAME(同卷不搬数据) |
| cp | 新建 Record + 复制数据 |
| write | 修改 $DATA + 可能转 NonResident + 分配新簇 |
| stat | 读取元数据属性 |
总结
NTFS 的核心设计理念是”一切皆属性”:
- 定位链:Boot Sector → $MFT → Record → Attribute → Data
- Record:文件的”档案卡”,包含多个 Attribute
- Attribute:描述文件的不同方面,分为 Resident 和 NonResident
- 索引:目录通过 Index Entry 组织,通过 File Reference 定位子项
理解这些结构后,你就可以:
- 手动解析 NTFS 卷,定位任意文件
- 理解文件系统操作的底层实现
- 进行数据恢复或取证分析