UEFI 启动基础:规范、流程与 BIOS 的差异

好,我们正式开始第二讲。这一章是地基,把 UEFI 搞明白,后面用 Yocto 构建启动分区时你才知道自己在拼什么。

很多工程师做了好几年嵌入式,对 UEFI 的理解还停留在「按 F2 进设置」这个层面。说实话,这远远不够。你想想看,UEFI 本质上是一个微型操作系统,它负责把硬件从冷启动状态带到能加载操作系统内核的状态。这个过程中,它要初始化 CPU、内存、外设,还要找到启动设备、加载引导程序。

我个人习惯把 UEFI 比作「硬件与操作系统之间的翻译官」。它说一种叫 EFI 字节码的语言,不依赖具体的 CPU 架构。这一点跟传统 BIOS 完全不同,后面我会细说。

UEFI 规范简介

UEFI 的全称是 Unified Extensible Firmware Interface,统一可扩展固件接口。它由 UEFI 论坛维护,目前最新的正式版本是 2.10。规范本身非常厚,大概有 2500 多页,但我们做嵌入式开发不需要全读,抓住几个核心概念就行。

UEFI 规范的核心要素:

  • EFI 系统分区 (ESP):一个 FAT32 格式的分区,存放引导加载程序、驱动和应用。这是 Yocto 构建时我们要重点处理的对象。
  • 固件文件系统 (FFS):固件内部存储文件的格式,跟 ESP 不是一回事。
  • 协议 (Protocols):UEFI 驱动和应用程序之间的接口规范。比如你写一个网卡驱动,就要实现 Simple Network Protocol。
  • 启动管理器 (Boot Manager):UEFI 固件内置的启动项管理工具,决定从哪个设备、哪个文件启动。
  • 运行时服务 (Runtime Services):操作系统启动后仍然可以调用的固件服务,比如获取系统时间、设置启动项。

我记得刚接触 UEFI 时,最让我困惑的是「为什么 ESP 必须是 FAT32?」后来才明白,FAT32 是 UEFI 规范强制要求的,因为它的实现简单,而且几乎所有操作系统都能读写。但这里有个坑——FAT32 单文件最大只能 4GB,如果你的内核镜像超过这个大小,就得用其他方式加载了。

UEFI 启动流程

UEFI 的启动流程比 BIOS 清晰得多。它分七个阶段,每个阶段都有明确的任务。我画了一张流程图,你一看就明白。

UEFI 启动流程 UEFI 启动流程 SEC (安全验证) PEI (EFI 预初始化) DXE (驱动执行环境) BDS (启动设备选择) TSL (短暂系统加载) RT (运行时) AL (灾难恢复) 关键说明: • SEC 阶段:CPU 刚上电,执行第一条指令,验证固件完整性。 • PEI 阶段:初始化内存控制器,发现内存大小和位置。 • DXE 阶段:加载大部分驱动,提供完整的硬件抽象。 启动设备选择 (BDS): • 遍历启动项列表,找到第一个可启动设备。 • 加载 EFI 应用程序(如 GRUB、systemd-boot、shim.efi)。

这七个阶段里,我们做嵌入式开发最常打交道的是 BDS 和 TSL 阶段。BDS 阶段决定从哪个设备启动,TSL 阶段加载引导程序。在 Yocto 构建中,我们生成的 bootx64.efi 或 grubx64.efi 就是在 TSL 阶段被加载的。

一个小技巧: 调试 UEFI 启动问题时,可以在固件设置中开启「串口重定向」或「UEFI Shell」。UEFI Shell 是一个命令行环境,你可以手动执行 fs0:\EFI\BOOT\bootx64.efi 来测试引导程序是否正常。我曾经靠这个排查过一个 grub 配置文件路径写错的 bug,省了至少半天时间。

UEFI 与 BIOS 的区别

很多人觉得 UEFI 就是 BIOS 的升级版,其实不对。它们的架构差异非常大。我整理了一张对比表,你一看就清楚。

对比维度 传统 BIOS UEFI
接口模式 16 位实模式,只能访问 1MB 空间 32/64 位保护模式,可访问全部内存
启动速度 慢,需要自检所有硬件 快,可以并行初始化设备
分区表 MBR,最大支持 2TB 磁盘 GPT,支持 9.4ZB 磁盘
引导加载方式 读取 MBR 中的引导代码 直接加载 EFI 应用程序 (.efi)
驱动模型 Option ROM,16 位代码 EFI 驱动,跨架构
安全启动 不支持 支持 Secure Boot,验证签名
用户界面 文本菜单,键盘操作 图形界面,支持鼠标

这里我想重点说一下 安全启动 (Secure Boot)。这是 UEFI 引入的一个关键特性,也是我们在 Yocto 构建中必须处理的问题。Secure Boot 要求所有在 TSL 阶段加载的 EFI 应用程序都必须有合法的数字签名。如果你自己编译了一个 grub.efi,没有签名就直接放到 ESP 分区,固件会拒绝加载它。

避坑指南: 我曾经在一个项目中,用 Yocto 构建了一个自定义的 UEFI 引导程序,烧到板子上死活启动不了。查了两天才发现是 Secure Boot 在作怪。解决方案有两个:要么在固件设置中关闭 Secure Boot,要么给引导程序签名。对于开发板,我建议先关掉 Secure Boot 调试,等稳定了再考虑签名流程。

UEFI 的启动项管理

UEFI 固件内部维护了一个启动项列表,存储在 NVRAM 中。每个启动项包含一个名称、一个设备路径和一个文件路径。比如:

Boot0000: Ubuntu    PciRoot(0x0)/Pci(0x1F,0x2)/Sata(0x0,0x0)/HD(1,GPT)/\EFI\ubuntu\shimx64.efi
Boot0001: UEFI Shell PciRoot(0x0)/Pci(0x1F,0x2)/Sata(0x0,0x0)/HD(1,GPT)/\EFI\tools\Shell.efi

启动顺序由 BootOrder 变量控制。固件会按照这个顺序依次尝试每个启动项,直到成功为止。在 Yocto 构建中,我们通常会把引导程序放在 \EFI\BOOT\bootx64.efi 这个默认路径,这样即使 NVRAM 被清空,固件也能找到它。

嗯,这里要注意:不同架构的默认路径不一样。x86_64 是 bootx64.efi,ARM64 是 bootaa64.efi,IA32 是 bootia32.efi。在 Yocto 的机器配置文件中,你需要正确设置这些路径。

UEFI 与嵌入式 Linux 的关联

你可能觉得 UEFI 是 PC 领域的东西,跟嵌入式关系不大。其实不然。现在很多 ARM 开发板(比如 Raspberry Pi 4、Jetson 系列)都开始支持 UEFI。原因很简单:UEFI 提供了一套标准化的启动接口,让 Linux 内核可以不用关心底层硬件的差异。

在嵌入式场景下,UEFI 带来的好处很明显:

  • 设备树传递:UEFI 可以通过 EFI_DT_FIXUP_PROTOCOL 修改设备树,把硬件信息传给内核。
  • ACPI 支持:对于服务器级别的嵌入式系统,UEFI 可以生成 ACPI 表,让操作系统发现硬件资源。
  • 统一启动流程:不管你是 x86 还是 ARM,UEFI 的启动流程都一样,减少了平台适配的工作量。

我个人觉得,UEFI 在嵌入式领域的普及是必然趋势。你想想看,以前做嵌入式 Linux 启动,每个板子都要写一套 bootloader 脚本,从 U-Boot 的环境变量到内核加载地址,全是板级相关的。有了 UEFI,这些都可以标准化。Yocto 项目也提供了 meta-efi 层来支持 UEFI 启动,我们后面会详细讲。

一个实用的调试命令: 在 UEFI Shell 中,你可以用 bcfg 命令管理启动项。比如 bcfg boot dump 列出所有启动项,bcfg boot add 0 fs0:\EFI\BOOT\bootx64.efi "My Bootloader" 添加一个新启动项。这个命令在调试时非常有用,我几乎每周都会用到。