简介面向数据恢复、系统调试与安全分析初学者及需要补强磁盘底层认知的IT从业者这份PPT课件围绕WinHex定位文件首个扇区展开帮助读者建立从物理磁盘到分区、文件系统再到具体文件的定位思路。压缩包仅含1个pptx文件大小约1.1MB以图示化页面串联MBR与分区表、DBR、FAT32的FAT表、根目录区、32字节目录项以及起始簇号换算等关键概念并给出分区起始扇区叠加、根目录扇区计算和文件首扇区确认的完整案例。已有79人学习适合课堂演示、自学梳理或实验复盘时对照使用可据此理解扇区号、簇号与分区偏移之间的关系形成可迁移的磁盘分析框架。1. 一个文件“在哪”为什么答案不是一个数字而是三层地址的叠加很多人以为问“这个文件在磁盘的哪个扇区”答案应该是一个整数。真去操作系统课上讲的磁盘结构里翻一遍就会发现文件系统从来不直接记录物理扇区号。文件里存的是一个簇号簇号是分区内部的相对编号分区自己又是整块盘里的一个区间。想知道某个 txt 文件的字节究竟躺在哪一格磁介质上得把三层的地址一段段叠起来MBR 里的分区表给出分区起点DBR 里的 BPB 给出 FAT 表和数据区布局目录项里给出起始簇号最后才换算出物理扇区。这套链条在数据恢复、磁盘取证、坏盘抢救里是唯一的坐标系搞不清它工具再强也只是个黑匣子。2. MBR 与分区表0 号扇区里那 64 个字节是怎么切盘的2.1 446 加 64 加 2一个扇区被切成三段整块磁盘的第一个扇区也就是 0 号扇区在传统 MBR 分区方案里承担着“目录页”的角色。它的 512 个字节并不是随意堆放的而是严格分成三段前 446 字节是引导代码接着 64 字节是分区表最后 2 字节是固定签名 0x55AA。三段各有各的职责也各有各的历史包袱。区域偏移长度作用引导代码0x000 - 0x1BD446 字节早期用于跳转并加载活动分区的引导记录分区表0x1BE - 0x1FD64 字节4 个分区表项每项 16 字节结束标识0x1FE - 0x1FF2 字节0x55AA供 BIOS 判断该扇区是否可引导最容易忽略的是最后那 2 个字节。0x55AA 不是校验和也不是版本号它只是“这个扇区的最后两个字节恰好是这两个值”的一个标志。BIOS 在开机自检阶段读入 0 号扇区后会检查这个签名不匹配就直接判定为不可引导设备。反过来说做磁盘分析时它也是极好用的锚点你在 WinHex 里跳到 0 号扇区看到最后两个字节不是 55 AA那基本可以断定你读的位置不对或者这块盘用的是 GPT 而保护 MBR 被改写过。提示GPT 磁盘的 0 号扇区通常是一个类型为 0xEE 的“保护 MBR”整块盘被描述成一个覆盖全盘的分区目的是让不认识 GPT 的老工具不要误以为这是一块空盘而重新分区。2.2 16 字节分区表项起始扇区藏在偏移 864 字节分成 4 份每份 16 字节对应一个主分区。这 16 个字节的内部结构从 1980 年代沿用至今字段安排相当紧凑偏移项内长度含义0x001 字节状态0x80 表示活动分区0x00 表示非活动0x013 字节起始 CHS 地址柱面/磁头/扇区0x041 字节分区类型0x0B/0x0C 为 FAT320x07 为 NTFS0xEE 为 GPT 保护0x053 字节结束 CHS 地址0x084 字节分区起始扇区号LBA小端序0x0C4 字节分区总扇区数小端序真正有用的是偏移 8 开始的 4 字节。前面那两组 CHS 字段在超过 8GB 的盘上早就溢出了——CHS 用 10 位表示柱面最大只能编到 1023按当时的几何换算大概 7.8GB 封顶。所以后来的工具和系统基本都只看 LBA 字段CHS 字段留着只是为了老 BIOS 能在过渡年代启动系统。在 WinHex 里看这 16 个字节最直观的方式是打开“视图 → 显示 → 数据解释器”把光标停在偏移 0x1C6也就是 0x1BE 8上解释器会直接把 4 个字节按小端解释成十进制数。分区起始扇区是 2048对应的十六进制就是 00 08 00 00反过来读就是 0x00000800。用代码验证一遍更踏实import struct DISK r\\.\PhysicalDrive0 # Linux 下换成 /dev/sda with open(DISK, rb) as fp: mbr fp.read(512) if mbr[510:512] ! b\x55\xAA: raise SystemExit(签名不匹配读到的不一定是 MBR) for i in range(4): ent mbr[446 i * 16 : 446 (i 1) * 16] ptype ent[4] if ptype 0x00: continue # 空表项直接跳过 lba struct.unpack(I, ent[8:12])[0] # 分区起始扇区LBA count struct.unpack(I, ent[12:16])[0] # 分区总扇区数 print(f分区{i1} 类型0x{ptype:02X} 起始{lba} 扇区数{count})这段代码里有两个细节值得说明。struct.unpack(I, ...)中的表示小端序磁盘上的多字节整数几乎都是小端存放如果你按大端读2048 会被读成 0x00080000一个完全无意义的巨大数字。另外我用ptype 0x00来跳过空表项而不是用“起始扇区为 0”来判断因为理论上存在起始扇区写 0 的畸形分区表按类型判断更稳。2.3 为什么第一个分区偏偏从 2048 开始拆开一块盘你会发现绝大多数磁盘的第一个分区起始 LBA 都是 2048这个数字稳定得像约定俗成。2048 × 512 字节 1MB也就是说 0 号扇区到 2047 号扇区这 1MB 空间被完整空了出来。这不是浪费。早期机械盘时代分区起始扇区按“柱面对齐”来放第一个分区常常从 63 号扇区开始因为一条磁道是 63 个扇区这样分区边界正好落在柱面开头。到了固态盘和大容量机械盘时代物理扇区从 512 字节变成 4096 字节闪存的擦除块更大如果一个 4KB 的逻辑页跨越了两个物理页每次写入都要先读-改-写两次性能直接腰斩。1MB 是 512、4096 乃至更大擦除块的公倍数从 2048 起步天然满足所有对齐要求。空出来的这 1MB 还顺便安置了引导代码、GPT 头、以及一些引导管理器的 stage 1.5。所以你在分析时看到第一个分区从 2048 起不要以为是巧合这是几十年踩坑之后形成的工程惯例。3. DBR 与 BPB分区自己的“第 0 扇区”里写了什么3.1 两个坐标系分区内偏移与物理偏移拿到 2048 之后第一个容易翻车的地方就来了。2048 是物理磁盘上的绝对扇区号而从这一刻起DBR 里记录的所有数字都是“分区内相对扇区号”。两套坐标系同时存在是从这里开始贯穿全文的主线。用 WinHex 的操作来说在物理磁盘视图下跳到 2048看到的是这个分区的第一个扇区也就是 DBRDOS Boot Record。它等价于分区内的 0 号扇区。DBR 里有一个字段叫“隐藏扇区数”记录的值正好就是 2048它就是连接两套坐标系的桥。有些分析工具懒得读 MBR直接从这个字段反推分区起点就是这个道理。DBR 本身还有备份。FAT32 规定在分区的第 6 号扇区保存一份 DBR 副本第 7 号扇区保存 FSInfo 的副本。主 DBR 被病毒或误操作写坏时把第 6 扇区的内容拷回 0 号扇区往往能让分区重新被识别——这是数据恢复里成本最低的一招。3.2 BPB 关键字段决定整条地址链的几个数字DBR 的前 90 个字节左右是 BPBBIOS Parameter Block文件系统的几何参数全在这里。FAT32 的几个关键字段偏移长度字段本例取值作用0x0B2每扇区字节数512扇区换算的基准0x0D1每簇扇区数16簇到扇区的乘数0x0E2保留扇区数3234DBR 到第一个 FAT 表之间的间隔0x101FAT 表个数2通常双份互为备份0x1C4隐藏扇区数2048分区起始 LBA坐标系的桥0x204总扇区数32 位—分区总容量0x244每个 FAT 表扇区数14767第二个 FAT 表和数据区的偏移来源0x2C4根目录起始簇2FAT32 固定为 2不是固定区域0x302FSInfo 扇区号1记录空闲簇数仅供参考保留扇区数 3234 这个值偏大正常 FAT32 格式化一般只保留 32 个扇区。这里之所以是 3234通常是格式化工具按 FAT 表容量做了对齐或者这块分区是从别的布局调整过来的。不管来源如何后续所有计算都必须用 BPB 里的实测值不能套“一般是 32”这种经验值——这正是后面避坑章要展开的一条。每簇扇区数为 16意味着一个簇是 8KB。簇越大FAT 表项越少、表越小但文件末尾浪费的空间越多。一个 1 字节的文件也要占满 8KB这就是内部碎片。FAT32 在大容量盘上往往被迫用大簇是它作为过渡方案的一个结构性缺陷。3.3 根目录公式是怎么推出来的FAT32 最反直觉的一点是根目录不再是一块固定大小的区域。FAT12/16 时代BPB 里有个“根目录项数”字段根目录被固定安排在 FAT 表之后占 512 项、每项 32 字节合计 32 个扇区位置永远不变。FAT32 把根目录降级成了普通簇链起始簇固定为 2BPB 里的根目录项数字段直接写 0。这个改动让根目录可以像普通目录一样动态增长但也让“根目录在哪”从一个常量变成了一个算式。算式本身很朴素就是把分区内的布局从左到右依次加起来分区内布局 保留扇区 → FAT 表 1 → FAT 表 2 → 数据区数据区的第一个扇区也就是簇 2 的起始位置等于保留扇区数加上 FAT 表个数乘以每个 FAT 表的扇区数。代入本例32768 3234 2 × 147673234 是保留扇区两个 FAT 表各占 14767 个扇区。这也顺便解释了为什么根目录簇号是 2 而不是 0 或 1簇号 0 和 1 被 FAT 表自身占用了0 号表项是介质描述符1 号表项存的是脏标志真正的数据从 2 开始编号。簇 2 就是数据区的第一个簇正好落在 32768。用代码把 BPB 完整读一遍import struct PART_START 2048 # 来自 MBR 分区表 with open(DISK, rb) as fp: fp.seek(PART_START * 512) # 物理偏移 分区起点 × 扇区大小 dbr fp.read(512) bps struct.unpack(H, dbr[0x0B:0x0D])[0] # 每扇区字节数 spc dbr[0x0D] # 每簇扇区数 reserved struct.unpack(H, dbr[0x0E:0x10])[0] # 保留扇区数 nfat dbr[0x10] # FAT 表个数 hidden struct.unpack(I, dbr[0x1C:0x20])[0] # 应等于 PART_START fat_size struct.unpack(I, dbr[0x24:0x28])[0] # 每个 FAT 表扇区数 root_clus struct.unpack(I, dbr[0x2C:0x30])[0] # 根目录起始簇 data_start reserved nfat * fat_size # 数据区起始分区内 print(f分区起点{hidden} 保留{reserved} FAT数{nfat} fFAT大小{fat_size} 数据区起{data_start} 根簇{root_clus})这段的输出应该是分区起点2048 保留3234 FAT数2 FAT大小14767 数据区起32768 根簇2。注意hidden和PART_START应当一致如果不一致说明你从 MBR 读到的分区表和 DBR 里自述的分区位置矛盾这块盘的结构被动过后面所有计算都得重新核对。4. 目录项与簇号32 个字节如何扛起一个文件的全部身份4.1 32 字节字段表文件名的位置比想象中更紧FAT32 把目录也当成文件来存目录的内容就是一串 32 字节的目录项顺序排列直到遇到首字节为 0x00 的项表示结束。每个目录项描述一个文件或子目录偏移长度含义0x008主文件名8.3 格式不足补空格0x083扩展名0x0B1属性0x10 目录、0x20 归档、0x0F 长文件名项0x0C1NT 保留一般置 00x0D1创建时间的 10 毫秒位0x0E2创建时间0x102创建日期0x122最后访问日期0x142起始簇号高 16 位0x162最后修改时间0x182最后修改日期0x1A2起始簇号低 16 位0x1C4文件大小字节在 WinHex 里把光标放到根目录区每 32 字节就是一个整齐的分组。首字节为 0xE5 表示这个目录项已被删除文件名首字节为 0x00 表示后面没有更多项了。子目录里的 “.” 和 “..” 也是真实的目录项分别指向自己和父目录别当成普通文件名处理。本例中根目录区里能看到一个卷标项、一个自动创建的 docs 目录项以及后续在 docs 目录里找到的 note.txt 目录项。卷标项的属性字节是 0x08它占一个目录项位置但不代表任何文件遍历时按属性过滤掉即可。4.2 起始簇号为什么被劈成两半FAT32 的簇号是 32 位整数但目录项里只给了两个 16 位的槽0x14 处 2 字节存高位0x1A 处 2 字节存低位。中间隔着修改时间、修改日期这些字段看起来毫无章法。原因是这套目录项格式定型的年代32 位簇号还远没有普及设计师需要留出位置给时间戳后来扩到 32 位簇号时只能在原有结构里找空隙塞。拼装公式是起始簇号 低 16 位 高 16 位 × 6553665536 就是 2 的 16 次方高位的权重。本例中 docs 目录的簇号是 6note.txt 的簇号是 7两个值都很小高位字段全是 0所以只看低 2 字节也能算对。这是最阴险的一个陷阱在小分区上练习时一切正常换到用了几百 GB 的盘上高位不为零只读低位就会算出一个完全错误的簇号然后指向一片毫无意义的数据。def parse_entry(raw): if raw[0] in (0x00, 0xE5): return None # 结束标记或已删除 if raw[0x0B] 0x0F: return None # 长文件名项单独处理 name raw[0x00:0x08].decode(ascii, replace).rstrip() ext raw[0x08:0x0B].decode(ascii, replace).rstrip() attr raw[0x0B] hi struct.unpack(H, raw[0x14:0x16])[0] # 簇号高 16 位 lo struct.unpack(H, raw[0x1A:0x1C])[0] # 簇号低 16 位 size struct.unpack(I, raw[0x1C:0x20])[0] return {name: name . ext, attr: attr, cluster: lo hi * 65536, size: size}返回的cluster就是后面一切计算的起点。attr保留下来是为了区分目录0x10和普通文件0x20两者在后续遍历时的处理逻辑不同目录的内容是更多目录项文件的内容才是真正的数据。4.3 从簇号到物理扇区三次加法缺一不可现在手上有了簇号要算出物理扇区还得做两步换算。第一步把簇号换成数据区内的偏移第二步把分区内偏移换成磁盘绝对位置。簇号到数据区偏移要减 2因为簇 0 和簇 1 不存在于数据区。簇 N 的数据区偏移等于(N - 2) × 每簇扇区数。docs 目录簇号 6(6 - 2) × 16 64数据区起始 32768所以分区内扇区号是 32832。note.txt 簇号 7(7 - 2) × 16 80分区内扇区号 32848。分区内到物理要加分区起点 2048。docs 目录的物理扇区是32832 2048 34880note.txt 的物理扇区是32848 2048 34896。def cluster_to_abs(part_start, data_start, cluster, spc): part_start: MBR 给出的分区起始 LBA data_start: BPB 算出的数据区起始分区内 cluster: 目录项里的 32 位簇号 spc: 每簇扇区数 return part_start data_start (cluster - 2) * spc print(cluster_to_abs(2048, 32768, 6, 16)) # 34880docs 目录 print(cluster_to_abs(2048, 32768, 7, 16)) # 34896note.txt 文件两个结果和 WinHex 里跳转看到的完全一致。注意 note.txt 也可以用另一种算法验证docs 目录在 348801 簇等于 16 个扇区如果两个簇在 FAT 表里是连续的下一个簇的起点就是34880 16 34896。两种算法一致说明 FAT 链在这里是顺序分配的。如果两者不一致说明文件被碎片化了FAT 表里记录的下一簇号跳到了别处这时候必须沿着 FAT 链一跳一跳地走。4.4 全链条复算把六个关键数字串成一条线步骤数据来源数值分区起始扇区MBR 分区表偏移 82048保留扇区数DBR BPB 偏移 0x0E3234FAT 表个数DBR BPB 偏移 0x102每个 FAT 表扇区数DBR BPB 偏移 0x2414767数据区起始3234 2 × 1476732768每簇扇区数DBR BPB 偏移 0x0D16docs 目录簇号目录项偏移 0x1A6docs 物理扇区2048 32768 (6-2)×1634880note.txt 簇号docs 目录区内目录项7note.txt 物理扇区2048 32768 (7-2)×1634896这张表就是整篇文章的核心。每一个数字都能在 WinHex 的十六进制视图里指出来每一步运算都能用代码复现。如果你能从任意一块 FAT32 盘出发把这张表重新填一遍并和工具跳转的结果对上这条地址链就算真正掌握了。5. 避坑这五处最容易翻车5.1 直接跳 32768 看到一片空白现象按公式算出根目录在 32768在 WinHex 里跳过去看到的是一堆零或者不相干的数据完全找不到目录项。原因32768 是分区内扇区号不是物理扇区号。WinHex 在物理磁盘视图下跳转是按绝对 LBA 解释的它不知道你心里的坐标系是分区内的。解决加上分区起始扇区 2048跳 34816 才能看到根目录的起始。判断自己有没有踩这个坑有个简单办法跳过去之后看首个目录项的位置如果整片区域全是 0x00十有八九是偏移错了因为真实的根目录不可能从第一字节开始就是空的。5.2 拿簇号当扇区号直接用现象从目录项里读到簇号 6直接在 WinHex 里跳到 6 号扇区看到的是 MBR 里的引导代码根本不是目标目录。原因簇号和扇区号是两个量纲。簇号是数据区内的逻辑编号从 2 起算扇区号是物理地址从 0 起算。中间的换算要经过“减 2 乘以每簇扇区数”和“加数据区起始”两道手续。解决永远走完整公式不要图省事。临时用手算时先把data_start和spc两个值写在纸上再代入簇号能避免大部分心算错误。5.3 只读簇号低 2 字节现象在小分区上演练时一切正常换到一块用得很满的大盘上算出来的簇号跳过去是别人的数据或者干脆落在文件系统管理区里。原因目录项里簇号低 2 字节在偏移 0x1A高 2 字节在偏移 0x14中间隔着好几个时间字段。很多手写脚本图快只读 0x1A在小盘上高位恰好是 0看不出问题簇号超过 65535 之后高位非零结果就错了。解决拼装公式写死lo hi * 65536不要有任何“高位一般是 0”的假设。写完脚本可以拿一个大文件试一下看算出的簇号是否大于 65535能覆盖到高位就说明代码没问题。5.4 把 FAT32 的根目录当成固定区域现象照着 FAT16 的资料把根目录位置算成“FAT 表之后固定 32 个扇区”结果遍历出来的目录项错乱文件名读出来是一堆乱码。原因FAT32 的根目录已经改成普通簇链BPB 里的根目录项数字段是 0根目录起始簇号写在偏移 0x2C通常是 2。套用 FAT12/16 的固定区域模型必然出错。解决先看 BPB 偏移 0x52 处的文件系统类型字符串是不是 “FAT32 ”确认之后再按簇链方式读根目录。用簇链方式读还有个好处根目录项数超过一个簇时能自动沿着 FAT 表继续往下读不会读一半就断掉。5.5 默认每扇区 512 字节现象在一块较新的盘上所有偏移量都算错跳转位置系统性地偏移了一截而且偏移量随扇区号增大而线性放大。原因部分磁盘的物理扇区是 4096 字节虽然逻辑扇区对外仍报告 512但如果你的工具或脚本按 4096 去乘偏移或者反过来在 4K 原生盘上按 512 乘都会整体错位。解决偏移计算里的扇区大小一律从 BPB 的 0x0B 字段读不要硬编码 512。读取物理磁盘时也要确认工具报告的逻辑扇区大小WinHex 的状态栏会显示当前扇区尺寸跳转前扫一眼能省掉大量排查时间。6. 设计取舍为什么文件系统不肯直接记住物理扇区把整条链路走完一个自然的疑问会浮上来既然每次访问文件都要做这么多次加法和一次 FAT 链遍历为什么不干脆在目录项里直接存物理扇区号一步到位答案藏在“分区是可以整体搬家的”这件事里。一块盘做镜像克隆到另一块盘分区起始 LBA 可能从 2048 变成 4096用工具调整分区大小数据区整体后移把分区做成镜像文件挂载物理位置根本不存在。如果目录项里存的是物理扇区号上述每一个操作都要把整盘所有目录项重写一遍成本高到不可接受。存簇号则完全解耦簇号是分区内的相对编号分区怎么平移簇号都不变只要改一下 MBR 里的一个 4 字节字段就行。代价也很明确。每次文件访问都要经过“目录项 → 簇号 → FAT 链 → 数据区偏移 → 分区偏移 → 物理扇区”这条链链上任何一环出错指向的就是完全错误的数据而且错误往往不会报错只会静默地读到别人的内容。这就是为什么数据恢复这行当里交叉验证是铁律BPB 算出的数据区起始、FAT 表里记录的簇链、目录项里的簇号三处必须能互相印证。另一个取舍是 FAT 链本身的形式。FAT32 用链表串起文件的各个簇优点是实现简单、删除文件只需改一个表项缺点是随机访问要先走一遍链大文件读到尾部时前面的表项全被扫过。后来的文件系统改用索引结构把数据块地址直接放在 inode 里多级指针一跳到位本质上是拿空间换随机访问性能。FAT32 单文件 4GB 的上限也是同一批取舍的产物文件大小字段只有 4 字节目录项格式一旦定下来再改就要破坏兼容性。我一般会这么理解这套机制它不是设计得不好而是在 1980 年代的硬件约束下做出了当时最优的解然后被兼容性一路绑到了今天。也正因为如此理解它的每一层偏移比记住任何工具的操作步骤都值钱。从那以后我每次分析一块盘都强制先把三个锚点写下来对着看分区起始 LBA、数据区起始扇区、根目录簇号。三个数字对不上就说明这块盘的结构被动过绝不往下算。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?