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为例):
- 读取启动模式引脚——硬件上通过GPIO电平决定优先从哪个设备启动
- 检查启动设备列表——按
boot_device环境变量定义的顺序尝试 - 尝试读取分区表——每个设备都尝试读取MBR/GPT
- 校验签名或魔数——确认分区表有效
- 选择第一个成功的设备——作为启动设备
小技巧:调试阶段,我建议在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和分区交互的完整流程就是:
- 上电后,Bootloader先初始化存储控制器(MMC、NAND等)
- 按枚举顺序尝试每个存储设备
- 读取设备上的分区表(MBR/GPT/自定义)
- 遍历分区表项,找到标记为“活动”或“可引导”的分区
- 从该分区读取内核或下一级Bootloader
- 跳转执行
下面这张图把整个流程串起来了:
总结一下:Bootloader和分区交互的核心就是“找设备→读分区表→查激活标志→启动”。每一步都有对应的错误处理,任何一个环节出问题,系统都起不来。
你想想看,如果分区表读错了,或者激活标志没设对,Bootloader就像没头苍蝇一样乱撞。所以调试的时候,我建议先确认Bootlog里有没有打印出“Found device”和“Partition table valid”这两行。没有的话,问题大概率出在硬件或者分区表格式上。