2、UEFI规范与架构:UEFI规范体系、UEFI驱动模型、UEFI运行时服务与启动服务
这一章,我们聊聊UEFI的骨架——规范体系与核心架构。很多初学者一上来就啃Spec,结果被几百页的文档劝退了。其实没必要,你只需要抓住几个关键点:规范怎么分层、驱动怎么跑、服务怎么用。搞懂这些,后面写代码心里就有底了。
2.1 UEFI规范体系:从Spec到落地
UEFI规范,说白了就是一份“接口合同”。它规定了固件和操作系统之间怎么对话,驱动怎么加载,内存怎么管理。我刚开始接触UEFI时,总觉得这玩意儿太抽象。直到有一次,我在调试一个NVMe驱动加载失败的问题,翻到Spec里关于EFI_DRIVER_BINDING_PROTOCOL的章节,才恍然大悟——原来规范里每一句话,背后都是血的教训。
UEFI规范体系大致分为三层:
- 核心规范(UEFI Specification):定义了系统表、启动服务、运行时服务、协议接口等基础框架。这是所有UEFI固件的“宪法”。
- 平台初始化规范(PI Specification):聚焦于硬件初始化流程,比如CPU、芯片组、主板的早期启动。这部分更多是BIOS工程师的战场。
- 类属扩展规范:比如UEFI Shell规范、ACPI规范、SMBIOS规范等。它们补充了具体场景下的接口定义。
我的经验: 做固件开发,核心规范和PI规范是必读的。但别从头到尾啃,先看目录,找到你当前模块对应的章节。比如写网络驱动,就重点看Simple Network Protocol那部分。我习惯把Spec里关键的Protocol GUID和数据结构截图存下来,调试时直接翻,比搜PDF快多了。
下面这张图,帮你理清UEFI规范体系的整体结构:
2.2 UEFI驱动模型:谁加载了谁?
UEFI的驱动模型,和传统BIOS的INT 13h那种“硬编码”方式完全不同。它更像一个“插拔式”的框架。驱动本身是一个EFI Image,通过EFI_DRIVER_BINDING_PROTOCOL来声明自己能驱动哪种硬件。系统在启动时,会遍历所有驱动,找到匹配的硬件,然后绑定。
我举个例子。你写了一个NVMe驱动,它会在Supported()函数里检查设备的Class Code是不是NVMe控制器。如果是,就返回成功。然后系统调用Start(),驱动开始初始化设备,安装EFI_BLOCK_IO_PROTOCOL。这样,上层文件系统驱动就能通过Block IO来读写NVMe盘了。
避坑指南: 我曾经在一个项目里,驱动加载顺序出了问题。原因是两个驱动都声称支持同一个硬件,但Supported()函数的优先级没处理好。结果系统加载了错误的驱动,导致磁盘无法识别。后来我加了一个版本号检查,才搞定。记住:Supported()里一定要做充分的硬件特征匹配,别偷懒只检查GUID。
驱动模型的核心接口就三个函数:
| 函数 | 作用 | 调用时机 |
|---|---|---|
Supported() |
检查驱动是否能控制该硬件 | 系统枚举设备时 |
Start() |
初始化硬件,安装协议 | 匹配成功后 |
Stop() |
卸载驱动,释放资源 | 驱动被移除或系统退出时 |
你想想看,这种模型的好处是什么?松耦合。驱动和硬件之间没有强绑定,系统可以动态加载、卸载驱动。这也是UEFI能支持多种启动设备(USB、NVMe、网络)的原因。
2.3 UEFI启动服务:Boot Services
启动服务,是UEFI固件在启动阶段提供的一组核心API。它们只在Boot Manager退出前可用。一旦操作系统接管,这些服务就失效了。说白了,它们是“临时工”。
启动服务涵盖的功能很多,我挑几个最常用的:
- 内存管理:
AllocatePages()、FreePages()。我习惯用AllocateAnyPages类型,让固件自己选地址,省心。 - 事件与定时器:
CreateEvent()、SetTimer()。用于实现超时、轮询等异步操作。 - 协议管理:
InstallProtocolInterface()、LocateProtocol()。这是驱动和应用程序之间通信的桥梁。 - 图像加载:
LoadImage()、StartImage()。用于加载并执行EFI应用程序或驱动。
注意: 启动服务在ExitBootServices()被调用后,就彻底不能用了。如果你在操作系统内核里还调用gBS->AllocatePages(),系统会直接崩溃。我见过有同事在OS Loader里忘了关Boot Services,结果内核启动时访问了已经被释放的内存,查了一整天才找到原因。
2.4 UEFI运行时服务:Runtime Services
运行时服务,和启动服务正好相反。它们在操作系统运行期间依然可用。为什么需要这个?因为有些操作必须由固件来完成,比如读写UEFI变量、重置系统、获取时间等。
运行时服务包括:
- 变量服务:
GetVariable()、SetVariable()。用于存储启动配置、Boot Manager条目等。这些变量保存在NVRAM里,断电不丢失。 - 时间服务:
GetTime()、SetTime()。读取或设置硬件时钟。 - 重置服务:
ResetSystem()。冷重启、热重启或关机。 - 虚拟内存映射:
SetVirtualAddressMap()。用于在操作系统启用分页后,将固件使用的物理地址映射到虚拟地址。
这里有个关键点:运行时服务必须运行在物理地址模式下,直到操作系统调用SetVirtualAddressMap()。之后,固件代码会切换到虚拟地址模式。这个过程如果没处理好,系统会在运行时崩溃。
我的建议: 写运行时服务代码时,一定要小心内存访问。因为操作系统可能已经把内存页换出去了,或者映射到了不同的虚拟地址。我习惯在SetVirtualAddressMap()回调里,把所有指针都转换成虚拟地址。否则,半夜被叫起来修Bug的就是你。
2.5 启动服务 vs 运行时服务:一张表说清楚
| 对比项 | 启动服务 (Boot Services) | 运行时服务 (Runtime Services) |
|---|---|---|
| 生命周期 | 从固件启动到ExitBootServices() |
从固件启动到系统关机 |
| 可用场景 | 驱动加载、硬件初始化、OS Loader | OS运行时、S3唤醒、变量读写 |
| 内存模式 | 物理地址 | 物理地址 → 虚拟地址(转换后) |
| 典型接口 | AllocatePages, LocateProtocol |
GetVariable, ResetSystem |
| 代码驻留 | 可被OS覆盖 | 必须保留在内存中 |
嗯,到这里,UEFI规范与架构的核心内容就讲完了。你可能会问:这些服务怎么用?别急,后面几章我们会手把手写代码,从HelloWorld到完整的驱动,一步步来。记住,理解这些概念,比背代码重要得多。