简介:这是一份面向IT运维、数据恢复与安全分析初学者的PPT课件,聚焦用WinHex查看磁盘底层数据并定位文件首个扇区。内容围绕MBR与分区表、DBR、FAT32根目录及目录项展开,串联分区起始扇区、FAT表位置、簇号与扇区号换算等关键概念,帮助读者理解卷内偏移与物理磁盘扇区的对应关系,适合数据恢复、系统调试与取证入门等需要理解磁盘组织方式的场景。压缩包内共1个pptx文件,约1.1MB,体量轻便,适合课堂讲解或按页自学。已有79人学习,反映出该主题在文件系统入门与取证基础场景中的关注度。课件将抽象结构落到具体定位思路上,便于掌握目录项字段、簇大小换算等易错点,为排查文件丢失、分析磁盘布局提供可复用的知识框架。
1. 从 34896 这个数字说起:WinHex 定位文件首扇区的完整链路
数据恢复现场最尴尬的一幕,是在 WinHex 里按计算器算了半天,跳过去发现一片 00。我见过不止一个人卡在这一步——文件明明还在资源管理器里躺着,扇区号却怎么都对不上。用 WinHex 找到磁盘中文件所在的第一个扇区位置,本质上只做三件事:从 MBR 分区表拿到分区起点,从 DBR 拿到 FAT32 的布局参数,再从 32 字节目录项里拼出起始簇号,最后做一次簇到扇区的换算。
链路不长,但每一环都可能因为「分区内相对扇区」和「物理磁盘绝对扇区」混用而翻车。这也是操作系统课程讲文件系统时最容易一笔带过、实操里却最要命的一段:教材画的是逻辑结构图,真机上你拿到的是十六进制字节,中间差着一层偏移换算。下面把每一步的字段偏移、手算结果和脚本校验都摊开,适合做数据恢复、镜像取证,或者单纯想把 FAT32 吃透的人照着走一遍。
2. 先分清三层坐标:物理扇区、分区内扇区、簇
2.1 MBR 那 64 字节分区表决定了起点偏移
磁盘的 0 号扇区就是 MBR,一共 512 字节,结构非常固定:前 446 字节是引导代码,接着 64 字节是分区表,正好放 4 个分区表项、每项 16 字节,最后两个字节是结束标识 0x55AA。这 16 字节里真正影响定位的只有两个字段——分区类型和起始 LBA。
| 项内偏移 | 长度 | 字段 | 定位时的用途 |
|---|---|---|---|
| 0x00 | 1 | 分区状态(0x80 为活动) | 判断是否可引导,定位时基本不看 |
| 0x01 | 3 | 起始 CHS 地址 | 历史遗留,现代磁盘忽略 |
| 0x04 | 1 | 分区类型(0x0B/0x0C 为 FAT32) | 确认该分区是不是你要找的文件系统 |
| 0x05 | 3 | 结束 CHS 地址 | 忽略 |
| 0x08 | 4 | 起始 LBA(小端) | 关键:分区在物理磁盘上的起点 |
| 0x0C | 4 | 分区总扇区数(小端) | 用来判断跳转是否越界 |
第一个分区表项从 0x1BE 开始,所以起始 LBA 落在 0x1BE + 0x08 = 0x1C6 这四个字节上。以文中这块盘为例,这四个字节读出来是 00 08 00 00,小端解释就是 0x00000800,即 2048 号扇区。也就是说,这个分区的第一个扇区在物理磁盘的第 2048 号扇区,分区内的一切相对偏移都要加上这个 2048 才是物理磁盘上的真实位置。
这里有个容易被忽略的前提:MBR 分区表只描述主分区,如果你的目标分区是逻辑分区,起点藏在扩展分区的 EBR 链里,得顺着链一节一节找。常见做法是先确认分区是主分区再往下走,否则 0x1C6 读出来的可能是扩展分区容器的起点,不是文件系统真正的 DBR 位置。
2.2 DBR 里 FAT32 的四个参数,撑起整个数据区
分区的第一个扇区叫 DBR(DOS Boot Record),FAT32 的布局参数全在这里。定位文件首扇区只需要四个值:每扇区字节数、每簇扇区数、保留扇区数、FAT 表个数和 FAT 表大小。它们的偏移是 FAT32 规范写死的,不随格式化工具变化。
| DBR 偏移 | 长度 | 字段 | 本文案例取值 |
|---|---|---|---|
| 0x0B | 2 | 每扇区字节数 | 512 |
| 0x0D | 1 | 每簇扇区数 | 16 |
| 0x0E | 2 | 保留扇区数(FAT 表起始,分区内相对) | 3234 |
| 0x10 | 1 | FAT 表个数 | 2 |
| 0x11 | 2 | 根目录项数(FAT32 固定为 0) | 0 |
| 0x24 | 4 | 每个 FAT 表占用的扇区数 | 14767 |
| 0x2C | 4 | 根目录起始簇号(通常为 2) | 2 |
| 0x52 | 8 | 文件系统类型标记,内容为 "FAT32" | FAT32 |
FAT32 的数据区起点算法是:保留扇区数 + FAT 表个数 × 每个 FAT 表扇区数。代入案例数字,3234 + 2 × 14767 = 32768。这个 32768 是分区内相对扇区号,它同时也是根目录区的起点——因为 FAT32 的根目录不再像 FAT16 那样占用固定区域,而是当成一个普通目录放在数据区里,簇号固定为 2,所以数据区起点就等于簇 2 的起点,也等于根目录区起点。
为什么 FAT32 要这么设计?因为 FAT16 的根目录项数写死在 DBR 里(比如 512 项),根目录一满就没法再加文件;FAT32 把根目录下放到数据区、用簇链管理,根目录能像子目录一样动态增长。代价就是你不能再假设「根目录就在 DBR 后面」,必须自己算一次。
再强调一遍边界:这套算法只对 FAT32 成立。NTFS 的 MFT 起点要从 $MFT 的引导记录里读,exFAT 的簇堆起点在 0x58 处,套用上面的公式一定错。拿到一块盘,第一件事是看 0x52 处的类型标记,对不上就别往下算了。
2.3 把换算写成脚本,别用计算器手敲
三个数字乘加一次,手算不会错;但一条链路要算五六次,每次都要在「分区内」和「物理」之间切换,手动敲计算器就是自找麻烦。我一般会先写个参数化的小脚本,把整条换算固化下来,后面在 WinHex 里跳转时直接抄输出结果。
# fat32_sector_map.py # 作用:把 MBR 分区表参数 + DBR 参数 + 簇号,换算成物理磁盘绝对扇区号 PART_START_LBA = 2048 # 来源:MBR 偏移 0x1C6 处的 4 字节小端值(分区表项内偏移 0x08) RESERVED_SEC = 3234 # 来源:DBR 偏移 0x0E,保留扇区数,也是 FAT 表的起始相对扇区 FAT_COUNT = 2 # 来源:DBR 偏移 0x10 FAT_SIZE_SEC = 14767 # 来源:DBR 偏移 0x24(FAT32 用 4 字节版本,注意别读 0x16 的 2 字节) SEC_PER_CLUS = 16 # 来源:DBR 偏移 0x0D # 分区内相对坐标:数据区起点 = 保留扇区 + FAT 表个数 * 单个 FAT 大小 DATA_START_REL = RESERVED_SEC + FAT_COUNT * FAT_SIZE_SEC ROOT_DIR_REL = DATA_START_REL # FAT32 根目录簇号固定为 2,所以根目录区 = 数据区起点 def clus_to_abs(clus: int) -> int: """簇号 -> 物理磁盘绝对扇区号。减 2 是因为 FAT 数据区从簇 2 开始编号。""" rel = DATA_START_REL + (clus - 2) * SEC_PER_CLUS return PART_START_LBA + rel if __name__ == "__main__": print(f"数据区起点(分区内相对) = {DATA_START_REL}") print(f"根目录区 相对 {ROOT_DIR_REL} 绝对 {PART_START_LBA + ROOT_DIR_REL}") for c in (2, 6, 7): print(f"簇 {c}: 相对 {DATA_START_REL + (c - 2) * SEC_PER_CLUS} 绝对 {clus_to_abs(c)}")跑出来的结果和手工链路完全对齐:数据区起点 32768,根目录绝对扇区 34816,簇 6 落在 34880,簇 7 落在 34896。后面所有跳转都拿这几个数去对,对不上就说明参数抄错了,不用在现场一边翻计算器一边怀疑人生。
这几个常量的来源必须逐个核对,不能想当然。RESERVED_SEC有的盘是 32,有的盘是 3234,完全由格式化工具决定;FAT_SIZE_SEC在 FAT32 里必须读 4 字节版本,0x16 处的 2 字节字段在 FAT32 中约定写 0,读它会得到 0,然后整个数据区起点算成 3234,跳过去就是 FAT 表内部,看到的内容全是簇链,很容易误判成「目录被覆盖了」。
3. WinHex 实操:从打开物理磁盘到跳到 34816
3.1 打开磁盘与数据解释器的正确姿势
启动 WinHex 时建议以管理员身份运行,否则「打开磁盘」列表里可能只显示逻辑驱动器、看不到物理磁盘,或者能看不能读。菜单里选 文件 → 打开磁盘,在弹出的列表里选物理磁盘那一层(标识是 Physical Disks / HDD),不要选下面的某个分区盘符。这一步决定了你后续所有偏移的基准:从物理磁盘打开,偏移是绝对扇区;从分区盘符打开,偏移是分区内相对扇区,两者差着 2048,混用就是第一类翻车的来源。
打开后先看 0 号扇区。前 446 字节是一片引导代码,跟着就是分区表。把光标点到 0x1C6,然后走 查看 → 显示 → 数据解释器。数据解释器会把光标处的字节按不同宽度解释出来,其中 4 字节无符号整型那一行会直接显示成 2048。这里要注意字节序:WinHex 默认按当前 CPU 的小端顺序解释,如果你手动切过大端,同样的字节会显示成 0x00080000,差了 256 倍。发现数字离谱,先回头看解释器设置,而不是怀疑盘坏了。
顺便核对一下 0x1FE 处的 0x55AA 结束标识。它本身不参与定位,但它是判断「这 512 字节到底是不是一个合法 MBR」最快的办法。有些整盘镜像的开头被别的数据覆盖过,0x55AA 不在,说明你手上的镜像开头就有问题,后面所有偏移都不可信。
3.2 读 MBR 分区表,取出 2048
在物理磁盘视图下,按 Alt+G 或者走 定位 → 转到偏移量,把单位切换成扇区,输入 0 回车,回到 MBR。再定位到 0x1C6,读这 4 个字节。案例里是 00 08 00 00,小端即 0x0800 = 2048。
这个 2048 不是随便来的。传统上分区从 63 号扇区开始(磁道对齐),现代大容量盘普遍用 2048(1MB 对齐),目的是让分区的起始位置跟闪存页、机械盘的物理扇区边界对齐,减少读改写放大。所以你在不同年份格式化的盘上看到的起点可能是 63、2048、4096,没有对错之分,只有「按读到的值算」这一条原则。
拿到 2048 之后,立即验证一下:跳转到 2048 号扇区,看开头是不是一个像样的 DBR——偏移 0x03 处应该有 "FAT32" 字样(有些格式化工具写 "MSDOS5.0"),0x1FE 处应该有 0x55AA。这两处对上了,说明 0x1C6 读到的起始 LBA 是真的指向文件系统引导扇区,而不是指向某个中间层容器。
3.3 按 DBR 算根目录区并处理分区内偏移
在 2048 号扇区这个 DBR 里,依次读四个参数:0x0D 处一个字节 = 16(每簇扇区数),0x0E 处两个字节 = 3234(保留扇区数),0x10 处一个字节 = 2(FAT 表个数),0x24 处四个字节 = 14767(FAT 表大小)。四个数一凑,根目录区相对扇区 = 3234 + 2 × 14767 = 32768。
这里就是整条链路上最容易翻车的地方。直接在 WinHex 里跳到 32768,看到的是一片空白或者一堆 00——因为 32768 是分区内的第 32768 个扇区,而当前视图的基准是物理磁盘。换算成物理扇区要加分区起点:32768 + 2048 = 34816。跳到 34816,才能看到真正的根目录区,里面是卷标项、若干目录项和文件项。
文档里那句「跳转到 32768 扇区什么都没有」,很多人第一次看会以为是操作错了,其实是坐标系没说清。养成习惯:只要是从 DBR 参数算出来的数,一律先标记成「分区内相对」,要落到 WinHex 的物理磁盘视图上,必须再过一次+ PART_START_LBA。用第 2 章的脚本,这一步是自动完成的,不会漏。
4. 拆目录项:32 个字节里怎么拼出起始簇号 7
4.1 目录项的字段布局
FAT32 的目录区就是一串 32 字节的记录,一个文件或目录占一项(长文件名会占多项)。字段偏移是固定的,最需要关注的是起始簇号的高 16 位和低 16 位——它们被故意拆开放置,是为了兼容 FAT12/16 的旧结构。
| 项内偏移 | 长度 | 字段 | 说明 |
|---|---|---|---|
| 0x00 | 11 | 短文件名(8.3 格式) | 首字节 0xE5 表示已删除,0x00 表示后面没有更多项 |
| 0x0B | 1 | 属性 | 0x10 目录、0x20 归档文件、0x0F 长文件名项 |
| 0x0D | 1 | 创建时间(十分之一秒) | 精度字段,定位时忽略 |
| 0x14 | 2 | 起始簇号高 16 位 | FAT32 新增,FAT12/16 中为 0 |
| 0x1A | 2 | 起始簇号低 16 位 | 老字段,FAT12/16 直接用它 |
| 0x1C | 4 | 文件大小(字节) | 目录项此字段为 0 |
拼簇号的公式是:低 16 位 + 高 16 位 × 65536。案例里 demo.txt 的目录项,0x1A 处读出来是 07 00(小端 7),0x14 处是 00 00,拼出来簇号就是 7。test 目录那一项的 0x1A 处是 06 00,簇号 6。
看目录项时还有个细节:属性字节为 0x0F 的项是长文件名项,它只是名字的碎片,不携带簇号,跳过就行。一个文件如果有长文件名,会先出现若干个 0x0F 项,最后才是真正的 8.3 短名项。如果你按顺序数第 N 项去对应第 N 个文件,遇到长文件名就会错位——正确做法是只认属性不等于 0x0F 的项。
下面这段脚本可以直接把从 WinHex 里拷出来的 32 字节解成结构化结果,省得对着字节表数格子。
import struct # 从 WinHex 里选中 32 字节,右键 Copy Hex,粘进来 raw = bytes.fromhex( "44454D4F20202020545854" # 0x00 短文件名 DEMO TXT "20" # 0x0B 属性 0x20 = 归档文件 "00" # 0x0C NT 保留 "00" # 0x0D 创建时间十分之一秒 "0000" "0000" "0000" # 0x0E 创建时间 / 创建日期 / 访问日期 "0000" # 0x14 起始簇号高 16 位 "0000" "0000" # 0x16 写入时间 / 写入日期 "0700" # 0x1A 起始簇号低 16 位 = 7 "00040000" # 0x1C 文件大小 = 1024 字节 ) name = raw[0x00:0x0B].decode("ascii", "replace").rstrip() attr = raw[0x0B] clus_hi = struct.unpack("<H", raw[0x14:0x16])[0] clus_lo = struct.unpack("<H", raw[0x1A:0x1C])[0] size = struct.unpack("<I", raw[0x1C:0x20])[0] cluster = clus_hi * 65536 + clus_lo print(f"文件名={name} 属性=0x{attr:02X} 起始簇号={cluster} 大小={size}")<H和<I里的<明确指定小端,这是唯一正确的读法,别依赖运行平台的默认字节序。跑出来起始簇号 7,跟手工从 0x1A 读 07 00 的结果一致。如果这一项是已删除文件,首字节会是 0xE5,其余字段仍然有效,簇号还在,这也是数据恢复能靠目录项找回文件的原因。
4.2 从簇号到扇区:为什么会出现 +16 的写法
拿到簇号 7 之后,换算成扇区号有两种写法,结果一样但适用范围完全不同。
写法一(严格公式):绝对扇区 = 分区起点 + 数据区起点 + (簇号 − 2) × 每簇扇区数 = 2048 + 32768 + (7 − 2) × 16 = 34896。
写法二(相对加一簇):在已经知道 test 目录项所在扇区 34880 的前提下,直接 34880 + 16 = 34896,因为一簇等于 16 个扇区,簇 6 的下一个簇就是簇 7。
两种写法的前提条件不一样。写法二只在「目录项所在簇号 + 1 恰好等于文件起始簇号」时成立,也就是目录刚建好、里面第一个文件紧邻分配的情况。案例里的 test 目录占簇 6、demo.txt 占簇 7,刚好连续,所以能这么算。一旦文件被删过又新建、磁盘有碎片、或者目录里文件超过一个,簇号就不再连续,写法二直接失效,算出来的可能是别的文件的头。
我一般只教写法一,然后用写法二做快速心算校验。两个结果一致,说明参数没错;不一致,优先信严格公式,再去 FAT 表里追簇链看是不是有碎片。FAT 表本身就是一条链表:FAT 表第 N 个 4 字节表项记录簇 N 的下一个簇号,末尾用 0x0FFFFFFF 标记结束。文件跨簇时,顺着这条链走就能得到全部簇号,每个簇再套一次换算,就是文件的完整扇区列表。
还有一个边界要记住:簇号从 2 开始,不是从 0 也开始。0 和 1 是保留项,簇 2 是数据区第一个簇。所有公式里的-2都来自这里,漏掉它整条链路会整体偏移两个簇、32 个扇区,症状是永远找不到文件头,但能在附近看到一堆看起来像内容的字节。
4.3 用 WinHex 的「跳转分区」省掉一次换算
WinHex 打开物理磁盘后,左侧树状结构里会列出解析到的分区节点(Partition 1、Partition 2 之类)。双击进入某个分区节点,视图就切换成该分区的相对坐标系了。这时候再走定位 → 转到偏移量,单位选簇,输入 7,会直接跳到分区内的 32848 号扇区——WinHex 自己帮你做了簇到扇区的乘法,省掉手算。
这个功能的坑在于坐标系会静默切换。你在分区视图里看到的状态栏偏移是相对的,一旦回到物理磁盘节点,同一位置又变成绝对偏移。同一个扇区在两个视图里能差出 2048 甚至更多,来回切换时看清楚标题栏写的是物理磁盘还是分区名,比什么都重要。
案例的操作顺序就是这样:先在物理磁盘视图下跳到 34816 找到根目录区,看到 test 目录项,算出簇号 6;双击分区 1 进入分区视图,跳到簇 6 得到 32832;回到物理磁盘视图,实际位置是 32832 + 2048 = 34880,里面能看到卷标项、test 目录项、test1 文件项。test 目录里再找 demo.txt 的目录项,算出簇号 7;此时可以直接在分区视图跳簇 7,也可以在当前扇区基础上加一簇的 16 个扇区,两个结果都指向 34896。
5. 五条踩坑记录:扇区号对不上时按顺序排查
5.1 定位链路里的三个高频翻车点
现象一:按公式算出的扇区跳过去是一片 00,但文件在系统里明明存在。原因是漏加分区起点。DBR 里算出来的所有数字,包括保留扇区数、FAT 表大小、根目录起点,全是分区内相对坐标;而 WinHex 从物理磁盘打开时用的是绝对坐标,两者差一个PART_START_LBA。解决方式很机械:在草稿纸上给每个中间结果标上「相对」还是「绝对」,跳转前统一换算成绝对,或者干脆全程用第 2 章那个脚本输出,不手算。
现象二:数据解释器显示起始 LBA 是 0x00080000,跳过去完全不对。这不是盘的问题,是数据解释器的字节序被改成了大端。同样的 4 字节00 08 00 00,小端读是 2048,大端读是 0x00080000。WinHex 默认跟随当前平台的小端顺序,但如果你手动动过解释器设置、或者用的是特殊配置,就会静默出错。解决方式是打开数据解释器面板确认字节序选项,并养成交叉验证习惯:读出来的值乘以 512 之后,应该大致符合分区的实际容量,对不上就说明读错了。
现象三:打开磁盘后看不到物理磁盘节点,只能看到 C 盘 D 盘。这是权限问题,不是软件缺陷。非管理员权限下,WinHex 拿不到物理设备的句柄,列表里只剩逻辑卷。在这种情况下所有偏移都是相对某个卷的,跟你手上按 MBR 算出来的绝对扇区号完全是两套坐标系,怎么对都对不上。解决方式是关掉程序、以管理员身份重新启动,再走 文件 → 打开磁盘,选物理磁盘那一层。
5.2 目录项与簇链的两个二次核对
现象四:目录项拼出来的簇号是 0,或者大得离谱(几万、几十万)。两个原因,一是只读了低 16 位没读高 16 位,二是把高地位的位置记反了。FAT32 里高 16 位在项内偏移 0x14,低 16 位在 0x1A,跟 FAT12/16 时代的布局是反的——很多人凭印象去 0x1A 读一个「簇号」,在 FAT32 上读到的只是低半截。簇号 0 在 FAT32 里表示空文件或空目录,如果遇到大小非 0 但簇号为 0 的项,基本可以判定读错位置了。解决方式是严格按偏移表读,并用「簇号 × 每簇扇区数 + 数据区起点」反算一下,看落点是否在分区容量范围内。
现象五:用「目录扇区 + 16」算出文件扇区,结果里面是别的文件内容。这是把连续分配当成了必然。加一簇的写法只在目录和文件恰好占用相邻簇时成立,一旦目录里删过文件、或者文件是通过碎片化分配写入的,下一个簇号根本不属于这个文件。真正的做法是回 FAT 表查:用簇号做索引,读 FAT 表里第 N 个 4 字节表项,得到下一个簇号,直到读到 0x0FFFFFFF 为止,这样拿到的是文件的完整簇链而不是猜测。解决方式是把「+16」彻底降级为心算校验手段,正式计算一律走「簇号 → 严格公式」或者「簇号 → FAT 表链」。
6. 把手工链路固化成校验脚本:三条断言挡住手算错误
手工链路走通一次不难,难的是每次都走通。我在做过几次镜像取证之后养成了一个习惯:把当次的关键数字全部写成断言,跑一遍再开始跳转。断言不过,说明参数抄错了,这时候去 WinHex 里翻是浪费时间;断言全过,后面每一步跳转都有据可查,出问题也能立刻定位到是哪一环算错了。
具体做法是在第 2 章那个换算脚本末尾加一段校验,用案例里的已知结果做基准值。三个数字分别对应根目录区、目录所在簇、文件首簇,覆盖了整条链路的三个关键节点,任何一个环节算错都会在这里暴露。
# verify_chain.py —— 与 fat32_sector_map.py 放在同一目录 from fat32_sector_map import PART_START_LBA, ROOT_DIR_REL, clus_to_abs # 基准值来自已在 WinHex 中人工核对的样本盘,换盘时整组替换 checks = [ ("根目录区", PART_START_LBA + ROOT_DIR_REL, 34816), ("test 目录", clus_to_abs(6), 34880), ("demo.txt", clus_to_abs(7), 34896), ] for label, got, expect in checks: status = "OK" if got == expect else "MISMATCH" print(f"[{status}] {label}: 计算 {got} / 基准 {expect}") # 附加检查:所有落点必须在分区容量范围内 PART_TOTAL_SEC = 40960000 # 来源:MBR 分区表项内偏移 0x0C 的 4 字节小端值 for label, got, _ in checks: assert PART_START_LBA <= got < PART_START_LBA + PART_TOTAL_SEC, f"{label} 越界"最后那条越界检查看着多余,实际上救过我一次。当时把 FAT 表大小从 4 字节字段误读成了 2 字节字段附近的相邻字节,算出来的数据区起点大了一倍,三个基准值全部对不上,脚本第一时间报 MISMATCH,我才回头去核对偏移,而不是在几万个扇区之外瞎跳。如果只做等值比较不做范围检查,遇到「基准值也被抄错」的情况就会一起错下去。
换一块盘的时候,这套脚本的用法是:先把 MBR 和 DBR 里的真实参数填进常量,人工走一遍链路得到三个基准扇区号,再回填进checks。第二次起,任何一次换算都先跑脚本,输出的绝对扇区号直接抄进 WinHex 的跳转框,中间不再经过计算器。这套流程对 FAT32 有效,换到 NTFS 或 exFAT 就得换参数来源,但「先固化换算、再手工验证、最后写成断言」这个顺序是通用的。
从那以后我每次碰磁盘镜像,不管是恢复一个误删文件还是给别人演示文件系统结构,都强制先跑一遍这三个断言再动手,宁可多花两分钟写脚本,也不愿意在对不上的扇区号里靠猜。希望帮到你。
本文还有配套的精品资源,点击获取