2、UEFI规范体系:UEFI规范架构、EFI系统表、运行时服务与启动服务
好,我们正式进入UEFI的核心地带。这一章,我打算把UEFI规范体系给你拆开揉碎了讲清楚。
很多人学UEFI,上来就啃代码,结果越看越懵。为什么?因为没搞懂规范层面的骨架。UEFI规范本身是一套接口契约,不是实现。你想想看,如果连EFI系统表长什么样、启动服务和运行时服务到底有什么区别都搞不清楚,那看代码就像看天书。
我个人习惯,学任何底层系统,先画图,再读规范,最后才看代码。这一章,我们就按这个路子来。
2.1 UEFI规范的整体架构
UEFI规范不是一本书,而是一套文档族。它定义了从固件到操作系统之间的标准接口。说白了,就是让不同厂商的固件和不同厂商的操作系统能互相认识。
整个规范架构可以分成三层:
- 底层:硬件抽象层,处理CPU、芯片组、总线的差异。
- 核心层:EFI系统表、启动服务、运行时服务、协议接口。
- 应用层:UEFI驱动、UEFI应用(比如Shell、Boot Manager)。
我在项目中遇到过最典型的问题:某厂商的固件在启动服务阶段一切正常,但一进入操作系统就蓝屏。查到最后,是运行时服务里的SetVariable接口实现不符合规范。你看,规范细节决定成败。
核心观点:UEFI规范的核心价值在于「标准化接口」。只要固件和OS都遵守同一套接口契约,就能实现跨平台、跨厂商的启动兼容性。
下面这张图,是我自己画的一个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实现,都会轻松很多。