3、UEFI基础概念:规范、GUID、Protocol、Handle Database、Boot Services与Runtime Services

好,我们正式开始啃UEFI的核心概念。说实话,我刚接触UEFI那会儿,面对这一堆术语也是一头雾水。什么Protocol、Handle、Boot Services……感觉像进了迷宫。但后来我发现,只要抓住一条主线——UEFI本质上是一个微型的、模块化的操作系统——这些东西就都好理解了。

这一章,我们就来拆解UEFI的六大基础概念。它们是后续所有开发工作的基石。

3.1 UEFI规范:不只是BIOS的替代品

UEFI规范,说白了就是一套标准接口。它定义了固件和操作系统之间怎么对话。我经常跟团队里的新人说:别把UEFI当成BIOS 2.0,它俩的架构完全不同。

  • BIOS:运行在16位实模式,寻址空间只有1MB,启动慢,界面丑。
  • UEFI:运行在32/64位保护模式或长模式,可以访问全部内存,支持网络、图形界面,模块化设计。

UEFI规范由UEFI论坛维护,目前主流版本是2.x。规范里定义了所有东西——从系统表、启动管理器,到各种Protocol的接口。你写UEFI驱动时,其实就是在实现规范里定义的接口。

核心要点:UEFI规范是“契约”。你遵守它,你的固件就能和任何遵守它的操作系统(Windows、Linux、macOS)正常通信。

3.2 GUID:UEFI世界的身份证

在UEFI里,每个东西都有一个唯一的128位标识符,这就是GUID(Globally Unique Identifier)。

为什么需要GUID?因为UEFI是模块化的。你想啊,系统里可能有几十个Protocol、几百个变量、上千个Handle。如果没有唯一ID,怎么区分它们?

GUID的格式长这样:

typedef struct {
    UINT32  Data1;
    UINT16  Data2;
    UINT16  Data3;
    UINT8   Data4[8];
} GUID;

在代码里,我们通常用宏来定义GUID:

// EFI_LOADED_IMAGE_PROTOCOL 的 GUID
#define EFI_LOADED_IMAGE_PROTOCOL_GUID \
    {0x5B1B31A1, 0x9562, 0x11d2, \
    {0x8E, 0x3F, 0x00, 0xA0, 0xC9, 0x69, 0x72, 0x3B}}

我个人习惯把GUID定义放在单独的.h文件里,方便复用。你在开发中会频繁用到GUID——注册Protocol、查找Handle、读写变量,都离不开它。

小技巧:可以用 uuidgen 命令(Linux)或 guidgen.exe(Windows)生成新的GUID。别自己瞎编,容易冲突。

3.3 Protocol:UEFI的“接口”与“服务”

Protocol是UEFI里最重要的概念之一。你可以把它理解成C++里的抽象类,或者COM里的接口。每个Protocol定义了一组函数指针(方法),以及相关的数据结构。

举个例子,EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL 定义了输出字符的方法:

typedef struct _EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL {
    EFI_TEXT_RESET             Reset;
    EFI_TEXT_OUTPUT_STRING     OutputString;
    EFI_TEXT_TEST_STRING       TestString;
    // ... 其他方法
} EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL;

你调用 OutputString 就能在屏幕上打印文字。至于底层是往串口写还是往帧缓冲写,你不用关心——这就是Protocol的封装魅力。

我在项目中遇到过一个问题:两个驱动都实现了同一个Protocol,结果系统不知道该用哪个。后来发现是GUID搞混了。记住:Protocol靠GUID唯一标识,同名不同GUID就是两个不同的Protocol。

3.4 Handle Database:对象的“注册中心”

Handle Database,说白了就是一个全局的“对象仓库”。每个Handle是一个不透明的指针,指向某个设备或驱动实例。而Protocol就“挂载”在Handle上。

结构关系是这样的:

  • Handle:一个编号,代表一个对象(设备、驱动、镜像等)。
  • Protocol Interface:挂在Handle上的具体接口实例。
  • Handle Database:管理所有Handle和Protocol的数据库。

你想想看,系统启动时,PCI总线驱动枚举到一块网卡,它会创建一个Handle,然后把 EFI_PCI_IO_PROTOCOL 挂上去。网卡驱动发现这个Handle上有 EFI_PCI_IO_PROTOCOL,就知道“哦,这是个PCI设备”,然后加载自己的驱动,再挂上 EFI_SIMPLE_NETWORK_PROTOCOL。

这就是UEFI的驱动模型——基于Handle和Protocol的“匹配-绑定”机制。

避坑指南:我曾经在调试一个USB键盘驱动时,发现Handle Database里出现了重复的Handle。原因是驱动在卸载时没有正确清理Protocol。这会导致系统越来越慢,甚至崩溃。所以,创建和销毁Handle一定要成对出现。

3.5 Boot Services 与 Runtime Services

UEFI把服务分成两大类:Boot Services和Runtime Services。这个划分很关键,直接决定了你的代码什么时候能跑、什么时候不能跑。

特性 Boot Services Runtime Services
可用时段 固件启动阶段(Boot Time) 启动阶段 + 操作系统运行时(Runtime)
退出Boot Services后 不可用 仍然可用
主要功能 内存管理、Protocol管理、事件、加载镜像 变量服务、时间服务、重置系统
典型接口 AllocatePages, LocateProtocol, StartImage GetVariable, SetVariable, ResetSystem

这里有个重要的分水岭:ExitBootServices()。操作系统加载器(如GRUB、Windows Boot Manager)在启动内核之前,会调用这个函数。一旦调用,Boot Services就“退休”了,所有相关的内存、Protocol、事件都不能再用了。

嗯,这里要注意:Runtime Services必须用特殊内存类型(EfiRuntimeServicesData/EfiRuntimeServicesCode)分配,否则操作系统接管后,这些代码和数据会被覆盖。

警告:不要在Runtime Services里调用Boot Services的函数!我见过有人写UEFI变量驱动时,在 SetVariable 里调用了 AllocatePool。结果操作系统一启动,系统直接蓝屏。因为 AllocatePool 属于Boot Services,那时候已经不可用了。

3.6 知识体系总览

下面这张图把本章的核心概念串起来了。你可以看到UEFI规范在最顶层,Handle Database是中心,Protocol挂在Handle上,Boot/Runtime Services提供底层能力。

UEFI基础概念体系结构图 UEFI基础概念体系结构 UEFI规范 (Specification) Handle Database (对象注册中心) Handle (设备/驱动实例) Handle (镜像/文件) Protocol (接口实例) + GUID Protocol (接口实例) + GUID Boot Services (启动阶段可用) Runtime Services (启动+运行时可用) 说明:Protocol通过GUID唯一标识,挂在Handle上;Handle Database管理所有Handle; Boot/Runtime Services提供底层能力,ExitBootServices()是两者的分水岭。

3.7 小结

这一章的内容有点多,但都是硬通货。我建议你记住几个关键点:

  • GUID是UEFI世界的身份证,Protocol、Handle、变量都靠它区分。
  • Protocol是接口,Handle是对象实例,Handle Database是对象管理器。
  • Boot Services和Runtime Services的划分,决定了你的代码生命周期。