BIOS开发笔记 17 – UEFI Capsule Update 是如何被触发的?

平常我们在测试 WU 的时候,都是将 BIOS 做成一个驱动包的形式,像驱动一样安装后,重启便能进行 BIOS 升级。

这里有一个疑问,操作系统是如何“通知”BIOS 进行更新的呢?UEFI 手册对此有定义,它称为 UEFI Capsule Update。操作系统通过调用UEFI的运行时服务,将固件传递给BIOS,BIOS 接收到后即可启动更新。下面来详细了解此更新机制。

 UEFI Capsule Update 机制介绍 

1. 什么是 Capsule Update?

UEFI Capsule Update 是 UEFI 规范定义的一种标准机制,允许操作系统(OS)向固件传递数据块(称为 "Capsule"或胶囊)。最常见的用途是固件更新,它可以是一个 UEFI 固件,也可以是触摸板固件。

它解决了传统固件更新需要运行特定工具的痛点,使得固件更新可以像驱动更新一样在 OS 中无缝完成。

2. 核心流程

整个过程跨越了 OS 运行期和固件启动期(重启后)。

第一阶段:OS 发起更新 (Runtime)

  1. 准备胶囊: OS(如 Windows Update)将固件更新包(Capsule)加载到内存中。
  2. 调用服务: OS 调用 UEFI Runtime Service UpdateCapsule
  3. 设置标志UpdateCapsule 函数会检查标志位。如果包含 CAPSULE_FLAGS_PERSIST_ACROSS_RESET(需要在重启后保留),它会将胶囊的内存地址列表(ScatterGatherList)保存到 UEFI 变量 CapsuleUpdateData 中。
  4. 重启系统: 如果需要,调用 EfiResetSystem 重启计算机。

第二阶段:固件处理更新 (Pre-Boot)

  1. 检测胶囊: 系统重启后,PEI (Pre-EFI Initialization) 阶段会检测内存中是否存在胶囊,或者检查 CapsuleUpdateData 变量。
  2. 处理胶囊: DXE (Driver Execution Environment) 阶段会处理这些胶囊。
  3. 分发更新:
  • 固件根据胶囊中的 ImageTypeId (GUID),找到对应的 FMP 驱动(例如 BIOS 驱动、EC 驱动)。
  • 调用 FMP 协议的 SetImage 函数将新固件写入闪存。

3. Capsule 文件结构

Capsule 文件本质上是一个带有特定文件头的二进制文件。对于现代固件更新,通常使用 FMP Capsule 格式。

其结构层级如下:

A. 通用胶囊头 (EFI_CAPSULE_HEADER)

所有胶囊文件都以这个头开始。定义在 UefiSpec.h 中。

typedef struct {
  EFI_GUID    CapsuleGuid;      // 胶囊类型 GUID (FMP 胶囊通常使用 gEfiFmpCapsuleGuid)
  UINT32      HeaderSize;       // 头大小
  UINT32      Flags;            // 标志位 (如 PERSIST_ACROSS_RESET, POPULATE_SYSTEM_TABLE)
  UINT32      CapsuleImageSize; // 整个胶囊文件的大小
} EFI_CAPSULE_HEADER;

B. FMP 胶囊头 (EFI_FIRMWARE_MANAGEMENT_CAPSULE_HEADER)

如果 CapsuleGuid 是 gEfiFmpCapsuleGuid,则紧随其后的是 FMP 头。定义在 Guid/FmpCapsule.h 中。

typedef struct {
  UINT32    Version;             // 版本 (通常为 1)
  UINT16    EmbeddedDriverCount; // 嵌入驱动数量 (通常为 0)
  UINT16    PayloadItemCount;    // 负载项目数量 (通常为 1)
  // UINT64 ItemOffsetList[];    // 偏移量列表,指向具体的 Image Header
} EFI_FIRMWARE_MANAGEMENT_CAPSULE_HEADER;

C. 镜像头 (EFI_FIRMWARE_MANAGEMENT_CAPSULE_IMAGE_HEADER)

根据偏移量列表找到的具体镜像头。

typedef struct {
  UINT32      Version;                // 版本
  EFI_GUID    UpdateImageTypeId;      // 目标固件的 GUID (重要!用于匹配 ESRT 中的 FwClass)
  UINT8       UpdateImageIndex;       // 索引
  UINT8       reserved_bytes[3];
  UINT32      UpdateImageSize;        // 固件二进制大小
  UINT32      UpdateVendorCodeSize;   // 厂商代码大小
  UINT64      UpdateHardwareInstance; // 硬件实例 ID
} EFI_FIRMWARE_MANAGEMENT_CAPSULE_IMAGE_HEADER;

D. 固件负载 (Payload)

最后是实际的固件二进制数据(如 .bin 或 .rom 文件内容)。

E. 结构示意图

1. Capsule Header:表示这是一个标准的 Capsule 文件,Cap 更新校验的第一层。
2. FMP Capsule Header:表示这是一个 FMP 类型的 Cap 文件,包含了 Payload 的数量及其位置
3. FMP Capsule Image Header:包含 Payload 的信息,比较重要的是UpdateImageTypeId,这是固件中定义的 ESRT GUID,生成 Cap 文件的时候,需指定此 GUID。
4. Firmware Authentication:用于固件安全验证,可选。
5. FMP Payload Header:让 FMP 驱动在不依赖外部元数据的情况下,能直接从数据流中解析出版本号,用于执行 CheckImage 和防回滚检查,可选。

4. Capsule on Disk (文件传输方式)

除了通过内存传递胶囊 (UpdateCapsule),UEFI 还支持通过文件系统传递胶囊,称为 Capsule on Disk。这种方式常用于不支持 Runtime UpdateCapsule 的系统,或者作为一种更简单的更新手段。

核心机制

1. 文件放置: OS 将胶囊文件复制到 EFI 系统分区 (ESP) 的特定目录下:EFI\UpdateCapsule
2. 设置标志: OS 设置 UEFI 变量 OsIndications,通知 BIOS 下次启动时检查该目录。
3. 重启: 系统重启。
4. 固件处理: BIOS 在启动早期 (PEI/DXE) 检测到标志,从磁盘读取文件,并将其加载到内存中,随后流程与普通 Capsule Update 一致。

关键变量与标志

要启用 Capsule on Disk,OS 需要操作以下 UEFI 变量:

  • 变量名OsIndications
  • GUIDgEfiGlobalVariableGuid (Standard Global Variable)
  • 设置值: 需要设置第 2 位 (Bit 2)。
// UefiSpec.h
#define EFI_OS_INDICATIONS_FILE_CAPSULE_DELIVERY_SUPPORTED  0x0000000000000004

操作步骤:

  1. OS 读取 OsIndicationsSupported 变量,检查 Bit 2 是否为 1 (确认 BIOS 支持此功能)。
  2. OS 读取当前的 OsIndications 变量。
  3. OS 将 EFI_OS_INDICATIONS_FILE_CAPSULE_DELIVERY_SUPPORTED (0x04) OR 到当前值中。
  4. OS 调用 SetVariable 写回 OsIndications
  5. OS 将胶囊文件放入 EFIUpdateCapsule
  6. OS 重启系统 (EfiResetWarm 或 EfiResetCold)。

 手动触发 UEFI Capsule Update 

手动触发 Capsule Update,首先要将固件打包成符合 Capsule 标准的文件,即为它加上上述的 Header。EDK2 的代码中,有提供相关的脚本工具,可以用于 Capsule 文件的生成。工具是一个 Python 脚本,位于:
edk2BaseToolsSourcePythonCapsuleGenerateCapsule.py

此脚本是一个通用的 Capsule 文件生成工具,它可以生成 E. 结构示意图中的结构文件。值得注意的是,它会强制加入 FMP Payload Header,像有些 BIOS (如 AMI BIOS)不会校验该区域,会导致数据解析出错,以至无法进行 Capsule update。可以通过修改如下代码去除该 Header:

FmpPayloadHeader.FwVersion = SinglePayloadDescriptor.FwVersion
FmpPayloadHeader.LowestSupportedVersion = SinglePayloadDescriptor.LowestSupportedVersion
FmpPayloadHeader.Payload                = SinglePayloadDescriptor.Payload
# 注释此行 Result = FmpPayloadHeader.Encode ()

打开命令行窗口,执行:

cd BaseToolsSourcePythonCapsule
set PYTHONPATH=x:edk2BaseToolsSourcePython
python.exe GenerateCapsule.py -e BIOS.rom --guid a0327fe0-1fda-4e5b-905d-b510c45a61d0 -o BIOS.cap --fw-version 1 --lsv 1

--fw-version 1 --lsv 1 是属于fmp payload header 的,根据实际情况填写,如果是去掉了,则可随意。--capflag 参数也可以加上,更新完会自动重启,如果是 In-Memory 的还要加上 Persist 参数,以标记重启保留 Capsule 区域内存。注意 Guid 是 ESRT GUID。执行成功后,Capsule 文件便生成了。

Windows 下触发

Windows 下只能使用 On-Disk 的方式,In-Memory 使用调用 UEFI 的 UpdateCapsule 运行时服务,需要特别权限,所以只能使用 On-Disk。可以自己使用 DISKGENIUS 磁盘工具,将 Capsule 文件放至 ESP 分区的 EFIUpdateCapsule 目录,再利用 PowerShell 命令设置 UEFI Variable:

Install-Module -Name UEFIv2 -Scope CurrentUser 
Import-Module UEFIv2
Set-UEFIVariable -VariableName OsIndications -Namespace "{8BE4DF61-93CA-11D2-AA0D-00E098032B8C}" -Value 0x04

EFI Shell 下触发

EFI Shell 下比较简单,因为 EDK2 的代码中,实现一个 Capsule App,直接用它触发更新,工具需要自己编译代码生成 EFI 文件。EDK2 CapsuleApp 路径:
edk2MdeModulePkgApplicationCapsuleApp

Capsule App 实现了 In-Memory 和 On-Disk 的方式:

CapsuleApp.efi BIOS.cap # In-Memory
CapsuleApp.efi BIOS.cap -OD # On-Disk

可以查看当前 Cap 文件的 Header 信息:

CapsuleApp.efi -D BIOS.cap

版权声明:
作者:bin
链接:https://ay123.net/mystudy/bios/1920/
来源:爱影博客
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
海报
BIOS开发笔记 17 – UEFI Capsule Update 是如何被触发的?
平常我们在测试 WU 的时候,都是将 BIOS 做成一个驱动包的形式,像驱动一样安装后,重启便能进行 BIOS 升级。 这里有一个疑问,操作系统是如何“通知”BIOS 进行……
<<上一篇
下一篇>>
文章目录
关闭
目 录