2、UEFI固件基础:架构、驱动模型与协议句柄

好,我们正式开始啃UEFI固件这块硬骨头。很多工程师刚接触UEFI时,会被一堆术语砸晕——DXE、Boot Driver、Protocol、Handle……说实话,我当年也懵过。但只要你把架构、驱动模型、协议句柄这三条主线理清楚,后面就顺了。

这一章,我们就围绕这三件事展开。不扯虚的,全是实战里天天要打交道的概念。

2.1 UEFI固件架构:从Reset到Shell

UEFI固件的启动流程,说白了就是一套分阶段的接力赛。每个阶段干完活,就把控制权交给下一棒。我习惯把它分成七个阶段,但核心就四个:SEC、PEI、DXE、BDS。

核心阶段速览

  • SEC(安全验证):CPU刚上电,第一条指令。做Cache-as-RAM,建立临时堆栈。我见过有板子在这阶段卡死,原因是Cache配置错了。
  • PEI(EFI前初始化):检测内存、CPU、芯片组。说白了就是让系统能跑起来。PEI阶段代码量不大,但出问题很难调——因为还没内存。
  • DXE(驱动执行环境):这才是UEFI的主战场。大部分驱动、协议、服务都在这里加载。DXE阶段跑完,系统基本就绪。
  • BDS(启动设备选择):找启动设备,加载Boot Manager。你按F12选启动项,就是BDS在干活。

下面这张图,是我每次培训必画的。它把UEFI的启动流水线讲清楚了。

UEFI 固件启动流水线 SEC PEI DXE BDS TSL RT SEC/PEI 做硬件初始化 → DXE 加载驱动 → BDS 选启动设备 → TSL/RT 进入操作系统 Cache-as-RAM 内存初始化 驱动加载 启动设备选择 Boot Manager OS Runtime 关键点:每个阶段都有独立的 FV(固件卷)和入口函数 阶段之间通过 HOB(Hand-Off Block)传递数据

我的经验:调试UEFI启动问题,第一步就是看卡在哪个阶段。SEC卡住多半是硬件问题,PEI卡住查内存,DXE卡住查驱动。我曾经花了两天追一个DXE hang,最后发现是某个驱动里有个死循环——嗯,从那以后我写驱动必加看门狗。

2.2 UEFI驱动模型:Bind、Start、Stop

UEFI驱动模型,和Windows/Linux的驱动模型不太一样。它不依赖中断,不依赖DMA,全靠协议和句柄来沟通。你想想看,在EFI环境下,连中断控制器都没初始化,驱动怎么工作?

答案就是:轮询 + 协议绑定。

每个UEFI驱动,本质上就是一个实现了EFI_DRIVER_BINDING_PROTOCOL的模块。这个协议有三个核心函数:

函数 作用 我踩过的坑
Supported() 检查驱动是否能控制某个设备 返回值写错,驱动死活不绑定
Start() 开始驱动设备,安装协议 忘记安装I/O协议,上层驱动找不到
Stop() 停止驱动,卸载协议 资源没释放,内存泄漏

驱动加载的流程是这样的:

  1. DXE调度器遍历所有驱动镜像。
  2. 对每个驱动,调用Supported()检查它能否控制某个设备句柄。
  3. 如果返回成功,调用Start(),驱动开始干活。
  4. 驱动在Start()里安装自己的协议,供其他驱动使用。

注意:UEFI驱动不支持热插拔(除了USB等少数总线)。所有设备都在DXE阶段枚举完毕。如果你在BDS阶段才发现一个新设备……抱歉,它不会被识别。我曾经在项目里遇到这个问题,最后只能改固件,让设备在PEI阶段就被发现。

2.3 UEFI协议与句柄:驱动之间的沟通语言

协议(Protocol)和句柄(Handle),是UEFI世界里最基础的两个概念。我经常跟新人说:句柄是“谁”,协议是“能干什么”。

  • 句柄:代表一个设备、一个驱动实例、或者一个事件。它就是一个指针,指向一个数据结构。
  • 协议:是一组函数指针和数据的集合。比如EFI_BLOCK_IO_PROTOCOL,就包含了读写块设备的函数。

每个句柄上可以挂多个协议。比如一个硬盘句柄,可能同时挂着EFI_BLOCK_IO_PROTOCOL和EFI_DISK_INFO_PROTOCOL。驱动通过OpenProtocol()来获取这些协议,然后调用里面的函数。

协议查找流程

// 伪代码:驱动A想使用硬盘
Handle = LocateHandleBuffer(ByProtocol, &gEfiBlockIoProtocolGuid);
// 返回所有支持Block I/O的句柄

// 打开协议
Status = OpenProtocol(
    Handle,
    &gEfiBlockIoProtocolGuid,
    &BlockIo,
    DriverHandle,
    Handle,
    EFI_OPEN_PROTOCOL_GET_PROTOCOL
);
// 现在可以通过 BlockIo->ReadBlocks() 读写硬盘了

这里有个关键点:协议GUID。每个协议都有一个唯一的128位GUID。你写驱动时,必须用正确的GUID去查找协议。我见过最离谱的bug,就是两个团队用了同一个GUID——结果驱动A拿到了驱动B的协议,函数指针全乱套了。

2.4 实战:写一个最简单的UEFI驱动

光说不练假把式。我们写一个最简单的UEFI驱动,它什么都不做,只在加载时打印一行字。这个驱动会帮你理解驱动模型的全流程。

#include <Uefi.h>
#include <Library/UefiDriverEntryPoint.h>
#include <Library/UefiBootServicesTableLib.h>

EFI_STATUS
EFIAPI
SimpleDriverSupported (
  IN EFI_DRIVER_BINDING_PROTOCOL  *This,
  IN EFI_HANDLE                   ControllerHandle,
  IN EFI_DEVICE_PATH_PROTOCOL     *RemainingDevicePath
  )
{
  // 这里我们假装支持所有设备
  return EFI_SUCCESS;
}

EFI_STATUS
EFIAPI
SimpleDriverStart (
  IN EFI_DRIVER_BINDING_PROTOCOL  *This,
  IN EFI_HANDLE                   ControllerHandle,
  IN EFI_DEVICE_PATH_PROTOCOL     *RemainingDevicePath
  )
{
  Print(L"Hello from SimpleDriver! Controller: %p\n", ControllerHandle);
  return EFI_SUCCESS;
}

EFI_STATUS
EFIAPI
SimpleDriverStop (
  IN EFI_DRIVER_BINDING_PROTOCOL  *This,
  IN EFI_HANDLE                   ControllerHandle,
  IN UINTN                        NumberOfChildren,
  IN EFI_HANDLE                   *ChildHandleBuffer
  )
{
  Print(L"SimpleDriver stopping...\n");
  return EFI_SUCCESS;
}

EFI_DRIVER_BINDING_PROTOCOL gSimpleDriverBinding = {
  SimpleDriverSupported,
  SimpleDriverStart,
  SimpleDriverStop,
  0x10,  // Version
  NULL,  // ImageHandle
  NULL   // ControllerHandle
};

EFI_STATUS
EFIAPI
UefiMain (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
  EFI_STATUS Status;

  Status = gBS->InstallMultipleProtocolInterfaces(
    &ImageHandle,
    &gEfiDriverBindingProtocolGuid,
    &gSimpleDriverBinding,
    NULL
  );

  Print(L"SimpleDriver installed. Status: %r\n", Status);
  return Status;
}

编译与测试:把这个代码放到EDK2的MyDriver/目录下,在.dsc和.fdf里加上它。编译后用QEMU跑,你会看到驱动加载时打印的信息。我第一次跑通时,屏幕上跳出那行字,说实话挺有成就感的。

2.5 避坑指南:协议与句柄的常见陷阱

做UEFI开发,有些坑几乎是必踩的。我列几个最常见的:

陷阱 现象 解决办法
协议GUID冲突 驱动拿到错误的协议指针 用GenGUID工具生成唯一GUID
句柄重复安装 InstallProtocolInterface返回EFI_ALREADY_STARTED 先检查句柄上是否已有该协议
忘记关闭协议 内存泄漏,系统变慢 在Stop()里调用CloseProtocol()
驱动绑定顺序 驱动A依赖驱动B,但B还没加载 用Depex表达式声明依赖

特别提醒:Depex(依赖表达式)是UEFI驱动里最容易写错的地方。我曾经写过一个Depex = gEfiBlockIoProtocolGuid,结果驱动永远不加载——因为那个协议在DXE后期才安装,而我的驱动在DXE早期就被调度了。后来改成Depex = gEfiDiskIoProtocolGuid才解决问题。

2.6 本章小结

这一章我们聊了三件事:

  • UEFI架构:SEC→PEI→DXE→BDS→TSL→RT,每个阶段各司其职。
  • 驱动模型:Supported/Start/Stop三部曲,通过协议绑定来驱动设备。
  • 协议与句柄:句柄是设备,协议是能力,GUID是钥匙。