分区表结构解析: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;
- 最多4个主分区——超过就得用扩展分区,嵌套逻辑复杂
- 最大支持2TB磁盘——因为LBA地址只用32位
- 没有校验和——分区表被写坏,没有任何检测手段
- 引导代码和分区表挤在512字节里——改分区表可能连带破坏引导
我在一个工业平板项目里就踩过这个坑。当时OTA升级只更新了分区表,结果引导代码被意外覆盖,设备直接变砖。后来拆机用JTAG才救回来。从那以后,我对MBR就特别警惕。
GPT:现代系统的选择
GPT(GUID分区表)是UEFI规范的一部分,它彻底解决了MBR的痛点。我个人的习惯是,只要是新项目,一律用GPT。哪怕磁盘只有32GB,我也用GPT——因为它的校验和机制太重要了。
GPT在磁盘上的布局是这样的:
- LBA 0:保护MBR(防止老工具误识别)
- LBA 1:GPT头部(主表)
- LBA 2 ~ 33:分区表项数组(主表,默认128项)
- LBA 34 ~ 磁盘末尾-33:数据区域
- 磁盘末尾-32 ~ 末尾-1:分区表项数组(备份表)
- 磁盘末尾: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的校验和机制是分层设计的:
- 头部自校验:
header_crc32保护GPT头部本身 - 分区表校验:
partition_array_crc32保护所有分区表项 - 主备互校验:主表和备份表互相引用对方的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分区表布局与校验流程
小结
MBR和GPT的差异,说白了就是「能用」和「可靠」的区别。MBR设计于软盘和几十MB硬盘的年代,它根本没想到今天会有TB级存储和OTA升级的需求。GPT的校验和机制、主备双表结构,才是做分区表回滚保护的真正基础。