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卡住查驱动。我曾经花了两天追一个DXE hang,最后发现是某个驱动里有个死循环——嗯,从那以后我写驱动必加看门狗。
2.2 UEFI驱动模型:Bind、Start、Stop
UEFI驱动模型,和Windows/Linux的驱动模型不太一样。它不依赖中断,不依赖DMA,全靠协议和句柄来沟通。你想想看,在EFI环境下,连中断控制器都没初始化,驱动怎么工作?
答案就是:轮询 + 协议绑定。
每个UEFI驱动,本质上就是一个实现了EFI_DRIVER_BINDING_PROTOCOL的模块。这个协议有三个核心函数:
| 函数 | 作用 | 我踩过的坑 |
|---|---|---|
Supported() |
检查驱动是否能控制某个设备 | 返回值写错,驱动死活不绑定 |
Start() |
开始驱动设备,安装协议 | 忘记安装I/O协议,上层驱动找不到 |
Stop() |
停止驱动,卸载协议 | 资源没释放,内存泄漏 |
驱动加载的流程是这样的:
- DXE调度器遍历所有驱动镜像。
- 对每个驱动,调用
Supported()检查它能否控制某个设备句柄。 - 如果返回成功,调用
Start(),驱动开始干活。 - 驱动在
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是钥匙。