分区表基础:存储设备的“房产证”

做BSP开发这么多年,我越来越觉得分区表就像存储设备的“房产证”。没有它,CPU根本不知道哪里能放代码,哪里能存数据。说白了,分区表就是一张地图,告诉Bootloader和操作系统:“你的地盘在哪儿,边界在哪儿。”

我记得刚入行那会儿,有一次调试一块NAND Flash,死活启动不了。查了两天,最后发现是分区表被擦掉了。嗯,从那以后,我每次烧写Bootloader都会先确认分区表还在不在。

分区表到底是什么?

分区表是一段存储在存储设备起始位置的数据结构。它记录了整个存储介质被划分成几个区域,每个区域的起始地址、大小、类型等信息。对于Bootloader来说,分区表决定了它该从哪里加载内核,从哪里读取设备树,又该把根文件系统放在哪儿。

核心要点:分区表是存储设备上第0个扇区(或第0个块)开始的一段元数据。没有它,存储设备就是一块“荒地”。

你想想看,一块64MB的SPI Flash,如果不分区,Bootloader、内核、文件系统全混在一起,升级的时候怎么办?总不能每次都全擦全写吧。分区表就是用来解决这个问题的。

分区类型:主分区、扩展分区、逻辑分区

这里我要先澄清一个概念——我们做嵌入式BSP开发时,接触最多的是MBR(Master Boot Record)分区表。它把分区分为三种类型:

分区类型 最大数量 特点 典型用途
主分区 最多4个 可直接引导系统,分区表项直接写在MBR中 Bootloader分区、内核分区
扩展分区 最多1个 不能直接使用,是逻辑分区的“容器” 容纳多个逻辑分区
逻辑分区 无限制(受地址空间限制) 嵌套在扩展分区内,通过EBR(Extended Boot Record)链式管理 文件系统分区、数据分区

为什么会有这种设计?其实这是历史遗留问题。MBR分区表只有64字节的空间来存放分区信息,每个分区表项占16字节,所以最多只能记录4个分区。后来大家发现4个不够用,才搞出了扩展分区和逻辑分区的概念。

我的经验:在嵌入式系统中,我一般只用主分区。逻辑分区在Bootloader阶段处理起来比较麻烦,很多轻量级Bootloader根本不支持。如果你需要超过4个分区,我建议直接上GPT(GUID Partition Table),而不是用扩展分区绕弯子。

分区表在存储设备中的位置

这个问题很关键。不同的存储设备,分区表的位置略有不同,但原理是相通的。

1. MBR分区表(传统BIOS启动)

MBR位于存储设备的第0个扇区(LBA 0),也就是最开头的位置。它占512字节,结构如下:

偏移量    大小      内容
0x000     446字节    Bootloader代码(第一阶段引导程序)
0x1BE     16字节     第1个分区表项
0x1CE     16字节     第2个分区表项
0x1DE     16字节     第3个分区表项
0x1EE     16字节     第4个分区表项
0x1FE     2字节      结束标志(0x55AA)

看到没?分区表项从偏移0x1BE开始,一共4个,每个16字节。最后两个字节必须是0x55AA,否则BIOS会认为这个设备不可引导。

注意:我曾经遇到过一块Flash,前512字节被意外写坏了,结果MBR里的分区表全丢了。从那以后,我养成了一个习惯——在Flash的末尾备份一份分区表。虽然麻烦,但关键时刻能救命。

2. GPT分区表(UEFI启动)

GPT比MBR先进得多。它不再局限于512字节,而是使用整个存储设备的前34个扇区(LBA 0~33)和后33个扇区作为备份。结构如下:

LBA 0:    Protective MBR(兼容MBR,防止旧工具误操作)
LBA 1:    GPT Header(主GPT头)
LBA 2~33: GPT分区表项(每个分区表项128字节,最多128个分区)
...
LBA -33~-1: GPT分区表项备份
LBA -34:   GPT Header备份

我个人更喜欢GPT。原因很简单:它支持更大的磁盘(超过2TB),分区数量不受限,而且有备份机制。在嵌入式领域,如果你的芯片支持UEFI启动,我建议优先用GPT。

分区表在Bootloader中的实际应用

说了这么多理论,咱们来点实际的。在BSP开发中,分区表通常是在烧写脚本或设备树里定义的。比如在U-Boot中,你可以这样定义分区:

// U-Boot环境变量中的mtdparts定义
mtdparts=nand0:1M(u-boot),4M(kernel),-(rootfs)

// 或者通过设备树描述
partitions {
    compatible = "fixed-partitions";
    #address-cells = <1>;
    #size-cells = <1>;

    partition@0 {
        label = "u-boot";
        reg = <0x00000000 0x00100000>;  // 1MB
    };
    partition@1 {
        label = "kernel";
        reg = <0x00100000 0x00400000>;  // 4MB
    };
    partition@2 {
        label = "rootfs";
        reg = <0x00500000 0x0FB00000>;  // 剩余空间
    };
};

这里要注意,分区表的定义必须和Bootloader的烧写地址严格对应。比如U-Boot烧在0x0~0x100000,内核在0x100000~0x500000。如果对不上,系统就起不来。

避坑指南:我曾经在项目里把内核分区大小写小了,结果内核编译后超过了分区边界,烧写时没报错,但启动时直接挂掉。查了三天才发现是分区大小不够。所以,分区大小一定要留余量,尤其是内核分区,建议多留30%的空间。

分区表与Bootloader的协作关系

为了让你更直观地理解,我画了一张图:

分区表与Bootloader协作关系图 分区表与Bootloader协作关系 存储设备(NAND Flash / eMMC / SPI Flash) 分区表 MBR / GPT Bootloader U-Boot / Barebox 内核分区 zImage / Image 设备树分区 DTB / DTS 根文件系统 rootfs / squashfs 数据分区 Bootloader读取分区表 加载 挂载 分区表决定每个分区的 起始地址和大小

从这张图可以看出,分区表是整个存储布局的“总指挥”。Bootloader启动时,首先读取分区表,找到自己的位置,然后根据分区表找到内核和设备树的位置,最后加载内核并挂载根文件系统。

嵌入式系统中分区表的特殊之处

和PC不同,嵌入式系统的分区表往往不是由用户手动创建的,而是在烧写镜像时就固化好的。我们通常会在烧写脚本里直接指定每个分区的偏移和大小。比如:

# 烧写脚本示例(fastboot)
fastboot flash partition:1 bootloader.bin
fastboot flash partition:2 kernel.img
fastboot flash partition:3 rootfs.img

或者更常见的,通过分区表镜像一次性烧写:

# 使用dd命令烧写整个分区表+数据
dd if=full_image.bin of=/dev/mmcblk0 bs=1M

重要提醒:千万不要在生产环境中随意修改分区表!我见过有同事在产线上为了测试临时改了分区大小,结果忘了改回来,导致后续批次的产品全部变砖。分区表一旦确定,就要作为基线管理起来,任何修改都要走变更流程。

小结

分区表是BSP开发中最基础也最容易忽略的一环。它决定了Bootloader、内核、文件系统在存储设备上的布局。理解主分区、扩展分区、逻辑分区的区别,知道MBR和GPT的位置,能帮你避免很多低级错误。

我个人建议,在开始任何Bootloader开发之前,先把分区表画出来。用一张纸,把每个分区的起始地址、大小、用途写清楚。这张纸,就是你的“藏宝图”。