MBR 分区表详解:MBR 结构、主分区与扩展分区、引导代码区、MBR 的局限性

说到 MBR,很多做嵌入式开发的朋友第一反应是「老古董」。确实,现在 UEFI + GPT 已经成了主流,但我在做嵌入式 bootloader 和烧录工具的时候,发现 MBR 依然大量存在于 ARM 板卡、老旧工控设备、以及一些定制 Linux 系统中。你想想看,一个 10 年前的工业平板,它的 NAND Flash 上跑的就是 MBR 分区表。不懂它,你连分区偏移都算不对。

MBR 的全称是 Master Boot Record,它位于存储设备的第一个扇区(LBA 0),大小固定为 512 字节。这 512 字节里,塞了三个东西:引导代码、分区表、以及一个魔数签名。我习惯把这 512 字节拆成三块来看,这样结构就清晰了。

MBR 的 512 字节结构

咱们直接看布局,我画了一张图,方便你对照着理解。

MBR 512 字节结构图 MBR 512 字节结构(LBA 0) 引导代码区(Boot Code) 偏移 0x000 ~ 0x1BD | 大小:446 字节 存放第一阶段引导程序(IPL),负责加载活动分区引导扇区 主分区表(Partition Table) 偏移 0x1BE ~ 0x1FD | 大小:64 字节 4 个分区表项,每项 16 字节 包含:引导标志、CHS/LBA 地址、分区类型、大小 魔数签名(Boot Signature) 偏移 0x1FE ~ 0x1FF | 固定值 0x55AA | 2 字节 总计:446 + 64 + 2 = 512 字节,一个扇区刚好塞满

这张图我建议你保存下来。每次解析 MBR 的时候,对着偏移量去读,基本不会出错。我曾经在调试一个 SD 卡启动的板子时,发现分区表死活读不对,最后用 hexdump 一看,原来是 0x55AA 被写成了 0xAA55,字节序搞反了。这种坑,遇到了你就知道有多头疼。

引导代码区:不只是个跳板

引导代码区占了 446 字节,听起来不小,但实际能放的代码非常有限。早期的 BIOS 会读取 MBR 到 0x7C00 地址,然后跳过去执行。这段代码的任务很简单:找到活动分区,加载它的 VBR(Volume Boot Record),然后跳转。

我见过很多嵌入式工程师直接跳过引导代码区,只关心分区表。但如果你在做烧录工具,尤其是做裸机烧写的时候,引导代码区其实是个关键。举个例子,有些 ARM 芯片的 ROM 启动代码会检查 MBR 的引导代码区是否包含特定的跳转指令,如果没有,它就不认这个设备是可启动的。

引导代码区的典型内容(x86 环境):

; 伪代码示意
mov ax, 0x07C0
mov ds, ax
; 检查分区表项 0 的引导标志
cmp byte [0x7DBE], 0x80
jne not_bootable
; 加载活动分区的第一个扇区到 0x7C00
int 0x13, ah=0x02
; 跳转执行
jmp 0x07C0:0x0000

嗯,这里要注意:引导代码区不是必须的。如果你的设备是从 SPI Flash 或者 NAND 启动,MBR 可能只用来存分区表,引导代码区全是 0。我在做某款路由器固件时,它的 bootloader 直接跳过 MBR 引导代码,从固定偏移加载内核。所以,别默认 MBR 的引导代码一定能用。

主分区与扩展分区:4 个不够怎么办?

MBR 分区表只有 64 字节,每项 16 字节,所以最多只能有 4 个主分区。这是硬限制,没办法。那如果我想分 5 个区怎么办?这时候就引入了扩展分区的概念。

说白了,扩展分区是一个「容器」。你可以在扩展分区内部再创建逻辑分区。扩展分区本身占一个主分区表项,但它指向的不是一个普通的数据区,而是一个 EBR(Extended Boot Record)链。

分区类型 数量限制 特点 我遇到的坑
主分区 最多 4 个 可直接引导,分区表在 MBR 中 有些 BIOS 只认第一个主分区可引导
扩展分区 最多 1 个 占用一个主分区表项,内部可嵌套逻辑分区 扩展分区本身不能格式化,只能存逻辑分区
逻辑分区 理论上无限制 通过 EBR 链式管理,每个 EBR 指向下一个 EBR 链断裂后,后面的逻辑分区全丢

我记得有一次,客户反馈说他们的 32GB SD 卡在设备上只能识别前 4 个分区。我查了半天,发现是烧录工具在创建扩展分区时,EBR 的链式指针写错了,导致第 5 个分区之后的逻辑分区全部不可见。从那以后,我写分区工具时都会加一个 EBR 链完整性校验。

分区表项的 16 字节结构

每个分区表项虽然只有 16 字节,但信息量很大。我拆开给你看:

偏移 0x00: 引导标志 (1 字节)
         0x80 = 可引导(活动分区)
         0x00 = 不可引导

偏移 0x01: 起始 CHS 地址 (3 字节)
         磁头、扇区、柱面(老式寻址,现在基本用 LBA)

偏移 0x04: 分区类型 (1 字节)
         0x0C = FAT32 (LBA)
         0x83 = Linux 原生分区
         0x07 = NTFS
         0x05 = 扩展分区

偏移 0x05: 结束 CHS 地址 (3 字节)

偏移 0x08: 起始 LBA 地址 (4 字节)
         小端序,单位是扇区

偏移 0x0C: 分区总扇区数 (4 字节)
         小端序,最大 2^32 扇区 ≈ 2TB

我的习惯: 在解析分区表时,优先使用 LBA 地址,不要碰 CHS。CHS 在大于 8GB 的磁盘上基本是无效的,很多 BIOS 和 bootloader 会返回错误的 CHS 值。直接用 LBA,简单可靠。

MBR 的局限性:2TB 墙与 4 分区限制

聊完结构,咱们得直面 MBR 的硬伤。这些局限性不是理论问题,是你在实际项目中一定会撞到的墙。

  • 最大磁盘容量 2TB:分区表项中的起始 LBA 和扇区数都是 32 位,最大寻址 2^32 扇区,每扇区 512 字节,算下来就是 2TB。超过 2TB 的磁盘,MBR 根本认不全。我遇到过有人把 4TB 硬盘用 MBR 分区,结果只显示了 2TB,剩下的空间「消失」了。
  • 最多 4 个主分区:这是硬编码的限制。虽然可以用扩展分区绕过去,但扩展分区的嵌套层数多了,兼容性会下降。有些老旧 BIOS 在读取嵌套超过 3 层的逻辑分区时会崩溃。
  • 没有备份机制:MBR 只有一个,存储在 LBA 0。如果这个扇区被损坏(比如意外断电、坏块),整个分区表就丢了。我在做工业级烧录工具时,都会额外在 LBA 1 或 LBA 2 备份一份 MBR 副本,虽然这不是标准做法,但救过我好几次。
  • 引导代码区空间不足:446 字节放一个完整的 bootloader 远远不够。现在的 bootloader(如 GRUB2、U-Boot)都采用多阶段引导,MBR 里的代码只负责加载第二阶段的 bootloader。

警告: 如果你在烧录工具中处理大于 2TB 的存储设备,请直接使用 GPT 分区表。强行使用 MBR 会导致数据不可访问。我曾经在客户现场帮人恢复过一块 3TB 硬盘的数据,就是因为用了 MBR 分区,后面的空间被错误地覆盖了。血的教训。

实际项目中的 MBR 操作要点

最后,我总结几个在烧录工具开发中处理 MBR 的实操要点,都是我自己踩过的坑:

  1. 字节序:MBR 分区表使用小端序(Little-Endian)。x86 平台没问题,但如果你在 ARM 或 RISC-V 上解析 MBR,记得做字节序转换。我见过有人直接在 ARM 上 memcpy 读取分区表,结果 LBA 地址全反了。
  2. 分区对齐:现代存储设备(尤其是 SSD 和 eMMC)要求分区起始地址对齐到 1MB 或 4KB。MBR 本身不强制对齐,但不对齐会导致性能下降。我习惯在创建分区时,将起始 LBA 设置为 2048(即 1MB 对齐),这是业界惯例。
  3. 保护 MBR:在 GPT 磁盘上,LBA 0 仍然是一个 MBR,叫做 Protective MBR。它的分区表项只有一个类型为 0xEE 的分区,覆盖整个磁盘。这样做是为了让不支持 GPT 的老旧工具不会误认为磁盘是空的。你在做烧录工具时,如果检测到分区类型 0xEE,就应该按 GPT 来处理。
  4. 校验 0x55AA:每次读取 MBR 后,务必检查最后两个字节是否为 0x55AA。如果不是,说明 MBR 损坏或者这个扇区根本不是 MBR。我在烧录工具中加了自动修复功能,如果校验失败,会尝试从备份扇区恢复。