UEFI 基础架构:核心概念与启动阶段详解

各位同学,今天我们正式进入 UEFI 的核心地带。说实话,我刚接触 UEFI 那会儿,也被它庞大的规范文档搞得晕头转向。但后来我发现,只要抓住几个关键概念——PEI、DXE、Protocol 和 Handle 数据库——整个框架就清晰了。这就像盖房子,先搭好骨架,再填充细节。

UEFI 规范的核心概念

UEFI 规范定义了一套标准的固件接口。它取代了传统的 BIOS,提供了更现代化的启动环境。我个人理解,UEFI 的核心思想就是“模块化”和“可扩展”。

  • 系统表 (System Table):这是 UEFI 的“总控台”。它包含了所有关键服务的入口指针,比如启动服务 (Boot Services) 和运行时服务 (Runtime Services)。任何 UEFI 驱动或应用,第一步就是拿到这个表的地址。
  • 启动服务 (Boot Services):在操作系统接管前,这些服务负责管理内存、事件、协议等。一旦操作系统启动,这些服务就会被关闭,释放内存。
  • 运行时服务 (Runtime Services):这些服务在操作系统启动后依然可用,比如读取系统时间、设置变量等。它们常驻内存,不能被覆盖。
  • 映像 (Image):UEFI 的可执行文件,可以是驱动 (Driver) 或应用 (Application)。它们遵循 PE32+ 格式,但扩展名通常是 .efi。

关键点:UEFI 启动过程不是线性的,而是事件驱动的。各个模块通过 Protocol 互相发现和调用,这跟传统 BIOS 的“顺序执行”完全不同。

PEI 阶段详解

PEI (Pre-EFI Initialization) 阶段,是整个启动流程的“开胃菜”。它的任务很简单:把 CPU 和内存初始化好,为后续的 DXE 阶段铺路。

为什么需要 PEI?因为刚上电时,系统只有 CPU 和少量的片上 SRAM 可用。内存控制器还没配置,大容量 DRAM 根本不可用。PEI 就是在这样“缺衣少食”的环境下工作的。

PEI 的核心组件

  • PEI 核心 (PEI Core):负责调度 PEIM (PEI Module) 的执行。它本身是固件中最早执行的代码之一。
  • PEIM (PEI Module):一个个功能模块,比如 CPU 初始化、内存初始化、芯片组初始化。它们通过 PPI (PEIM-to-PEIM Interface) 互相通信。
  • PPI (PEIM-to-PEIM Interface):PEI 阶段的“协议”。一个 PEIM 可以安装 PPI,另一个 PEIM 可以查找并使用它。
  • HOB (Hand-Off Block):PEI 阶段最重要的产出。它是一个数据结构链表,记录了内存布局、启动参数等信息。PEI 结束后,HOB 会被传递给 DXE 阶段。

我的经验:调试 PEI 阶段非常痛苦,因为此时串口可能还没初始化。我习惯在 PEI 阶段早期就通过 GPIO 或 LED 做状态指示,或者使用芯片厂商提供的“早期串口”功能。否则,一旦内存初始化失败,你连错误信息都看不到。

PEI 执行流程

  1. CPU 复位后,执行 SEC (Security) 阶段,然后跳转到 PEI 核心。
  2. PEI 核心启动,查找并执行第一个 PEIM(通常是 CPU 初始化)。
  3. 各 PEIM 按依赖关系依次执行,通过 PPI 交换信息。
  4. 内存初始化成功后,PEI 核心构建 HOB 列表。
  5. PEI 阶段结束,将控制权交给 DXE 阶段。

注意:PEI 阶段运行在物理地址空间,没有页表,没有虚拟内存。所有代码和数据都必须能在当前 CPU 模式下直接访问。这意味着你的代码必须位置无关 (PIC),或者被加载到已知的固定地址。

DXE 阶段详解

DXE (Driver Execution Environment) 阶段,是 UEFI 启动的“主战场”。内存已经可用,PEI 阶段已经铺好了路。DXE 阶段的任务是:加载并执行所有 UEFI 驱动,初始化所有硬件设备,最终找到并启动操作系统。

DXE 阶段比 PEI 阶段复杂得多。它拥有完整的内存管理、事件管理、协议管理能力。说白了,PEI 是“生存模式”,DXE 才是“正常模式”。

DXE 的核心组件

  • DXE 核心 (DXE Core):负责调度 DXE 驱动,管理 Handle 数据库和 Protocol。
  • DXE 驱动 (DXE Driver):可以是早期驱动(从固件 ROM 加载)或后期驱动(从文件系统加载)。它们负责初始化具体的硬件,比如 PCIe 总线、USB 控制器、磁盘控制器。
  • Protocol:UEFI 的“接口契约”。一个驱动安装 Protocol,另一个驱动通过 Protocol 调用它的功能。这是 UEFI 模块化的基石。
  • Handle 数据库:UEFI 的“对象管理器”。每个硬件设备或软件实体,在 UEFI 中都被抽象为一个 Handle。Handle 上可以安装多个 Protocol。

DXE 执行流程

  1. DXE 核心从 HOB 中获取内存信息,初始化内存管理。
  2. DXE 核心启动调度器,开始加载 DXE 驱动。
  3. 每个驱动被加载后,会注册自己支持的 Protocol。
  4. 驱动之间通过 Protocol 互相发现和调用,完成硬件初始化。
  5. 当所有必要的驱动都执行完毕,DXE 核心启动 BDS (Boot Device Selection) 阶段,选择并启动操作系统。

关键点:DXE 驱动的执行顺序不是固定的。它们通过依赖表达式 (Depex) 来声明自己需要哪些 Protocol。DXE 调度器会解析这些依赖,确保驱动按正确的顺序执行。这跟 Linux 内核的模块依赖有点像,但更轻量。

Protocol 与 Handle 数据库

这是 UEFI 最核心的设计模式。我当年花了很长时间才真正理解它。简单来说,Handle 数据库就是一个“注册中心”,Protocol 就是“服务接口”。

Handle 数据库

Handle 是一个不透明的指针,本质上是一个整数 ID。每个 Handle 代表一个实体——可以是一个物理设备(如 PCIe 根桥)、一个逻辑设备(如分区)、或者一个软件接口(如文件系统)。

Handle 数据库维护了一个全局的 Handle 列表。每个 Handle 上可以挂载多个 Protocol 接口。你可以通过 Protocol GUID 来查找 Handle,或者遍历所有 Handle。

Protocol

Protocol 是一个结构体,包含一组函数指针和数据字段。每个 Protocol 都有一个唯一的 GUID 来标识。比如,EFI_BLOCK_IO_PROTOCOL 定义了读写块设备的标准接口。

驱动的工作模式通常是:

  1. 安装 Protocol:驱动初始化硬件后,创建一个 Handle,并在上面安装 Protocol。
  2. 查找 Protocol:另一个驱动通过 GUID 查找 Handle,获取 Protocol 接口,然后调用其函数。

避坑指南:我曾经在项目中遇到一个 bug,某个驱动在安装 Protocol 后,没有正确设置 Protocol 的版本号。结果另一个驱动在查找时,因为版本不匹配而拒绝使用它。调试了两天才发现是版本号字段没初始化。所以,Protocol 结构体中的所有字段都必须正确初始化,包括那些看起来“没用”的保留字段。

代码示例:查找并使用 Protocol

#include <Uefi.h>
#include <Protocol/BlockIo.h>

EFI_STATUS
FindAndUseBlockIo (
  VOID
  )
{
  EFI_STATUS           Status;
  EFI_HANDLE           *HandleBuffer;
  UINTN                HandleCount;
  EFI_BLOCK_IO_PROTOCOL *BlockIo;
  UINTN                Index;

  // 1. 查找所有支持 BlockIo Protocol 的 Handle
  Status = gBS->LocateHandleBuffer (
                ByProtocol,
                &gEfiBlockIoProtocolGuid,
                NULL,
                &HandleCount,
                &HandleBuffer
                );
  if (EFI_ERROR (Status)) {
    return Status;
  }

  // 2. 遍历每个 Handle,获取 BlockIo Protocol
  for (Index = 0; Index < HandleCount; Index++) {
    Status = gBS->HandleProtocol (
                  HandleBuffer[Index],
                  &gEfiBlockIoProtocolGuid,
                  (VOID **)&BlockIo
                  );
    if (EFI_ERROR (Status)) {
      continue;
    }

    // 3. 使用 Protocol 提供的函数
    // 比如读取块设备信息
    Print (L"Block Size: %d\n", BlockIo->Media->BlockSize);
  }

  // 4. 释放缓冲区
  gBS->FreePool (HandleBuffer);

  return EFI_SUCCESS;
}

PEI 与 DXE 的对比

特性 PEI 阶段 DXE 阶段
内存环境 仅有片上 SRAM,DRAM 不可用 DRAM 已初始化,内存充足
核心数据结构 PPI (PEIM-to-PEIM Interface) Protocol (UEFI 标准接口)
模块类型 PEIM (PEI Module) DXE Driver
输出产物 HOB (Hand-Off Block) Handle 数据库 + Protocol 实例
执行环境 物理地址,无虚拟内存 物理地址,但支持页表
调试难度 极高(串口可能不可用) 中等(串口通常已可用)

整体架构图

下面这张图展示了 UEFI 启动过程中,PEI 和 DXE 阶段的关系,以及 Handle 数据库和 Protocol 的交互方式。

UEFI PEI 与 DXE 架构图 UEFI 启动架构:PEI → DXE 流程 PEI 阶段 PEI 核心 PEIM 模块 PPI 接口 HOB 数据 传递 HOB DXE 阶段 DXE 核心 DXE 驱动 Protocol Handle 数据库 PEI 阶段产出 HOB,传递给 DXE 阶段 DXE 阶段构建 Handle 数据库,通过 Protocol 驱动设备 进入 BDS 阶段

总结

UEFI 的基础架构,说白了就是一套“模块化 + 接口化”的固件框架。PEI 阶段在资源受限的环境下完成最基础的初始化,DXE 阶段在完整的内存环境中加载驱动、构建 Handle 数据库。Protocol 和 Handle 数据库是 UEFI 的“神经系统”,让各个模块能够灵活地发现和调用彼此。

嗯,这里要注意:理解 Handle 数据库和 Protocol 的交互模式,是编写 UEFI 驱动的关键。如果你只是做启动优化,可能不需要深入每个 Protocol 的细节,但必须理解这个“注册-查找-调用”的机制。否则,你连驱动为什么没被加载都查不出来。