UEFI 固件基础:从规范到启动流程

好,我们正式开始搭建安全启动环境之前,得先把地基打牢。UEFI 这东西,说白了就是 BIOS 的现代替代品。我早年做 x86 固件时,还在跟 Legacy BIOS 打交道,那会儿改个启动顺序都得进那个蓝底白字的界面。后来 UEFI 普及了,说实话,刚开始真不习惯——但用顺手了,你就回不去了。

UEFI 规范简介

UEFI(统一可扩展固件接口)不是某个公司的私有标准,它由 UEFI 论坛维护。你想想看,如果每个主板厂商都搞一套自己的固件接口,操作系统得疯掉。UEFI 规范就是定了一个统一的“游戏规则”。

我个人习惯把 UEFI 规范分成几个核心部分:

  • 启动管理器:负责决定从哪个设备启动,按什么顺序启动
  • 运行时服务:操作系统启动后还能调用的固件服务
  • 启动时服务:只在启动阶段可用的服务,比如内存分配、协议查找
  • 协议接口:驱动和应用程序之间的通信契约
  • GUID 分区表(GPT):替代 MBR 的磁盘分区方案

核心要点:UEFI 规范定义了固件与操作系统之间的接口,而不是固件内部怎么实现。这意味着你可以用 C 语言写 UEFI 应用,甚至可以在 UEFI Shell 下跑 Python——当然,前提是你得先移植一个解释器。

UEFI 启动流程

启动流程这东西,我建议你把它记在脑子里。每次排查启动问题,你都得在心里过一遍这个流程。

UEFI 启动大致分七个阶段:

  1. SEC(安全验证):CPU 复位后的第一条指令,验证固件完整性。嗯,这里要注意,安全启动的根信任锚点就在这。
  2. PEI(EFI 前初始化):初始化内存控制器,把内存搞出来。没有内存,啥都干不了。
  3. DXE(驱动执行环境):大部分硬件初始化在这里完成。PCIe 枚举、SATA 初始化、USB 控制器启动。
  4. BDS(启动设备选择):启动管理器登场,决定从哪个设备启动。
  5. TSL(临时系统加载):加载操作系统引导加载器,比如 bootx64.efi。
  6. RT(运行时):操作系统接管,固件进入运行时服务模式。
  7. AL(高级配置与电源接口):操作系统与固件通过 ACPI 交互,管理电源和配置。

我在项目中遇到过一个问题:某款主板在 DXE 阶段卡死,死活进不去 BDS。排查了半天,发现是某个 PCIe 设备的 Option ROM 加载时崩溃了。后来我直接在固件设置里禁用了那个设备的 Option ROM 加载,问题解决。

实战技巧:如果你在调试启动问题,可以在固件设置里开启“串口重定向”或“USB 调试”,这样就能看到 PEI 和 DXE 阶段的日志输出。很多主板默认是关闭的,你得手动打开。

下面这张图展示了 UEFI 启动流程的核心阶段和它们之间的过渡关系:

UEFI 启动流程核心阶段 SEC PEI DXE BDS TSL RT 各阶段说明: SEC:CPU 复位,验证固件完整性(安全启动的根信任锚点) PEI:初始化内存控制器,建立内存映射 DXE:驱动执行环境,完成大部分硬件初始化 BDS:启动设备选择,决定从哪个设备加载操作系统 TSL:加载操作系统引导加载器(如 bootx64.efi) RT:操作系统接管,固件进入运行时服务模式

UEFI 变量与 NVRAM

UEFI 变量是固件和操作系统之间交换数据的一种机制。它们存储在 NVRAM(非易失性随机存取存储器)里,断电不丢失。你想想看,如果没有 NVRAM,每次关机后启动顺序、安全启动状态这些设置都得重置,那得多崩溃。

UEFI 变量有几个关键属性:

属性 含义 典型用途
EFI_VARIABLE_NON_VOLATILE 变量存储在 NVRAM 中,断电不丢失 启动顺序、安全启动密钥
EFI_VARIABLE_BOOTSERVICE_ACCESS 只能在启动时服务阶段访问 固件内部配置
EFI_VARIABLE_RUNTIME_ACCESS 操作系统运行时也能访问 操作系统与固件通信
EFI_VARIABLE_AUTHENTICATED_WRITE_ACCESS 写入需要数字签名验证 安全启动变量(PK、KEK、db、dbx)

注意:NVRAM 空间是有限的。我曾经见过一台设备因为频繁更新固件配置,把 NVRAM 写满了,结果启动管理器直接罢工。后来我写了个工具定期清理无用的变量。记住,NVRAM 不是硬盘,别往里面塞大块数据。

操作 UEFI 变量,在 UEFI Shell 下可以用 dmpstore 命令查看所有变量,用 setvar 命令读写特定变量。在操作系统里,Linux 下可以通过 /sys/firmware/efi/efivars/ 接口访问。

# 在 UEFI Shell 下查看所有变量
Shell> dmpstore

# 查看特定变量(比如安全启动状态)
Shell> dmpstore -guid 8be4df61-93ca-11d2-aa0d-00e098032b8c SecureBoot

# 在 Linux 下读取安全启动状态
# cat /sys/firmware/efi/efivars/SecureBoot-8be4df61-93ca-11d2-aa0d-00e098032b8c

UEFI 驱动模型

UEFI 驱动模型跟传统 BIOS 的驱动方式完全不同。传统 BIOS 用中断向量表,UEFI 用协议(Protocol)。说白了,协议就是一组函数指针的集合,驱动通过安装协议来提供服务,应用程序通过查找协议来使用服务。

UEFI 驱动模型的核心概念:

  • 驱动绑定协议:驱动通过这个协议声明自己能控制哪些硬件
  • I/O 抽象:硬件操作被抽象成协议接口,驱动不直接操作寄存器
  • 即插即用:UEFI 固件在启动时枚举所有设备,为每个设备寻找匹配的驱动
  • 驱动栈:多个驱动可以分层协作,比如磁盘驱动之上可以挂文件系统驱动

我记得有一次调试一个 NVMe 驱动加载失败的问题。按照传统思路,我以为是 PCIe 配置空间没配好。后来发现,是驱动绑定协议的 Supported() 函数返回了错误——它检查设备 ID 时用错了掩码。改了一行代码,问题解决。

调试建议:如果你在写 UEFI 驱动,建议在 Supported() 和 Start() 函数里加调试输出。用 DEBUG((EFI_D_INFO, "Supported called for device %p\n", ControllerHandle)); 这种格式,固件日志里就能看到。

UEFI 驱动模型还有一个重要概念:句柄数据库。每个硬件设备、每个协议实例都有一个句柄。驱动通过句柄来查找和管理设备。你可以把句柄想象成设备的“身份证号”。

嗯,这一章的内容就到这里。UEFI 规范、启动流程、NVRAM 变量、驱动模型,这四个东西是后续所有安全启动相关操作的基础。你先把这些概念理清楚,后面我们讲安全启动策略、密钥管理、签名验证的时候,就不会觉得突兀了。