资讯动态

FAT32磁盘扇区直读与目录项隐藏:从BPB到簇链的底层实现

发布时间:2026/9/12 13:00:26 来源:尧图企业网站定制
简介基于VC与MFC实现的底层磁盘访问工程面向需要直接操作硬盘扇区、深入理解FAT文件系统的C开发者。代码围绕磁盘底层读写展开既支持以字节或字为单位读写扇区也能按簇进行读写同时封装了读取FAT项、获取BPB信息等关键操作在目录项层面可以读取某个文件或文件夹的入口簇及偏移量也能够设置目录项内容借此实现更改文件名、文件属性、创建时间等需求适合学习Windows磁盘结构、FAT表解析以及数据恢复原理时作为参考。压缩包共24个文件主体为9个头文件与7个C源文件辅以DSP/DSW工程文件、RC资源文件以及图标位图素材工程结构简洁可直接打开编译调试整体仅26KB。已有527人浏览学习内容具有较强针对性适合具备一定VC基础、希望向磁盘底层开发方向进阶的读者参考使用。1. 绕过文件系统直读硬盘扇区diskaccess 的立身之本在 Windows 下删除 FAT32 分区里的一个文件你看到的是文件消失实际磁盘上数据分毫未动文件系统只是把目录项首字节改写成了 0xE5。这个细节走正常 API 永远看不到要确认它必须直接读扇区。diskaccess 就是一套 VC 6.0 的 MFC 文档视图工程FS.dsw/FS.dsp 全套核心能力是按字节或字读写扇区与簇、解析 BPB 引导块、读取 FAT 表项以及定位并修改目录项的入口簇与偏移。做数据恢复、磁盘取证、文件隐藏或自研分区工具的人都能从这套代码里抽出可复用的底层函数。下面按 BPB 解析、FAT 表项、目录项、读写边界四层拆开讲。2. BPB 解析与扇区寻址读盘前先把引导扇区和字节序对齐2.1 引导扇区的 BPB 结构pack(1) 与字节序FAT 文件系统把卷的全部布局参数塞在引导扇区VBR里从偏移 0x0B 开始是 BPB。所有 WORD/DWORD 字段都是 x86 小端序即低字节在前。用 C 结构体直接映射时最经典的问题不是读而是对齐VC 6.0 默认按 8 字节对齐结构体字段间会插入空洞导致偏移错位。#pragma pack(push, 1) typedef struct _BPB32 { BYTE jmpBoot[3]; // 0x00 跳转指令 CHAR OEMName[8]; // 0x03 厂商标识如 MSDOS5.0 WORD BytsPerSec; // 0x0B 每扇区字节数FAT32 固定 512 BYTE SecPerClus; // 0x0D 每簇扇区数必须为 2 的幂 WORD RsvdSecCnt; // 0x0E 保留扇区数FAT32 通常是 32 BYTE NumFATs; // 0x10 FAT 表份数一般为 2 WORD RootEntCnt; // 0x11 根目录项数FAT32 必须为 0 WORD TotSec16; // 0x13 旧式总扇区数FAT32 为 0 BYTE Media; // 0x15 介质描述符硬盘为 0xF8 WORD FATSz16; // 0x16 旧式 FAT 大小FAT32 为 0 WORD SecPerTrk; // 0x18 每磁道扇区数 WORD NumHeads; // 0x1A 磁头数 DWORD HiddSec; // 0x1C 分区起始偏移扇区数 DWORD TotSec32; // 0x20 32 位总扇区数 DWORD FATSz32; // 0x24 每份 FAT 占用的扇区数 WORD ExtFlags; // 0x28 扩展标志 WORD FSVer; // 0x2A 文件系统版本 DWORD RootClus; // 0x2C 根目录起始簇号常见为 2 WORD FSInfo; // 0x30 FSInfo 扇区号通常为 1 WORD BkBootSec; // 0x32 备份引导扇区号通常为 6 BYTE Reserved[12]; // 0x34 保留 BYTE DrvNum; // 0x40 驱动器号 BYTE Reserved1; // 0x41 BYTE BootSig; // 0x42 0x29 表示后跟卷序列号 DWORD VolID; // 0x43 卷序列号 CHAR VolLab[11]; // 0x47 卷标 CHAR FilSysType[8]; // 0x52 FAT32 } BPB32; #pragma pack(pop)#pragma pack(push, 1)让所有成员按 1 字节紧凑排列结构体总长固定为 90 字节0x5A正好覆盖引导扇区偏移 0x00 到 0x59。验证方式是在 VC6 的 Watch 窗口里看sizeof(BPB32)或者对FilSysType取offsetof(BPB32, FilSysType)期望值是 0x52。VC 6.0 里查看结构体内存布局可以把变量拖进 Watch 窗口展开观察成员值要核对真实字节顺序再按 Alt6 打开 Memory 窗口盯着那块地址。读出来的 WORD 字段在内存里是01 40这种低字节在前的形态Watch 窗口显示成 0x4001不要被表象误导。RootEntCnt、TotSec16、FATSz16三个字段在 FAT32 下必须是 0这是判断分区是不是 FAT32 的快速依据。2.2 CreateFile 打开物理盘与卷句柄选型决定了 LBA 起点读扇区之前先决定目标是整块物理盘还是某个分区卷两者通过 CreateFile 的路径区分同时打开 GENERIC_READ 和 GENERIC_WRITE 需要管理员权限共享模式也必须放开否则被系统占用的卷会返回拒绝访问。HANDLE OpenDisk(LPCTSTR szPath, BOOL bWrite) { HANDLE hDisk CreateFile(szPath, bWrite ? (GENERIC_READ | GENERIC_WRITE) : GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL); if (hDisk INVALID_HANDLE_VALUE) AfxMessageBox(_T(打开失败请检查权限、路径和共享模式)); return hDisk; }调用时传_T(\\\\.\\PhysicalDrive0)或_T(\\\\.\\C:)。_T宏保证 ANSI 与 UNICODE 两种工程配置下都能编译VC6 默认 MBCS 工程里它就是普通字符串设备路径本身是 ASCII不涉及宽字符转换的坑。两种句柄的差异直接决定读到的东西句柄路径文件指针 0 对应的内容适用场景\\.\PhysicalDriveN整盘第一个扇区MBR读分区表、整盘镜像、跨分区操作\\.\C:卷引导扇区VBR即 BPB 所在扇区解析 FAT 参数、算 FAT 区起点\\.\D:同上对应 D 卷多分区分别处理物理盘句柄读到的第一个扇区就是硬盘第一个引导扇区 MBR分区表在偏移 0x1BE 处共 4 个 16 字节表项。卷句柄则完全看不到 MBR它把文件指针 0 映射到卷起始扇区也就是说用\\\\.\\C:读扇区 0拿到的是 BPB 而不是分区表。diskaccess 工程里 BPB 解析和分区解析两条路径都存在正是靠这两种句柄区分入口。2.3 扇区号到文件偏移的换算与扇区读取Windows 文件指针按字节计物理盘没有按扇区读的专用 API一切读操作都要先做乘法。FAT32 的每扇区字节数写在 BPB 里不要写死 512先进格式化盘上会有 4096 的情况。BOOL ReadSector(HANDLE hDisk, DWORDLONG sectorNo, PBYTE pBuf, DWORD nSectors, WORD bytsPerSec) { LARGE_INTEGER li; li.QuadPart (LONGLONG)(sectorNo * bytsPerSec); if (!SetFilePointerEx(hDisk, li, NULL, FILE_BEGIN)) return FALSE; DWORD bytesRead 0; if (!ReadFile(hDisk, pBuf, nSectors * bytsPerSec, bytesRead, NULL)) return FALSE; return bytesRead nSectors * bytsPerSec; }sectorNo用DWORDLONG是 64 位整数2TB 磁盘最后一个扇区的字节偏移已经超过 2 的 32 次方换算必须先提升到 64 位再乘否则乘法溢出后 SetFilePointerEx 定位到错误位置。SetFilePointerEx的偏移参数是LARGE_INTEGER在 VC6 里统一走.QuadPart不要用旧版SetFilePointer拆高 32 位和低 32 位分别传参的写法。拿到 BPB 之后有三个关键派生量FAT 区起点扇区等于RsvdSecCnt数据区起点扇区等于RsvdSecCnt NumFATs * FATSz32根目录起始簇等于RootClus。后面操作 FAT 表和目录项时这几个值会反复使用。3. FAT 表项与簇链遍历FAT.cpp 里 12/16/32 的分叉3.1 FAT12/16/32 表项的位宽与掩码差异FATFile Allocation Table是文件簇链的索引表文件每占一个簇表里就对应一项项值就是下一簇的簇号链尾填结束标记。三种 FAT 的差异集中在表项宽度和有效位数上这是 FAT.cpp 里最主要的 if 分叉点。FAT 类型表项宽度有效位坏簇标记链结束标记FAT1212 bit120xFF70xFF8 - 0xFFFFAT1616 bit160xFFF70xFFF8 - 0xFFFFFAT3232 bit280x0FFFFFF70x0FFFFFF8 - 0x0FFFFFFFFAT32 虽然每项占 4 字节但规范规定只使用低 28 位高 4 位保留。读取后必须做 0x0FFFFFFF掩码否则遇到高 4 位带残留值的表项会把 0x0FFFFFFF 这种正常结束标记误判成非法簇号。FAT12 是唯一需要跨字节拼位的情形偶数簇号的表项占 12 位中的低半部分奇数簇号占高半部分diskaccess 工程里通过对FilSysType字符串或 FAT 表大小做判断分成三套逻辑处理。3.2 读取 FAT 项扇区定位与 memcpy 取值FAT 表从RsvdSecCnt扇区开始连续存放表项在表内的字节偏移 簇号 × 表项宽度。以 FAT32 为例#define SECTOR_MAX 4096 // 兼容 4Kn 高级格式化盘 DWORD GetFatEntry(HANDLE hDisk, PBPB32 pBpb, DWORD cluster) { if (cluster 2) return 0; DWORD fatStartSec pBpb-RsvdSecCnt; DWORD entryOff cluster * 4; // FAT32 每项 4 字节 DWORD entrySec fatStartSec entryOff / pBpb-BytsPerSec; DWORD inSecOff entryOff % pBpb-BytsPerSec; BYTE sec[SECTOR_MAX]; if (!ReadSector(hDisk, entrySec, sec, 1, pBpb-BytsPerSec)) return 0xFFFFFFFF; DWORD val 0; memcpy(val, sec inSecOff, 4); // 避免非对齐直接解引用 return val 0x0FFFFFFF; // 只保留低 28 位 }这里有两个细节。第一cluster * 4会跨越扇区边界BytsPerSec为 512 时簇 127 的表项偏移 508 落在本扇区簇 128 的偏移 512 就进下一个扇区了所以必须先整除算出目标扇区号再取余做扇区内偏移。第二sec inSecOff在表项宽度为 4 字节时永远是 4 字节对齐的但写成*(DWORD*)(sec inSecOff)属于未定义行为换成 memcpy 后 VC6 的 Release 版照样生成一条 MOV 指令开销为零同时消除越界风险。3.3 簇链遍历与防环保护簇号到扇区的公式为数据区起点 (簇号 - 2) × 每簇扇区数。减 2 是因为 FAT 规范保留前两个簇号给介质描述符实际数据从簇 2 开始。DWORD ClusterToSector(PBPB32 pBpb, DWORD cluster) { DWORD dataStart pBpb-RsvdSecCnt pBpb-NumFATs * pBpb-FATSz32; return dataStart (cluster - 2) * pBpb-SecPerClus; } BOOL WalkClusterChain(HANDLE hDisk, PBPB32 pBpb, DWORD startClus, CArrayDWORD, DWORD chain) { DWORD clus startClus; int guard 0; while (clus 2 clus 0x0FFFFFF8) { chain.Add(clus); clus GetFatEntry(hDisk, pBpb, clus); if (guard 65536) return FALSE; // 防止 FAT 环导致死循环 } return (clus 0x0FFFFFFF) || (clus 0x0FFFFFF8 clus 0x0FFFFFFF); }guard计数器是必须的FAT 表被误改写后会出现环路簇 100 指向 200200 又指回 100没有上限保护这个 while 会无限转下去界面表现就是卡死。判断链尾时用(clus 0x0FFFFFFF)单独判断不严谨规范允许结束标记是一段区间 0x0FFFFFF8 到 0x0FFFFFFF所以用区间判断。调用者拿到chain数组后就能逐个扇区读出一个文件的全部内容。4. 目录项读写与隐藏入口簇、偏移和属性改写的落点4.1 32 字节目录项名字、属性、时间与簇号字段FAT 目录项固定 32 字节长文件名由连续多个属性为 0x0F 的 LFN 目录项叠加表示其后的短名目录项才是文件本体。短名结构如下#pragma pack(push, 1) typedef struct _DIRENTRY { BYTE DIR_Name[11]; // 0x00 8.3 文件名不足补空格 BYTE DIR_Attr; // 0x0B 属性 BYTE DIR_NTRes; // 0x0C 保留 BYTE DIR_CrtTimeTenth; // 0x0D 创建时间百分秒 WORD DIR_CrtTime; // 0x0E 创建时间 WORD DIR_CrtDate; // 0x10 创建日期 WORD DIR_LstAccDate; // 0x12 最后访问日期 WORD DIR_FstClusHI; // 0x14 簇号高 16 位 WORD DIR_WrtTime; // 0x16 最后写入时间 WORD DIR_WrtDate; // 0x18 最后写入日期 WORD DIR_FstClusLO; // 0x1A 簇号低 16 位 DWORD DIR_FileSize; // 0x1C 文件字节数目录为 0 } DIRENTRY; #pragma pack(pop)属性字节每一位是一个开关位值含义0x01只读0x02隐藏0x04系统0x08卷标0x10目录0x20存档0x0F长文件名项时间日期字段都是 16 位压缩编码日期高 7 位是年减去 1980中 4 位是月低 5 位是日时间高 5 位是小时中 6 位是分低 5 位是秒除以 2。写入时按位或拼回去读的时候按位拆。4.2 定位文件目录项根目录簇链扫描FAT32 的根目录不再是固定区域而是一条从RootClus开始的普通簇链。要找到某文件的目录项先遍历根目录链再在每簇范围内逐扇区逐条目比对BOOL FindDirEntry(HANDLE hDisk, PBPB32 pBpb, const char* shortName, DWORD entryClus, DWORD entryOffset) { CArrayDWORD, DWORD chain; if (!WalkClusterChain(hDisk, pBpb, pBpb-RootClus, chain)) return FALSE; for (int i 0; i chain.GetSize(); i) { DWORD sec0 ClusterToSector(pBpb, chain[i]); for (int s 0; s pBpb-SecPerClus; s) { BYTE buf[SECTOR_MAX]; if (!ReadSector(hDisk, sec0 s, buf, 1, pBpb-BytsPerSec)) continue; for (int e 0; e (int)(pBpb-BytsPerSec / 32); e) { PDIRENTRY pDe (PDIRENTRY)(buf e * 32); if (pDe-DIR_Name[0] 0x00) return FALSE; // 后续全空 if (pDe-DIR_Name[0] 0xE5) continue; // 已删除 if (pDe-DIR_Attr 0x0F) continue; // LFN 跳过 if (memcmp(pDe-DIR_Name, shortName, 11) 0) { entryClus chain[i]; // 目录项所在簇 entryOffset s * pBpb-BytsPerSec e * 32; return TRUE; } } } } return FALSE; }DIR_Name[0]是 0x00 表示这是目录中最后一个有效条目后面不可能再有文件可以直接终止扫描是 0xE5 表示该条目标记删除但要继续往后找删除位只影响当前条目。短名查找要求调用方先拼好 11 字节格式文件名不足 8 字节补空格扩展名不足 3 字节补空格。带中文名的用户需要先收集前面连续的 LFN 项注意 LFN 在磁盘上是逆序存放的最后一个字符片段在最前面拼接时要倒着读。4.3 写回目录项先备份再改写修改文件名、属性或创建时间的落点就是上面定位到的entryOffset。做法是读出所在扇区改 32 字节再原样写回BOOL SetDirEntryInfo(HANDLE hDisk, PBPB32 pBpb, DWORD entryClus, DWORD entryOffset, const DIRENTRY newEntry) { DWORD sec ClusterToSector(pBpb, entryClus) entryOffset / pBpb-BytsPerSec; DWORD off entryOffset % pBpb-BytsPerSec; BYTE buf[SECTOR_MAX]; if (!ReadSector(hDisk, sec, buf, 1, pBpb-BytsPerSec)) return FALSE; memcpy(buf off, newEntry, sizeof(DIRENTRY)); return WriteSector(hDisk, sec, buf, 1, pBpb-BytsPerSec); }写回顺序没有严格要求但整扇区回写意味着同扇区其他 15 个目录项也会被重写必须保证缓冲区里其他字节没有被破坏。我一般会先整扇区读进内存只 memcpy 目标 32 字节然后整扇区写回绝不只写 32 字节——Windows 底层扇区写的最小单位就是BytsPerSec局部写要么被忽略要么造成扇区内容残缺。4.4 Hide_FAT32.cpp 的隐藏手法工程里的 Hide_FAT32.cpp 对应的就是目录项级隐藏常见有三种做法。第一种把目录项首字节改成 0xE5文件看起来被删除但簇链和文件内容原样保留是最容易恢复的隐藏方式。注意首字节是 0x05 时实际文件名首字符是 0xE5 本身做隐藏判断时不要误伤。第二种把该文件在 FAT 里的下一簇值改成 0x0FFFFFF7坏簇标记资源管理器访问时会报错而不是列出内容目录项本身不动chkdsk 也不会去清理被标坏的簇。第三种最隐蔽改 BPB 的SecPerClus或FATSz32让系统重新解析后数据区起点整体偏移。数据其实还在只是文件系统层已经看不见它们。这种手法做错一个字段整卷都可能变成 RAW所以 Hide_FAT32.cpp 里通常都会先把原始 BPB 值存进一个持久化结构恢复时整扇区回写。5. 扇区级读写的对齐边界与验证技巧5.1 字节读与字读的选型以字节读取用于引导代码、OEM 字符串、卷标这类无对齐要求的区域以字读取用于 BPB 的 WORD 字段和 FAT16 表项。x86 小端序决定了WORD低字节在前memcpy 两个字节出来就是正确数值不需要交换字节序。VC6 里按字读要留意奇数偏移目录项里的DIR_CrtTime位于扇区偏移 14永远是偶数但 LFN 项的 Unicode 字符分布在偏移 1、3、5……按字读全踩在奇数上这种场合必须逐字节拼。5.2 写前备份与读回校验对物理盘做任何写操作之前先把这个扇区读出来存成备份文件命名用LBA号.bin例如LBA100.bin。写完立即重读该扇区与期望内容比对BOOL VerifySectorWrite(HANDLE hDisk, DWORDLONG sectorNo, PBYTE expected, WORD bytsPerSec) { BYTE back[SECTOR_MAX]; if (!ReadSector(hDisk, sectorNo, back, 1, bytsPerSec)) return FALSE; return memcmp(back, expected, bytsPerSec) 0; }这个校验能暴露两类问题写到了错误扇区偏移换算错以及磁盘控制器或 OS 缓存直接吞掉了写请求。对空硬盘做整盘填充时系统按 LBA 顺序从 0 号扇区向后写等价于先写引导扇区再推进到数据区配合这个校验函数可以顺带验证填充是否真正落盘。5.3 三个高频坑权限与共享冲突。物理盘句柄默认要管理员令牌且FILE_SHARE_READ | FILE_SHARE_WRITE缺一不可卷句柄在系统索引服务占用期间打开会失败重试或改用物理盘句柄绕过卷层。FILE_FLAG_NO_BUFFERING 的对齐要求。打开卷句柄时若加这个标志绕过缓存缓冲区地址和长度都必须按扇区对齐否则 ReadFile 直接返回 ERROR_INVALID_PARAMETER。配套做法是用VirtualAlloc分配扇区对齐内存而不是栈上的BYTE buf[SECTOR_MAX]。提示对正在挂载使用的卷做扇区级写Windows 可能用缓存里的旧数据覆盖你的改动。稳妥做法是先把卷脱机或写入后用卷句柄重新打开验证一遍。FSInfo 扇区不同步。手动改了 FAT 表项后FSInfo 扇区通常是扇区 1里存的空闲簇计数和下一可用簇号就失真了。修正方法把FSInfo字段指到的扇区读出来偏移 0x1E8 写回正确空闲簇数偏移 0x1EC 写回下一可用簇号或者直接把这两个位置写成 0xFFFFFFFF让 Windows 下次挂载时自动重建 FSInfo。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价