4. UEFI启动流程总览:从按下电源键到操作系统加载的完整路径

按下电源键的那一刻,机器里发生了什么?

很多人觉得,开机就是BIOS自检,然后引导系统。但说实话,UEFI时代这套流程已经完全不同了。我最早接触UEFI时也犯过迷糊——明明看到屏幕亮了,系统却卡在某处不动,连个报错都没有。后来才明白,这中间藏着好几个关键阶段。

今天我就带你走一遍完整的UEFI启动路径。从按下电源键,到操作系统接管控制权,每一步我都会告诉你:谁在执行、执行什么、为什么这么设计。

核心观点:UEFI启动不是一条直线,而是一个分阶段、可扩展的框架。每个阶段都有明确的责任边界,并且可以通过驱动和协议动态组合。

4.1 启动的五个阶段

UEFI规范把启动过程划分为七个阶段,但实际开发中我们通常关注五个核心阶段。我习惯把它们记成:SEC → PEI → DXE → BDS → TSL。你看,缩写连起来就是一句话:Security, Platform, Driver, Boot, System Load。

阶段 全称 主要任务 谁在执行
SEC Security (安全验证) 验证固件完整性,建立临时运行环境 CPU + 固件ROM中的微码
PEI Pre-EFI Initialization 初始化内存、检测基本硬件 PEI Core + PEIM
DXE Driver Execution Environment 加载UEFI驱动,枚举设备 DXE Core + UEFI Drivers
BDS Boot Device Selection 选择启动设备,加载启动管理器 BDS Manager
TSL Transient System Load 加载操作系统引导程序 OS Boot Loader

嗯,这里要注意:RT (Runtime) 和 AL (After Life) 两个阶段是操作系统接管后的状态,我们放到后面章节再细讲。

4.2 SEC阶段:信任的起点

按下电源键,CPU复位,第一条指令从固件ROM中取出。这时候内存还没初始化,连栈都没有。你想想看,代码在什么环境下运行?

答案是:Cache As RAM (CAR)。CPU内部缓存被临时当作内存用。我在调试一个国产平台时,就遇到过CAR配置错误导致SEC阶段直接死循环的问题——屏幕全黑,连Debug口都没输出。后来发现是MTRR寄存器没设对。

SEC阶段的核心任务有三件:

  • 验证Boot Firmware Volume (BFV) 的签名——确保固件没被篡改
  • 建立临时内存 (CAR)——让后续代码有地方放栈和变量
  • 找到PEI Core入口——把控制权交给下一阶段

避坑指南:我曾经在移植UEFI固件到新CPU时,忽略了SEC阶段的Cache配置。结果PEI阶段一访问内存就挂。折腾了两天才发现,是CAR的MTRR掩码写错了,导致缓存覆盖了非连续地址。记住:SEC阶段的CAR配置必须精确匹配CPU的缓存拓扑。

4.3 PEI阶段:把内存“变”出来

SEC阶段结束后,系统还处于“无内存”状态。PEI阶段的第一件事,就是初始化内存控制器,把真正的DRAM激活。

PEI阶段由 PEI Core 和一堆 PEIM (PEI Module) 组成。PEI Core负责调度,PEIM负责干活。常见的PEIM包括:

  • CPU PEIM:初始化CPU微码、设置MSR
  • Memory PEIM:训练内存、配置SPD
  • Platform PEIM:配置芯片组、GPIO、时钟

我印象最深的一次,是在某款笔记本上,Memory PEIM训练DDR5花了将近3秒。用户觉得开机慢,但其实是内存频率太高,需要反复校准。后来我们加了一个“快速启动”选项,跳过完整训练,只恢复上次保存的参数——开机时间直接砍半。

小技巧:PEI阶段可以通过 HOB (Hand-Off Block) 把数据传给DXE阶段。比如内存大小、CPU信息、启动原因(冷启动/热启动/唤醒)。如果你在DXE阶段发现某些信息丢了,多半是HOB没传对。

4.4 DXE阶段:驱动大爆发

内存有了,DXE阶段就可以放开手脚了。这个阶段的核心是 DXE Core 和 UEFI Driver Model。

DXE Core会扫描所有固件卷 (FV),找到并加载UEFI驱动。这些驱动按依赖关系排序,先加载基础驱动(如PCI总线驱动),再加载设备驱动(如网卡、硬盘、显卡)。

说白了,DXE阶段就是“搭积木”。每个驱动提供一个或多个 UEFI Protocol,其他驱动通过Protocol接口调用服务。比如:

  • PCI Bus Driver 枚举PCI设备,产生 gEfiPciIoProtocolGuid
  • Disk I/O Driver 识别分区表,产生 gEfiDiskIoProtocolGuid
  • Simple File System Driver 挂载文件系统,产生 gEfiSimpleFileSystemProtocolGuid
// 一个典型的UEFI驱动入口
EFI_STATUS
EFIAPI
MyDriverEntryPoint (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
  // 注册驱动绑定协议
  return gBS->InstallMultipleProtocolInterfaces(
           &ImageHandle,
           &gEfiDriverBindingProtocolGuid,
           &mMyDriverBinding,
           NULL
           );
}

你可能会问:这么多驱动,加载顺序怎么保证?答案是 Dependency Expression。每个驱动都声明自己依赖哪些Protocol,DXE Core按拓扑排序加载。如果某个驱动依赖的Protocol还没出现,它就等着——直到被其他驱动满足。

4.5 BDS阶段:选谁启动?

DXE阶段把所有硬件都枚举完了,接下来BDS阶段决定:从哪个设备启动?

BDS Manager会读取 Boot Manager Menu 中的启动项列表。每个启动项包含:

  • 设备路径:比如 PciRoot(0)/Pci(0x1F,0x2)/Sata(0x0,0x0)/HD(1,MBR,0x1234)
  • 启动选项:比如 EFI\BOOT\BOOTX64.EFI
  • 优先级:数字越小越优先

如果所有启动项都失败了,BDS会回退到 UEFI Shell 或者显示“No bootable device”。

实际经验:我遇到过一台服务器,每次重启都进Shell。查了半天,发现是NVRAM里的BootOrder被清空了。原因是CMOS电池没电,导致UEFI变量丢失。解决方案很简单:换电池,或者把默认启动项写死在固件里。

4.6 TSL阶段:操作系统接管

BDS选定了启动设备,加载了OS Boot Loader(比如GRUB2、Windows Boot Manager)。这时候系统进入TSL阶段。

OS Boot Loader会做几件事:

  1. 调用 ExitBootServices()——告诉UEFI:我要接管了,你把启动服务关掉
  2. 加载操作系统内核到内存
  3. 跳转到内核入口点

注意:ExitBootServices() 是一个不可逆操作。一旦调用,UEFI的启动服务(如内存分配、Protocol查找)全部失效,只剩下Runtime Service(如设置时间、重启、关机)。

我调试过一个Bug:某Linux发行版在ExitBootServices之后,系统直接死机。后来发现是UEFI固件在退出前没有关闭DMA,导致设备还在往内存写数据,覆盖了内核。解决方案是在ExitBootServices之前,先停掉所有Bus Master设备。

4.7 完整流程图

下面这张图,把整个流程串起来了。你可以对照着看,每个阶段做了什么,数据怎么传递。

UEFI 启动流程总览 SEC 阶段 安全验证 + CAR PEI 阶段 内存初始化 + PEIM DXE 阶段 驱动加载 + Protocol BDS 阶段 选择启动设备 TSL 阶段 加载OS Boot Loader 操作系统运行 数据传递 • HOB (PEI → DXE) • Protocol (DXE内部) • Boot Option (BDS) • ExitBootServices (TSL) 关键里程碑 ① 内存可用 (PEI结束) ② 所有设备就绪 (DXE结束) ③ 启动服务关闭 (TSL)

4.8 几个容易踩的坑

最后,我总结几个实际项目中常见的坑,你遇到了可以少走弯路:

问题 原因 解决思路
SEC阶段死机,无任何输出 CAR配置错误,或BFV签名验证失败 检查MTRR寄存器,确认固件签名
PEI阶段卡在内存训练 SPD数据异常,或内存频率不匹配 用串口Debug输出,查看训练状态码
DXE阶段找不到硬盘 PCI枚举失败,或驱动依赖未满足 检查PCIe链路状态,确认驱动加载顺序
BDS阶段启动项为空 NVRAM损坏,或BootOrder被清空 恢复默认启动项,检查CMOS电池
ExitBootServices后死机 DMA未关闭,或中断未屏蔽 在退出前停掉所有Bus Master设备

好了,这就是UEFI启动的完整路径。从按下电源键到操作系统加载,每一步都有它的设计逻辑。你掌握了这个框架,后面再深入每个阶段时,就不会迷路了。