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规范体系的整体结构:

UEFI规范体系结构图 UEFI规范体系结构 UEFI 规范 (UEFI Specification) 核心规范 (Core Spec) 平台初始化规范 (PI Spec) 启动服务 (Boot Services) 运行时服务 (Runtime Services) 协议 (Protocols) gBS->AllocatePages() gRT->GetVariable() EFI_SIMPLE_FILE_SYSTEM 说明 核心规范定义接口,PI规范定义硬件初始化流程。 两者配合,才能完成从Reset到Boot Device的全流程。

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到完整的驱动,一步步来。记住,理解这些概念,比背代码重要得多。