2. UEFI 规范核心概念:系统表、启动服务、运行时服务、协议与句柄
UEFI 规范定义了一套标准化的固件-操作系统接口,其核心由几个关键抽象构成:UEFI 系统表、启动服务、运行时服务、协议与句柄。理解这些概念是进行 UEFI 固件开发的基础。
2.1 UEFI 系统表 (System Table)
UEFI 系统表(EFI_SYSTEM_TABLE)是整个 UEFI 环境的入口点。它是一个全局结构体,包含了指向所有核心服务和配置信息的指针。在 UEFI 应用程序或驱动程序的入口点(ImageHandle 和 SystemTable 参数)中,系统表被传递给开发者。
系统表的主要组成部分包括:
- 固件供应商信息:固件版本号和供应商名称。
- 控制台句柄:标准输入、标准输出和标准错误输出设备的句柄。
- 启动服务表:指向
EFI_BOOT_SERVICES结构体的指针。 - 运行时服务表:指向
EFI_RUNTIME_SERVICES结构体的指针。 - 配置表:指向系统配置表(如 ACPI、SMBIOS)的指针数组。
关键点:系统表是 UEFI 应用的“根对象”,所有其他服务和资源都通过它来访问。
2.2 启动服务 (Boot Services)
启动服务(EFI_BOOT_SERVICES)提供了一组在操作系统启动前可用的函数接口。这些服务在操作系统加载器(OS Loader)调用 ExitBootServices() 后不再可用。
启动服务的主要功能类别:
| 服务类别 | 说明 | 关键函数示例 |
|---|---|---|
| 事件与定时器 | 创建、等待和关闭事件,设置定时器。 | CreateEvent, WaitForEvent, SetTimer |
| 内存管理 | 分配和释放内存页或内存池。 | AllocatePages, FreePages, AllocatePool |
| 协议管理 | 安装、卸载、查找和打开协议接口。 | InstallProtocolInterface, LocateProtocol, OpenProtocol |
| 句柄管理 | 安装、卸载和定位句柄。 | InstallMultipleProtocolInterfaces, LocateHandleBuffer |
| 镜像管理 | 加载、启动和卸载 UEFI 镜像。 | LoadImage, StartImage, UnloadImage |
| I/O 操作 | 对设备进行内存映射 I/O 或端口 I/O 操作。 | IoRead, IoWrite, PciRead, PciWrite |
开发提示:在编写 UEFI 驱动时,大部分初始化工作(如注册协议、分配内存)都在启动服务阶段完成。
2.3 运行时服务 (Runtime Services)
运行时服务(EFI_RUNTIME_SERVICES)在操作系统启动后仍然可用。它们提供与硬件平台状态相关的关键功能,例如时间管理、变量存储和重置系统。
运行时服务的主要功能:
- 变量服务:
GetVariable,SetVariable,GetNextVariableName。用于在 NVRAM 中存储和检索非易失性数据。 - 时间服务:
GetTime,SetTime,GetWakeupTime,SetWakeupTime。用于访问系统实时时钟。 - 重置服务:
ResetSystem。用于冷重启、热重启或关机。 - 虚拟地址转换:
SetVirtualAddressMap。用于在操作系统启用分页后,将运行时服务从物理地址映射到虚拟地址。
注意事项:运行时服务代码必须使用
EFI_RUNTIME_SERVICES 数据段,并且不能依赖启动服务中的内存分配或协议查找功能。
2.4 协议 (Protocol) 与句柄 (Handle) 机制
UEFI 的驱动模型基于 句柄 和 协议 的抽象。句柄代表一个实体(如设备、驱动或镜像),而协议是附加到句柄上的接口结构体,定义了该实体提供的功能。
2.4.1 句柄 (Handle)
句柄是一个不透明的指针(EFI_HANDLE),它唯一标识一个 UEFI 对象。句柄本身不包含数据,它只是一个标识符,用于关联一组协议。
2.4.2 协议 (Protocol)
协议是一个 GUID 标识的接口结构体,包含一组函数指针和数据字段。例如,EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL 定义了 OutputString() 函数用于在屏幕上打印文本。
协议与句柄的关系:
- 一个句柄可以安装多个协议。
- 一个协议可以被多个句柄安装。
- 通过
LocateProtocol()或OpenProtocol()可以获取指定句柄上的协议接口。
核心思想:UEFI 驱动通过“生产”和“消费”协议来工作。一个驱动安装协议到句柄上,另一个驱动通过打开该句柄上的协议来使用其功能。
2.5 协议与句柄的交互流程
在 UEFI 驱动模型中,设备驱动通常遵循以下流程来使用协议和句柄:
- 绑定:驱动通过
Supported()检查句柄是否支持其设备,然后通过Start()初始化设备。 - 安装协议:驱动在初始化时,通过
InstallMultipleProtocolInterfaces()将一个或多个协议接口安装到设备句柄上。 - 打开协议:其他驱动或应用通过
OpenProtocol()或LocateProtocol()获取已安装的协议接口。 - 使用协议:通过协议结构体中的函数指针调用设备功能。
- 关闭协议:使用完毕后,通过
CloseProtocol()释放协议接口。
开发实践:使用
LocateHandleBuffer() 可以枚举系统中所有安装了特定协议的句柄,这是实现设备枚举的常用方法。
2.6 核心概念对比
| 概念 | 本质 | 生命周期 | 访问方式 | 典型用途 |
|---|---|---|---|---|
| 系统表 | 全局结构体指针 | 整个 UEFI 运行期 | 入口参数 | 获取所有服务入口 |
| 启动服务 | 函数指针表 | 启动阶段(ExitBootServices 前) | gBS->Function() |
内存分配、协议管理 |
| 运行时服务 | 函数指针表 | 整个系统运行期 | gRT->Function() |
变量存储、时间访问 |
| 句柄 | 不透明标识符 | 由安装者管理 | LocateHandle |
标识设备或驱动 |
| 协议 | GUID 标识的结构体 | 由安装者管理 | OpenProtocol |
提供设备功能接口 |
常见误区:不要混淆
gBS 和 gRT 的使用场景。在 ExitBootServices() 之后调用 gBS 的任何函数都会导致系统崩溃。