2、UEFI规范体系:UEFI规范架构、EFI系统表、运行时服务与启动服务

好,我们正式进入UEFI的核心地带。这一章,我打算把UEFI规范体系给你拆开揉碎了讲清楚。

很多人学UEFI,上来就啃代码,结果越看越懵。为什么?因为没搞懂规范层面的骨架。UEFI规范本身是一套接口契约,不是实现。你想想看,如果连EFI系统表长什么样、启动服务和运行时服务到底有什么区别都搞不清楚,那看代码就像看天书。

我个人习惯,学任何底层系统,先画图,再读规范,最后才看代码。这一章,我们就按这个路子来。

2.1 UEFI规范的整体架构

UEFI规范不是一本书,而是一套文档族。它定义了从固件到操作系统之间的标准接口。说白了,就是让不同厂商的固件和不同厂商的操作系统能互相认识。

整个规范架构可以分成三层:

  • 底层:硬件抽象层,处理CPU、芯片组、总线的差异。
  • 核心层:EFI系统表、启动服务、运行时服务、协议接口。
  • 应用层:UEFI驱动、UEFI应用(比如Shell、Boot Manager)。

我在项目中遇到过最典型的问题:某厂商的固件在启动服务阶段一切正常,但一进入操作系统就蓝屏。查到最后,是运行时服务里的SetVariable接口实现不符合规范。你看,规范细节决定成败。

核心观点:UEFI规范的核心价值在于「标准化接口」。只要固件和OS都遵守同一套接口契约,就能实现跨平台、跨厂商的启动兼容性。

下面这张图,是我自己画的一个UEFI规范架构总览。你盯着看两分钟,后面讲的所有东西都能找到位置。

UEFI规范架构总览 UEFI规范架构总览 应用层 UEFI驱动 | UEFI应用 (Shell, Boot Manager, 诊断工具) 通过协议接口调用核心服务 核心服务层 EFI系统表 启动服务 (Boot Services) 运行时服务 (Runtime Services) 协议接口 (Protocols): 设备路径、简单文件系统、图形输出... Handle Database & 事件管理 硬件抽象层 CPU架构封装 | 芯片组初始化 | 总线枚举 提供统一的硬件访问接口给上层 💡 关键点:启动服务在ExitBootServices()后失效,运行时服务持续可用 协议接口是UEFI的「插件」机制,驱动通过安装协议来扩展功能

2.2 EFI系统表——UEFI世界的入口

EFI系统表,简称gST。这是UEFI固件暴露给所有UEFI应用的第一个数据结构。你可以把它理解成操作系统的「内核控制块」。没有它,你连一个UEFI协议都拿不到。

它的定义在UEFI规范PDF里,但我给你提炼一下核心字段:

字段 类型 说明
Hdr EFI_TABLE_HEADER 包含签名(54524953)、版本号、头大小、CRC32校验
FirmwareVendor CHAR16* 固件厂商字符串,比如"American Megatrends"
FirmwareRevision UINT32 固件版本号,高16位主版本,低16位次版本
ConsoleInHandle EFI_HANDLE 当前控制台输入设备的Handle
ConIn EFI_SIMPLE_TEXT_INPUT_PROTOCOL* 键盘输入协议接口
ConsoleOutHandle EFI_HANDLE 当前控制台输出设备的Handle
ConOut EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL* 屏幕输出协议接口
BootServices EFI_BOOT_SERVICES* 指向启动服务表的指针
RuntimeServices EFI_RUNTIME_SERVICES* 指向运行时服务表的指针
NumberOfTableEntries UINTN 配置表项数量
ConfigurationTable EFI_CONFIGURATION_TABLE* 配置表数组,包含ACPI、SMBIOS、DTB等系统表

实战技巧:我调试UEFI应用时,第一件事就是打印gST->FirmwareVendor和gST->FirmwareRevision。这能立刻确认固件版本和厂商,避免因为固件bug浪费大量时间。

EFI系统表在UEFI应用入口函数EFI_ENTRY_POINT中作为参数传入。原型是这样的:

typedef EFI_STATUS (EFIAPI *EFI_IMAGE_ENTRY_POINT)(
    IN EFI_HANDLE           ImageHandle,
    IN EFI_SYSTEM_TABLE     *SystemTable
);

嗯,这里要注意:SystemTable指针在整个UEFI启动服务阶段都有效。但一旦调用了ExitBootServices(),系统表里的启动服务指针就变成无效了。运行时服务指针还能用,但启动服务相关的字段就别碰了。

2.3 启动服务——固件世界的「操作系统」

启动服务,说白了就是UEFI固件在启动阶段提供的一套API。它涵盖了内存管理、协议管理、事件管理、映像加载等核心功能。

我列一下最常用的几个:

  • 内存管理:AllocatePages、FreePages、GetMemoryMap。注意,GetMemoryMap返回的内存映射是操作系统加载器获取物理内存布局的唯一途径。
  • 协议管理:InstallProtocolInterface、LocateProtocol、OpenProtocol。这是UEFI的「依赖注入」机制。
  • 事件管理:CreateEvent、SignalEvent、WaitForEvent。用于异步操作和定时器。
  • 映像加载:LoadImage、StartImage。这是Boot Manager启动操作系统的核心流程。

我曾经踩过一个坑:在ExitBootServices()之后还调用AllocatePages。结果系统直接挂死。为什么?因为启动服务在退出后,固件不再保证这些接口可用。有些固件会直接把启动服务表置零,有些会返回错误码,但最坑的是那些什么都不做、让你调用后静默崩溃的。

警告:ExitBootServices()是UEFI启动流程的分水岭。调用前,你拥有固件提供的全套服务;调用后,你只能使用运行时服务。任何在ExitBootServices之后调用启动服务的行为,都属于未定义行为。

2.4 运行时服务——跨越启动与OS的桥梁

运行时服务和启动服务最大的区别是什么?生命周期。

启动服务只在固件控制阶段有效。运行时服务从固件启动开始,一直持续到操作系统运行期间,甚至关机前。它由固件保留在内存中,操作系统通过SetVirtualAddressMap重映射后继续使用。

运行时服务主要包括:

服务名称 功能 我遇到的坑
GetTime / SetTime 读取/设置硬件实时时钟 某平台GetTime返回的时区字段始终为0,导致OS时间错乱
GetVariable / SetVariable 读写UEFI非易失变量 变量存储空间耗尽时SetVariable返回EFI_OUT_OF_RESOURCES
GetNextVariableName 枚举所有变量名 用于调试时遍历固件配置
SetVirtualAddressMap 将运行时服务从物理地址切换到虚拟地址 调用顺序必须在ExitBootServices之后,且只能调用一次
ConvertPointer 辅助指针地址转换 很少直接使用,但OS加载器内部依赖它
ResetSystem 冷重启、热重启或关机 调用后不返回,确保清理所有资源

我个人觉得,运行时服务里最容易被忽视的是GetVariable和SetVariable。UEFI变量是固件和操作系统之间持久化配置的唯一通道。比如,Windows的Boot Configuration Data (BCD) 就存储在UEFI变量里。如果变量服务出问题,系统连启动菜单都显示不出来。

2.5 启动服务与运行时服务的核心差异

我习惯用一张对比表来总结这两个服务的区别。你记牢这张表,面试和实战都够用。

对比维度 启动服务 (Boot Services) 运行时服务 (Runtime Services)
生命周期 固件启动 → ExitBootServices() 固件启动 → 关机 (持续可用)
内存驻留 可被OS回收覆盖 固件保留,OS不能覆盖
地址模式 始终物理地址 物理地址 → 虚拟地址 (通过SetVirtualAddressMap)
主要功能 内存、协议、事件、映像管理 时间、变量、重启、地址转换
调用者 UEFI驱动、UEFI应用、Boot Manager UEFI应用、OS内核、OS运行时驱动
典型风险 ExitBootServices后调用 → 崩溃 虚拟地址映射错误 → 页错误

一句话总结:启动服务是固件给UEFI应用准备的「开发工具包」,用完即弃;运行时服务是固件给操作系统准备的「持久化接口」,伴随系统整个生命周期。

好,这一章的内容就到这里。EFI系统表是UEFI世界的入口,启动服务是固件阶段的核心能力,运行时服务是跨越固件和OS的桥梁。这三者构成了UEFI规范体系的骨架。你把这些概念吃透了,后面看UEFI驱动模型、Boot Manager流程、Secure Boot实现,都会轻松很多。