3. Bootloader与分区交互:Bootloader如何读取分区表、启动设备枚举顺序、分区激活标志的作用

这一节,我们来聊聊Bootloader和分区表之间的那点事。说白了,就是Bootloader怎么知道自己该从哪启动、怎么找到正确的系统。

我刚开始做嵌入式的时候,总觉得Bootloader就是个“跳板”,跳过去就完事了。后来被坑过几次才明白——分区交互这块要是没搞透,系统根本起不来。

3.1 Bootloader如何读取分区表

分区表是什么?就是一张“地图”。它告诉Bootloader:你的Flash或者存储设备上,哪块是Bootloader自己,哪块是内核,哪块是文件系统。

常见的分区表格式有这么几种:

分区表类型 存储位置 典型应用
MBR(主引导记录) LBA0(第0扇区) x86 PC、部分ARM板
GPT(GUID分区表) LBA0~LBA33 UEFI启动、大容量存储
固定偏移分区表 Flash头部固定偏移 裸机、RTOS、小容量MCU
DTB分区描述 设备树中描述 Linux内核、U-Boot

读取流程其实不复杂。我拿U-Boot举个例子:

// U-Boot中读取MBR分区表的简化流程
int read_mbr_partitions(struct blk_desc *dev_desc)
{
    // 1. 读取LBA0扇区(512字节)
    if (blk_dread(dev_desc, 0, 1, &mbr_buf) != 1) {
        printf("读取MBR失败\n");
        return -1;
    }

    // 2. 检查MBR签名(0x55AA)
    if (mbr_buf[510] != 0x55 || mbr_buf[511] != 0xAA) {
        printf("无效的MBR签名\n");
        return -1;
    }

    // 3. 解析4个主分区表项(偏移446~509)
    for (i = 0; i < 4; i++) {
        part = &mbr_buf[446 + i * 16];
        // 读取分区类型、起始LBA、扇区数
        parts[i].type = part[4];
        parts[i].start_lba = *(uint32_t *)(part + 8);
        parts[i].sectors = *(uint32_t *)(part + 12);
    }

    return 0;
}

关键点:Bootloader读取分区表时,必须知道自己当前用的是哪种分区格式。如果格式不匹配,读出来的数据全是错的。

我个人习惯在分区表头部加一个魔数(Magic Number),用来校验分区表是否有效。比如U-Boot的CONFIG_PARTITION_UUIDS就是干这个的。

3.2 启动设备枚举顺序

Bootloader启动时,得先知道从哪个设备启动。这个过程叫“启动设备枚举”。

为什么会需要枚举?因为嵌入式设备可能有多个存储介质:eMMC、SD卡、NAND Flash、SPI Flash、USB……Bootloader得按优先级一个个试。

我见过一个项目,工程师把SD卡和eMMC的启动优先级搞反了。结果每次插着SD卡开机,系统就从SD卡启动,eMMC里的系统反而进不去。排查了两天才发现是枚举顺序的问题。

典型的枚举顺序(以U-Boot为例):

  1. 读取启动模式引脚——硬件上通过GPIO电平决定优先从哪个设备启动
  2. 检查启动设备列表——按boot_device环境变量定义的顺序尝试
  3. 尝试读取分区表——每个设备都尝试读取MBR/GPT
  4. 校验签名或魔数——确认分区表有效
  5. 选择第一个成功的设备——作为启动设备

小技巧:调试阶段,我建议在Bootlog里打印出枚举到的设备列表。这样一眼就能看出Bootloader到底认没认到你的存储设备。

来看看U-Boot中枚举设备的代码片段:

// U-Boot启动设备枚举(简化版)
static int boot_device_enum(void)
{
    struct blk_desc *dev;
    int i;

    // 按优先级枚举:MMC -> USB -> SATA -> NVMe
    static const enum blk_driver_type priority[] = {
        IF_TYPE_MMC,
        IF_TYPE_USB,
        IF_TYPE_SATA,
        IF_TYPE_NVME
    };

    for (i = 0; i < ARRAY_SIZE(priority); i++) {
        dev = blk_get_dev(priority[i], 0);
        if (dev && dev->lba > 0) {
            printf("发现设备: %s %d\n",
                   blk_get_if_type_name(priority[i]), 0);
            // 尝试读取分区表
            if (part_init(dev) == 0) {
                printf("分区表有效,选择该设备启动\n");
                return i;
            }
        }
    }

    printf("未找到可启动设备\n");
    return -1;
}

3.3 分区激活标志的作用

分区激活标志,说白了就是一个“开关”。它告诉Bootloader:这个分区能不能用来启动系统。

在MBR分区表中,每个分区表项的第一个字节就是引导标志(Boot Indicator):

  • 0x00——非活动分区,不参与启动
  • 0x80——活动分区(可引导),Bootloader会优先尝试从这里启动

嗯,这里要注意:MBR只允许一个活动分区。如果有多个分区都标记为0x80,那Bootloader的行为就不可预期了。

我曾经踩过的坑:有一次做双系统切换,我在MBR里把两个分区都设成了0x80。结果U-Boot每次启动都随机选一个,系统时好时坏。后来查规范才发现,MBR规范明确要求只能有一个活动分区。

GPT分区表没有“活动分区”这个概念,它用的是分区属性(Partition Attributes)。其中bit 60表示“Legacy BIOS可引导”,bit 63表示“要求固件自动检测”。

来看一个实际的分区表解析例子:

// 检查分区是否可引导
int is_partition_bootable(struct partition_entry *part)
{
    // MBR分区:检查引导标志
    if (part->boot_indicator == 0x80) {
        return 1;  // 活动分区
    }

    // GPT分区:检查属性位
    if (part->attributes & (1ULL << 60)) {
        return 1;  // Legacy可引导
    }

    return 0;
}

我个人建议,在设计分区方案时,把激活标志和分区类型结合起来判断。比如:

  • 只有标记为0x83(Linux文件系统)的分区才检查激活标志
  • 交换分区(0x82)即使标记为活动也忽略
  • 扩展分区(0x05或0x0F)不参与启动判断

3.4 整体交互流程

把上面三个环节串起来,Bootloader和分区交互的完整流程就是:

  1. 上电后,Bootloader先初始化存储控制器(MMC、NAND等)
  2. 按枚举顺序尝试每个存储设备
  3. 读取设备上的分区表(MBR/GPT/自定义)
  4. 遍历分区表项,找到标记为“活动”或“可引导”的分区
  5. 从该分区读取内核或下一级Bootloader
  6. 跳转执行

下面这张图把整个流程串起来了:

Bootloader与分区交互流程 上电初始化 枚举启动设备(MMC→USB→SATA) 读取分区表(MBR/GPT) 检查分区激活标志(0x80/属性位) 加载内核并跳转 关键点 每个环节失败都会回退到上一个步骤,直到所有设备尝试完毕 分区表无效或没有活动分区时,Bootloader会进入命令行或救援模式

总结一下:Bootloader和分区交互的核心就是“找设备→读分区表→查激活标志→启动”。每一步都有对应的错误处理,任何一个环节出问题,系统都起不来。

你想想看,如果分区表读错了,或者激活标志没设对,Bootloader就像没头苍蝇一样乱撞。所以调试的时候,我建议先确认Bootlog里有没有打印出“Found device”和“Partition table valid”这两行。没有的话,问题大概率出在硬件或者分区表格式上。