UEFI 规范基础:从起源到核心服务
聊 UEFI 之前,我得先说说 BIOS。你想想看,BIOS 这玩意儿从 1981 年 IBM PC 诞生就跟着了,三十多年没怎么大变。它用 16 位实模式,只能寻址 1MB 空间,启动流程死板得很——先 POST,再读主引导记录,然后加载操作系统。放到今天,这就像用算盘做大数据分析,不是不行,是太憋屈了。
UEFI 的出现,说白了就是要把固件拉进现代世界。它用 32 位或 64 位模式,能直接访问整个内存空间,有文件系统驱动,还能跑网络协议。我 2015 年第一次在 ARM 服务器上碰 UEFI,当时就觉得:这才是固件该有的样子。
UEFI 的起源:Intel 的野心与产业联盟
UEFI 的前身是 Intel 的 EFI(Extensible Firmware Interface),2000 年左右开始搞。Intel 当时推安腾处理器,发现传统 BIOS 根本撑不住 64 位系统的需求。于是他们写了一套全新的固件接口规范,叫 EFI 1.0。
2005 年,Intel 把 EFI 交给 UEFI 论坛管理,规范改名为 UEFI。从 2.0 版本开始,它加入了安全启动、网络支持、多架构兼容等特性。我印象最深的是 UEFI 2.3.1 版本,那会儿安全启动刚普及,很多主板因为签名问题翻车,我们团队没少加班处理。
关键节点:
- 2000 年:Intel 发布 EFI 1.0 规范
- 2005 年:UEFI 论坛成立,规范更名为 UEFI 2.0
- 2010 年:UEFI 2.3 加入安全启动(Secure Boot)
- 2012 年:Windows 8 强制要求 UEFI 安全启动
- 2020 年:UEFI 2.9 规范发布,支持 LoongArch 架构
GUID 分区表:告别 MBR 的 2TB 天花板
传统 MBR 分区表用 32 位逻辑块地址,最大只能管理 2TB 磁盘。你想想,现在随便一块 SSD 都 4TB 了,MBR 根本不够用。GUID 分区表(GPT)用 64 位 LBA,理论上支持 8ZB 的磁盘——这个数字大到什么程度?把地球上所有数据存进去还有富余。
GPT 的结构很清晰:
- 保护 MBR:位于 LBA 0,防止老工具误认磁盘为未分区
- 主 GPT 头:LBA 1,记录分区表位置、大小、CRC32 校验
- 分区表项:LBA 2 开始,每个表项 128 字节,支持 128 个分区
- 备份 GPT:磁盘末尾,防止主 GPT 损坏
我在项目中遇到过最坑的事:某次固件升级后,GPT 头里的 CRC32 校验值没更新,系统死活不认磁盘。查了两天才发现是固件 bug——备份 GPT 是对的,但主 GPT 头校验失败,UEFI 直接拒绝加载。从那以后,我每次做分区操作都会手动校验 CRC。
个人习惯:做嵌入式系统时,我会在 GPT 分区表里预留一个 16MB 的“固件备份分区”。万一主固件刷坏了,还能从备份分区启动恢复。这个习惯救过我两次。
启动管理器:UEFI 的“大脑”
UEFI 启动管理器是整个启动流程的调度中心。它不直接加载操作系统,而是管理启动项(Boot Option)。每个启动项包含:
- 启动描述(比如 "Windows Boot Manager")
- 设备路径(指向 EFI 系统分区)
- 文件路径(指向 .efi 文件)
- 启动参数(可选)
启动管理器按优先级遍历启动项,找到第一个能启动的就执行。如果所有启动项都失败,它会进入 UEFI Shell 或者显示错误界面。
我记得有一次调试,系统总是跳过 UEFI 网络启动直接进硬盘。查了半天发现是启动项顺序里 PXE 的优先级被设成了最低。用 bcfg 命令调了一下顺序就好了——这种问题看着简单,但没经验的人能折腾一整天。
// 查看当前启动项
Shell> bcfg boot dump
// 设置 PXE 为第一启动项
Shell> bcfg boot -mv 3 0
// 添加自定义启动项
Shell> bcfg boot -a 0 "MyApp" FS0:\EFI\MyApp.efi
注意:UEFI 启动项存储在 NVRAM 里。NVRAM 的写入次数有限,频繁修改启动项可能损坏 NVRAM。我见过有人写脚本每秒改一次启动顺序,结果三天后主板变砖。
运行时服务与启动时服务
UEFI 把服务分成两类:启动时服务和运行时服务。这个区分很关键,直接决定了固件和操作系统怎么交互。
| 特性 | 启动时服务 | 运行时服务 |
|---|---|---|
| 可用阶段 | 固件启动期间 | 启动期间 + 操作系统运行后 |
| 内存占用 | 可被 OS 回收 | 必须保留 |
| 典型功能 | 内存管理、协议接口、设备操作 | 时间、变量、重置 |
| 调用方式 | 通过 Boot Services 表 | 通过 Runtime Services 表 |
| 退出后 | 调用 ExitBootServices() 后不可用 | 始终可用 |
启动时服务包括:
- 内存管理:分配/释放内存、获取内存映射
- 协议管理:安装、查找、注册协议
- 设备操作:读写 I/O、中断管理
- 图像加载:加载并启动 EFI 应用程序
运行时服务包括:
- 变量服务:读写 UEFI 变量(存储在 NVRAM)
- 时间服务:获取/设置系统时间、闹钟
- 重置服务:冷重启、热重启、关机
- 虚拟地址映射:OS 切换页表时通知固件
这里有个容易踩的坑:操作系统调用 ExitBootServices() 之后,启动时服务就没了。如果你在 OS 内核里还想用 UEFI 的协议接口,那肯定崩溃。我见过有人把 UEFI 图形协议当成帧缓冲来用,结果 ExitBootServices 一调用,屏幕直接黑了。
实战经验:在 ATF 和 UEFI 协同启动时,ATF 的 BL31 运行在 EL3 异常级别,它需要 UEFI 的运行时服务来读写 NVRAM 变量。但 UEFI 的运行时服务是跑在 EL1 的,所以中间要经过 SMC 调用和异常级别切换。这个机制我后面会详细讲。
UEFI 启动流程全景图
下面这张图展示了 UEFI 从上电到加载 OS 的完整流程。我特意把启动时服务和运行时服务的分界点标出来了。
从图上你能看到,UEFI 的启动时服务覆盖了 SEC 到 TSL 阶段,而运行时服务从 RT 阶段开始一直持续到系统关机。ExitBootServices() 就是那个分水岭——过了这道门,启动时服务就跟你没关系了。
嗯,这里要注意:UEFI 的运行时服务虽然名字叫“运行时”,但它其实是一组回调函数,由操作系统在切换页表时调用。比如操作系统要休眠了,它会调用 UEFI 的 SetVirtualAddressMap() 告诉固件新的虚拟地址映射。如果固件没处理好这个转换,系统唤醒后就会崩溃。
我在调试某款 ARM 服务器时遇到过:操作系统调用 SetVirtualAddressMap() 后,UEFI 的 GetTime() 服务返回的时间全是乱的。查到最后发现是固件里有一个全局指针没做虚拟地址转换,还在用物理地址访问。这种问题在 x86 上不常见,但在 ARM 上因为页表切换更频繁,很容易翻车。
避坑指南:写 UEFI 运行时服务驱动时,所有全局指针都要用 EfiConvertPointer() 做虚拟地址转换。我习惯在驱动入口就注册一个虚拟地址转换回调,省得后面忘了。
好了,UEFI 的基础框架就聊到这儿。GPT 分区表、启动管理器、两类服务的划分——这些概念搞清楚了,后面讲 UEFI 和 ATF 怎么握手、怎么传数据,你就能跟上节奏了。