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的方式大致如下:

  1. 分区管理:NVRAM被划分为多个区域,比如变量存储区、FTW(Fault Tolerant Write)区、日志区。
  2. 垃圾回收:删除变量时不会立即擦除,而是标记为“已删除”。空间不够时才会触发压缩。
  3. 容错写入:写变量时先写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实现:

NVRAM布局与变量管理流程图 NVRAM布局与变量管理流程 变量存储区 BootOrder, BootXXXX OEM配置, 安全策略 运行时变量 FTW区 容错写入保护 写入时先写FTW 成功后再更新主区 日志/调试区 错误记录 调试信息 硬件错误记录 写入保护 确认后更新 错误记录 GetVariable() SetVariable() DeleteVariable() 读取 写入 删除 说明 NVRAM总容量通常为64KB~256KB,擦写次数有限 写入变量时先写FTW区,成功后再更新主区,防止掉电损坏

4.7 避坑指南

最后,分享几个我踩过的坑:

  • 变量名大小写敏感:UEFI变量名是大小写敏感的。BootOrder 和 bootorder 是两个不同的变量。我见过有人因为拼写问题,查了半天找不到变量。
  • GUID必须匹配:读变量时GUID必须与写入时一致。不同厂商的GUID不同,别搞混了。
  • 不要频繁写变量:NVRAM的擦写次数有限,频繁写入会缩短硬件寿命。我建议只在配置变更时写,不要每次启动都写。
  • 注意变量大小限制:单个变量通常不能超过NVRAM分区大小的一半。EDK2默认限制是 8KB 左右。

调试建议:在UEFI Shell下可以用 dmpstore 命令查看所有变量。这是我最常用的调试手段——先看看变量到底有没有写进去,属性对不对。

嗯,关于UEFI变量和NVRAM,今天就聊到这儿。这部分内容虽然基础,但却是整个UEFI引导流程的基石。你想想看,没有变量,Boot Manager怎么知道该启动哪个系统?Secure Boot的密钥存在哪里?所以,把变量搞透,后面的章节会轻松很多。