4、UEFI变量与NVRAM:UEFI变量的概念与分类、NVRAM存储管理、变量读写操作实战
4.1 什么是UEFI变量?
UEFI变量,说白了就是一组键值对。它们存储在NVRAM里,系统重启后不会丢。我刚开始接触UEFI时,总觉得这玩意儿跟注册表差不多——嗯,确实有点像,但更底层、更安全。
每个UEFI变量由三部分组成:
- 变量名(GUID + 名称)——全局唯一标识
- 属性(Attributes)——控制谁能读写、是否掉电保存
- 数据(Data)——实际存储的内容,可以是任意二进制
举个例子,Boot Manager会用变量 BootOrder 和 BootXXXX 来记录启动顺序。你在BIOS设置里改启动项,本质上就是在改这些变量。
核心要点:UEFI变量是固件与操作系统之间的“共享内存”。操作系统可以通过Runtime Services读写它们,固件则在Boot Services阶段使用。
4.2 变量的分类
UEFI规范把变量分成了几类。我习惯按“作用域”和“持久性”来理解:
| 分类 | GUID | 典型用途 | 是否掉电保存 |
|---|---|---|---|
| 全局变量 | EFI_GLOBAL_VARIABLE |
BootOrder、BootXXXX、Timeout | 是 |
| 厂商变量 | 厂商自定义GUID | OEM配置、安全策略 | 是 |
| 运行时变量 | 任意GUID | OS与固件通信 | 视属性而定 |
| 易失变量 | 任意GUID | 临时状态、调试信息 | 否 |
这里有个坑——易失变量。我曾经在调试一个启动失败的问题时,发现某个关键变量在重启后消失了。查了半天,原来是属性里没加 EFI_VARIABLE_NON_VOLATILE。你想想看,变量写进去了,一断电就没了,那不等于白写吗?
4.3 NVRAM存储管理
NVRAM不是无限的。我见过不少工程师把NVRAM当成普通硬盘用,结果系统变砖了。实际上,NVRAM的典型大小只有 64KB ~ 256KB,而且擦写次数有限。
UEFI固件管理NVRAM的方式大致如下:
- 分区管理:NVRAM被划分为多个区域,比如变量存储区、FTW(Fault Tolerant Write)区、日志区。
- 垃圾回收:删除变量时不会立即擦除,而是标记为“已删除”。空间不够时才会触发压缩。
- 容错写入:写变量时先写FTW区,成功后再更新主区。防止写入过程中掉电导致数据损坏。
注意:NVRAM写满后,SetVariable() 会返回 EFI_OUT_OF_RESOURCES。这时候系统可能连启动菜单都进不去。我遇到过一台设备,就是因为某个驱动反复写日志变量,把NVRAM撑爆了。
4.4 变量读写操作实战
实战部分,我们直接看代码。UEFI提供了两个核心Runtime Service:
GetVariable()—— 读变量SetVariable()—— 写变量
4.4.1 读取变量
下面是一个读取 BootOrder 的例子。我个人习惯在读取前先查一下变量是否存在:
#include <Uefi.h>
#include <Library/UefiRuntimeServicesTableLib.h>
EFI_STATUS GetBootOrder(VOID) {
EFI_STATUS Status;
UINTN DataSize = 0;
UINT16 *BootOrder = NULL;
// 第一次调用:获取大小
Status = gRT->GetVariable(
L"BootOrder",
&gEfiGlobalVariableGuid,
NULL,
&DataSize,
NULL
);
if (Status == EFI_BUFFER_TOO_SMALL) {
// 分配内存
BootOrder = (UINT16 *)AllocatePool(DataSize);
if (BootOrder == NULL) {
return EFI_OUT_OF_RESOURCES;
}
// 第二次调用:真正读取
Status = gRT->GetVariable(
L"BootOrder",
&gEfiGlobalVariableGuid,
NULL,
&DataSize,
BootOrder
);
if (EFI_ERROR(Status)) {
FreePool(BootOrder);
return Status;
}
// 打印启动项
UINTN Count = DataSize / sizeof(UINT16);
for (UINTN i = 0; i < Count; i++) {
Print(L"BootOrder[%d] = %04x\n", i, BootOrder[i]);
}
FreePool(BootOrder);
}
return Status;
}
这里有个细节:第一次调用GetVariable时传NULL的Buffer,目的是获取实际数据大小。我刚开始写UEFI代码时,总是先分配一个固定大小的Buffer,结果不是太大就是太小。后来发现这个“两次调用”模式才是标准做法。
4.4.2 写入变量
写变量相对简单,但属性设置要小心。我曾经犯过一个错误:写变量时忘了加 EFI_VARIABLE_BOOTSERVICE_ACCESS,结果进入操作系统后就读不到了。
EFI_STATUS WriteMyVariable(VOID) {
EFI_STATUS Status;
UINT32 Attributes;
CHAR16 *MyData = L"Hello, UEFI!";
UINTN DataSize = (StrLen(MyData) + 1) * sizeof(CHAR16);
// 设置属性:非易失 + 启动时访问 + 运行时访问
Attributes = EFI_VARIABLE_NON_VOLATILE |
EFI_VARIABLE_BOOTSERVICE_ACCESS |
EFI_VARIABLE_RUNTIME_ACCESS;
Status = gRT->SetVariable(
L"MyVar",
&gEfiCallerIdGuid, // 使用调用者自己的GUID
Attributes,
DataSize,
MyData
);
if (EFI_ERROR(Status)) {
Print(L"SetVariable failed: %r\n", Status);
} else {
Print(L"Variable written successfully!\n");
}
return Status;
}
小技巧:调试时可以用 EFI_VARIABLE_BOOTSERVICE_ACCESS 但不加 EFI_VARIABLE_RUNTIME_ACCESS。这样变量只在UEFI环境下可见,操作系统里看不到,方便隔离问题。
4.5 变量属性详解
UEFI规范定义了多种变量属性。我把常用的列出来,方便你对照:
| 属性宏 | 值 | 含义 |
|---|---|---|
EFI_VARIABLE_NON_VOLATILE |
0x00000001 | 掉电后保留 |
EFI_VARIABLE_BOOTSERVICE_ACCESS |
0x00000002 | Boot Services阶段可访问 |
EFI_VARIABLE_RUNTIME_ACCESS |
0x00000004 | Runtime阶段(操作系统下)可访问 |
EFI_VARIABLE_HARDWARE_ERROR_RECORD |
0x00000008 | 硬件错误记录,特殊保护 |
EFI_VARIABLE_AUTHENTICATED_WRITE_ACCESS |
0x00000010 | 需要签名认证才能写入 |
注意:EFI_VARIABLE_RUNTIME_ACCESS 必须与 EFI_VARIABLE_BOOTSERVICE_ACCESS 同时设置。这是UEFI规范的要求,单独设置 RUNTIME_ACCESS 会返回 EFI_INVALID_PARAMETER。
4.6 NVRAM布局与变量管理流程图
下面这张图展示了NVRAM的典型布局,以及变量读写时的数据流向。我画这张图时参考了Intel的EDK2实现:
4.7 避坑指南
最后,分享几个我踩过的坑:
- 变量名大小写敏感:UEFI变量名是大小写敏感的。
BootOrder和bootorder是两个不同的变量。我见过有人因为拼写问题,查了半天找不到变量。 - GUID必须匹配:读变量时GUID必须与写入时一致。不同厂商的GUID不同,别搞混了。
- 不要频繁写变量:NVRAM的擦写次数有限,频繁写入会缩短硬件寿命。我建议只在配置变更时写,不要每次启动都写。
- 注意变量大小限制:单个变量通常不能超过NVRAM分区大小的一半。EDK2默认限制是 8KB 左右。
调试建议:在UEFI Shell下可以用 dmpstore 命令查看所有变量。这是我最常用的调试手段——先看看变量到底有没有写进去,属性对不对。
嗯,关于UEFI变量和NVRAM,今天就聊到这儿。这部分内容虽然基础,但却是整个UEFI引导流程的基石。你想想看,没有变量,Boot Manager怎么知道该启动哪个系统?Secure Boot的密钥存在哪里?所以,把变量搞透,后面的章节会轻松很多。