分区表结构解析:MBR与GPT的底层差异

分区表这东西,说白了就是存储设备的「目录索引」。没有它,操作系统根本不知道数据块从哪开始、到哪结束。我早年做嵌入式存储驱动时,就因为分区表被意外擦掉,整块板子直接变砖——那种感觉,嗯,你懂的。

今天咱们就掰开揉碎,把MBR和GPT这两个分区表格式彻底讲清楚。尤其是GPT的校验和机制,这是实现回滚保护的核心基础。

MBR:老当益壮,但缺陷明显

MBR(主引导记录)诞生于1983年,跟IBM PC/AT一个年代。它放在磁盘的0号扇区(LBA 0),总共512字节。

这512字节的分配是这样的:

偏移 大小 内容
0x000 446字节 引导代码(Boot Code)
0x1BE 16字节 第1个分区表项
0x1CE 16字节 第2个分区表项
0x1DE 16字节 第3个分区表项
0x1EE 16字节 第4个分区表项
0x1FE 2字节 引导签名 0x55AA

每个分区表项16字节,结构如下:

typedef struct {
    uint8_t  status;          // 0x80 = 可引导
    uint8_t  start_head;      // 起始磁头号
    uint8_t  start_sector;    // 起始扇区(低6位)+ 起始柱面高2位
    uint8_t  start_cylinder;  // 起始柱面低8位
    uint8_t  type;            // 分区类型(0x83=Linux, 0x0C=FAT32等)
    uint8_t  end_head;        // 结束磁头号
    uint8_t  end_sector;      // 结束扇区
    uint8_t  end_cylinder;    // 结束柱面
    uint32_t lba_start;       // 起始LBA地址
    uint32_t sector_count;    // 扇区总数
} __attribute__((packed)) mbr_partition_t;
⚠️ MBR的致命缺陷:
  • 最多4个主分区——超过就得用扩展分区,嵌套逻辑复杂
  • 最大支持2TB磁盘——因为LBA地址只用32位
  • 没有校验和——分区表被写坏,没有任何检测手段
  • 引导代码和分区表挤在512字节里——改分区表可能连带破坏引导

我在一个工业平板项目里就踩过这个坑。当时OTA升级只更新了分区表,结果引导代码被意外覆盖,设备直接变砖。后来拆机用JTAG才救回来。从那以后,我对MBR就特别警惕。

GPT:现代系统的选择

GPT(GUID分区表)是UEFI规范的一部分,它彻底解决了MBR的痛点。我个人的习惯是,只要是新项目,一律用GPT。哪怕磁盘只有32GB,我也用GPT——因为它的校验和机制太重要了。

GPT在磁盘上的布局是这样的:

  1. LBA 0:保护MBR(防止老工具误识别)
  2. LBA 1:GPT头部(主表)
  3. LBA 2 ~ 33:分区表项数组(主表,默认128项)
  4. LBA 34 ~ 磁盘末尾-33:数据区域
  5. 磁盘末尾-32 ~ 末尾-1:分区表项数组(备份表)
  6. 磁盘末尾:GPT头部(备份表)

关键点:GPT在磁盘头部和尾部各存了一份完整的头部和分区表项。这就是回滚保护的物理基础——你坏了一份,还有另一份可以恢复。

GPT头部结构详解

GPT头部位于LBA 1,大小通常是92字节,但会填充到512字节。结构如下:

typedef struct {
    uint64_t signature;              // "EFI PART" (0x5452415020494645)
    uint32_t revision;               // 版本号,当前为 0x00010000
    uint32_t header_size;            // 头部大小,通常为92
    uint32_t header_crc32;           // 头部校验和
    uint32_t reserved;               // 保留,必须为0
    uint64_t my_lba;                 // 本头部所在的LBA(主表为1,备份表为末尾)
    uint64_t alternate_lba;          // 备份头部所在的LBA
    uint64_t first_usable_lba;       // 第一个可用数据块的LBA
    uint64_t last_usable_lba;        // 最后一个可用数据块的LBA
    guid_t   disk_guid;              // 磁盘唯一GUID
    uint64_t partition_entry_lba;    // 分区表项数组起始LBA
    uint32_t number_of_entries;      // 分区表项数量(默认128)
    uint32_t entry_size;             // 每个分区表项大小(默认128)
    uint32_t partition_array_crc32;  // 分区表项数组校验和
} __attribute__((packed)) gpt_header_t;

这里有两个CRC32字段:

  • header_crc32:对整个GPT头部(包括自身字段)做CRC32校验
  • partition_array_crc32:对所有分区表项做CRC32校验

💡 我的经验:在嵌入式系统中,每次写入分区表前,先计算好这两个CRC32值。写入后立即回读校验。如果CRC32不匹配,说明写入过程中发生了位翻转或地址错误——这时候就要触发回滚机制,从备份表恢复。

GPT分区表项结构

每个分区表项128字节,结构如下:

typedef struct {
    guid_t   partition_type_guid;    // 分区类型GUID(如ESP、MSR、基本数据)
    guid_t   unique_partition_guid;  // 分区唯一GUID
    uint64_t starting_lba;           // 起始LBA
    uint64_t ending_lba;             // 结束LBA(包含)
    uint64_t attributes;             // 属性位(如只读、隐藏、关键分区)
    uint16_t partition_name[36];     // 分区名称(UTF-16LE,最多36字符)
} __attribute__((packed)) gpt_partition_entry_t;

属性字段的位定义值得关注:

位 含义 说明
0 Required 此分区为系统必需,删除会导致系统无法启动
1 NoBlockIO 固件不应通过块I/O访问此分区
2 LegacyBIOSBootable 传统BIOS可引导
60-63 Type-specific 由分区类型GUID定义的具体用途

⚠️ 关键属性位:位0(Required)在回滚保护中特别重要。我在设计分区表更新策略时,会把bootloader、kernel、rootfs这些关键分区标记为Required。这样在回滚时,系统就知道哪些分区绝对不能动。

校验和机制:GPT的护城河

GPT的校验和机制是分层设计的:

  1. 头部自校验:header_crc32保护GPT头部本身
  2. 分区表校验:partition_array_crc32保护所有分区表项
  3. 主备互校验:主表和备份表互相引用对方的LBA地址

校验流程是这样的:

// 计算头部CRC32(header_crc32字段先置0)
gpt_header_t header;
header.header_crc32 = 0;
uint32_t crc = crc32_calculate(&header, header.header_size);
header.header_crc32 = crc;

// 计算分区表数组CRC32
uint32_t array_crc = crc32_calculate(
    partition_entries,
    number_of_entries * entry_size
);
header.partition_array_crc32 = array_crc;

读取时的校验逻辑:

// 1. 读取主GPT头部
gpt_header_t primary;
read_lba(1, &primary);

// 2. 校验头部CRC32
uint32_t saved_crc = primary.header_crc32;
primary.header_crc32 = 0;
if (crc32_calculate(&primary, primary.header_size) != saved_crc) {
    // 主头部损坏!尝试备份头部
    read_lba(primary.alternate_lba, &backup);
    // ... 校验备份头部
}

// 3. 校验分区表数组CRC32
uint32_t array_crc = crc32_calculate(
    partition_buffer,
    primary.number_of_entries * primary.entry_size
);
if (array_crc != primary.partition_array_crc32) {
    // 分区表损坏!从备份恢复
}

回滚保护的核心逻辑:

  • 写入主表前,先备份当前主表到内存
  • 写入主表后,立即校验CRC32
  • 如果校验失败,从内存中的备份恢复主表
  • 如果主表和备份表都损坏,从磁盘末尾的备份GPT恢复
  • 如果备份GPT也损坏...嗯,那就只能进恢复模式了

我曾经在一个车机项目里遇到过极端情况:OTA升级过程中突然断电,主GPT头部被写了一半。重启后UEFI固件检测到CRC32不匹配,自动从备份GPT恢复。用户甚至没察觉到异常——这就是校验和机制的价值。

MBR vs GPT:一张表说清楚

对比项 MBR GPT
诞生时间 1983年 2000年代(UEFI规范)
最大磁盘 2TB 9.4ZB(理论)
最大分区数 4个主分区 128个(默认,可扩展)
分区表位置 LBA 0(与引导代码共用) LBA 1~33(独立区域)
备份机制 无 头部和尾部各一份
校验和 无 CRC32(头部+分区表)
分区标识 1字节类型码 128位GUID
分区名称 不支持 支持(UTF-16LE,36字符)
回滚保护 几乎不可能 天然支持(主备互备+CRC)

看到这个对比,你应该明白为什么我坚持用GPT了。MBR没有校验和,没有备份,分区表和引导代码还挤在一起——这简直就是为变砖量身定做的设计。

SVG:GPT分区表布局与校验流程

GPT分区表布局与校验流程 LBA 0:保护MBR LBA 1:GPT头部(主表) LBA 2~33:分区表项数组(主表,128项) LBA 34 ~ 磁盘末尾-33:数据区域 磁盘末尾-32 ~ 末尾-1:分区表项数组(备份表) 磁盘末尾:GPT头部(备份表) 主表校验流程 读取LBA 1 → 校验header_crc32 → 校验partition_array_crc32 回滚恢复路径 主表损坏 → 读取备份表 → 校验通过 → 恢复主表 注:主表和备份表互相引用对方的LBA地址(alternate_lba字段),形成交叉验证 header_crc32 partition_array_crc32 主备互引用

小结

MBR和GPT的差异,说白了就是「能用」和「可靠」的区别。MBR设计于软盘和几十MB硬盘的年代,它根本没想到今天会有TB级存储和OTA升级的需求。GPT的校验和机制、主备双表结构,才是做分区表回滚保护的真正基础。