
1. UEFI变量固件与操作系统间的“共享记事本”如果你玩过UEFI BIOS设置改过启动顺序或者开启过虚拟化那你其实已经和UEFI变量打过交道了。简单来说UEFI变量就是一小块存储在非易失性存储器比如主板上的SPI Flash芯片里的数据它充当了固件UEFI和操作系统比如Windows、Linux之间一个持久化的、结构化的通信渠道。你可以把它想象成一个挂在UEFI环境里的“共享记事本”固件和操作系统都能在上面读写信息而且这些信息在断电重启后依然存在。这个机制解决了传统BIOS时代的一个大问题配置信息分散且不统一。以前硬件设置、启动路径、系统密钥等信息可能用各种“土办法”存储互操作性很差。UEFI变量通过标准化的服务GetVariable/SetVariable和命名规范为所有这类需要持久保存的配置数据提供了一个“官方接口”。它的核心价值在于标准化和持久化让固件开发、操作系统引导、安全启动、硬件配置管理等一系列动作有了可靠的数据交换基础。对于开发者尤其是做固件BIOS、操作系统引导器如GRUB2、systemd-boot、或需要与底层硬件深度交互的驱动开发者来说理解UEFI变量是必备技能。它能帮你实现从保存一个简单的启动标志到管理整个平台的安全密钥链等各种功能。接下来我们就深入这个“共享记事本”的内部看看它到底是怎么工作的以及在实际项目中如何正确地使用它。2. UEFI变量的核心架构与工作原理要玩转UEFI变量不能只停留在“读读写写”的层面必须理解其背后的设计逻辑和存储机制。这就像使用数据库了解其索引和存储引擎才能写出高效可靠的代码。2.1 变量服务的“三层架构”UEFI变量并非直接操作Flash芯片它通过一套清晰的分层服务来实现主要分为三层运行时服务层 (Runtime Services)这是操作系统在运行时Runtime可以调用的接口。主要是GetVariable()和SetVariable()这两个核心函数。操作系统或应用程序通过它们来查询或设置变量。这一层负责权限检查、缓存管理和接口暴露。变量驱动层 (Variable Driver)这是UEFI固件内部的一个模块是变量管理的“大脑”。它负责变量存储管理变量在非易失性存储介质如SPI Flash上的物理布局。磨损均衡由于Flash有擦写次数限制变量驱动需要实现算法将写操作均匀分布到不同物理区块延长Flash寿命。掉电保护确保在设置变量的过程中如果发生断电数据不会损坏通常通过“写前日志”或“原子操作”来实现。缓存在内存中缓存频繁访问的变量提升读取性能。非易失性存储层 (Non-Volatile Storage)通常是主板上的SPI Flash芯片的一个特定区域被划分为“变量存储区”。UEFI规范定义了EFI_FIRMWARE_VOLUME_BLOCK_PROTOCOL等协议来抽象对这个区域的操作使得变量驱动可以不关心具体Flash芯片的型号。当操作系统调用GetVariable(L”BootOrder”, gEfiGlobalVariableGuid, …)时这个请求会依次穿过运行时服务层、变量驱动层最终由变量驱动从Flash存储区或缓存中取出数据返回。SetVariable的过程则更复杂涉及写入缓存、日志记录、最终刷写到Flash等步骤。2.2 变量的“身份证”名称与GUID每个UEFI变量都由两个关键部分唯一标识VariableName (变量名)一个以NULL结尾的Unicode字符串。例如L”BootOrder”,L”Lang”,L”PlatformLang”。注意名称是大小写敏感的。VendorGuid (厂商GUID)一个128位的全局唯一标识符。GUID的作用是划分“命名空间”防止不同厂商或模块定义的变量发生名称冲突。例如BootOrder这个所有系统都关心的变量其GUID是gEfiGlobalVariableGuid(8BE4DF61-93CA-11D2-AA0D-00E098032B8C)。而一个硬件厂商自定义的特定配置变量可能会使用自己生成的GUID。正确使用GUID是避免问题的关键。在代码中永远不要硬编码GUID值而应该引用头文件中定义好的宏。2.3 数据的“包装盒”属性与内容除了名字每个变量还有两个重要属性Attributes (属性)一个32位的位掩码定义了变量的特性。最重要的几个属性包括EFI_VARIABLE_NON_VOLATILE非易失性。这是变量的默认且最重要的属性表示变量需要被持久化到Flash中。如果没有这个属性变量只存在于内存中重启后丢失。EFI_VARIABLE_BOOTSERVICE_ACCESS允许在UEFI引导服务和操作系统加载器的阶段访问。EFI_VARIABLE_RUNTIME_ACCESS允许操作系统在运行时即ExitBootServices之后访问。像BootOrder这类需要被OS运行时管理的变量就必须同时具有BOOTSERVICE_ACCESS和RUNTIME_ACCESS属性。EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE或EFI_VARIABLE_AUTHENTICATED_WRITE与安全启动相关表示写入此变量需要附带数字签名。Data (数据)变量的实际内容是一个字节数组VOID*。其格式和含义完全由变量的定义者决定。可以是简单的整型、字符串也可以是复杂的结构体。注意SetVariable时提供的DataSize必须精确匹配你准备写入的数据缓冲区大小。如果你有一个UINT32的值要写DataSize就应该是sizeof(UINT32)即4字节。DataSize0且DataNULL是一种特殊操作用于删除一个已存在的变量。3. 关键变量解析与实战操作指南了解了基本原理我们来看看那些最关键、最常用的变量并手把手演示如何操作它们。3.1 引导管理变量组这是UEFI变量中最核心的一组负责管理操作系统的启动流程。它们通常使用gEfiGlobalVariableGuid。BootOrder (类型: UINT16数组)一个按优先级排列的启动项索引号列表。系统固件会按照这个列表的顺序依次尝试从每个BootXXXX变量描述的设备启动。实操如何读取和修改启动顺序// 示例读取当前BootOrder #include Uefi.h #include Library/UefiRuntimeServicesTableLib.h EFI_STATUS Status; UINTN DataSize 0; UINT16 *BootOrderList NULL; // 第一次调用获取数据大小 Status gRT-GetVariable ( L”BootOrder”, gEfiGlobalVariableGuid, NULL, DataSize, NULL ); if (Status EFI_BUFFER_TOO_SMALL) { BootOrderList AllocatePool(DataSize); if (BootOrderList ! NULL) { // 第二次调用获取实际数据 Status gRT-GetVariable ( L”BootOrder”, gEfiGlobalVariableGuid, NULL, DataSize, BootOrderList ); if (!EFI_ERROR(Status)) { UINTN EntryCount DataSize / sizeof(UINT16); for (UINTN i 0; i EntryCount; i) { Print(L”Boot%04X\n”, BootOrderList[i]); } } FreePool(BootOrderList); } }避坑点GetVariable的标准用法是两段式。第一次传入DataSize0和DataNULL函数会返回EFI_BUFFER_TOO_SMALL并告知你所需的缓冲区大小。你根据这个大小分配内存后再进行第二次调用获取数据。直接猜测大小分配内存极易出错。BootXXXX (类型: EFI_LOAD_OPTION)每个启动项的具体描述。XXXX是一个四位十六进制数对应BootOrder中的索引。其数据结构EFI_LOAD_OPTION包含Attributes: 启动项属性如是否激活。FilePath: 一个EFI_DEVICE_PATH_PROTOCOL列表描述要启动的镜像文件所在位置如\EFI\ubuntu\grubx64.efi。Description: 一个用户可见的描述字符串如“Ubuntu 22.04 LTS”。OptionalData: 可选的、传递给启动镜像的额外数据。BootCurrent (类型: UINT16)当前本次启动所选择的BootXXXX索引号。BootNext (类型: UINT16)指定下一次启动且仅下一次使用的BootXXXX索引号。重启后该变量会被自动删除。这是实现“单次引导到特定系统”功能的关键。例如在双系统电脑上从Windows内设置下次重启进入Linux就是通过写BootNext实现的。3.2 平台与硬件配置变量这类变量通常由固件或硬件驱动创建用于保存平台特定设置。Lang / PlatformLang (类型: 字符串)指定固件界面的显示语言。Lang是平台支持的语言列表PlatformLang是当前选中的语言。格式遵循RFC 4646如”zh-Hans”,”en-US”。ConIn / ConOut / ErrOut (类型: EFI_DEVICE_PATH_PROTOCOL)分别指定标准输入、标准输出、标准错误输出的设备路径。通常指向串口或图形控制台。在无显示器的服务器上通过修改ConOut到串口路径可以实现串口重定向输出对于远程调试至关重要。变量名以Pcd…或gEfi…开头的变量这些通常是UEFI固件内部用于传递动态配置的变量其GUID和数据结构定义在对应的EDK II包中。普通开发者应避免直接操作这些变量除非有明确的文档说明。3.3 安全启动相关变量在启用了UEFI安全启动的系统中以下变量用于管理信任链它们通常具有AUTHENTICATED_WRITE属性写入需要签名。PK (Platform Key)平台密钥是信任链的根。KEK (Key Exchange Key)密钥交换密钥用于更新db和dbx。db (Authorized Signature Database)授权签名数据库存储被允许的镜像签名密钥或证书。dbx (Forbidden Signature Database)禁止签名数据库存储被吊销或禁止的签名。操作这些变量极其危险需要对应的私钥进行签名。普通应用开发几乎不会涉及主要是操作系统厂商如微软、红帽和OEM厂商在进行系统部署或更新时需要处理。4. 在操作系统中操作UEFI变量的实战操作系统提供了用户态接口来访问UEFI变量这比在UEFI Shell或固件内操作更为常见。4.1 Linux下的操作通过sysfs和efivarLinux内核将UEFI变量抽象为sysfs文件系统中的文件路径通常是/sys/firmware/efi/efivars/。每个文件对应一个变量文件名格式为VarName-VendorGuid。直接文件操作不推荐# 查看所有变量 ls -l /sys/firmware/efi/efivars/ # 读取BootOrder变量注意文件前4字节是属性后面才是数据 # 需要使用二进制工具如hexdump hexdump -C /sys/firmware/efi/efivars/BootOrder-8be4df61-93ca-11d2-aa0d-00e098032b8c重要警告直接cat文本文件可能会损坏二进制数据。更不要直接使用echo或文件重定向来写入这极易导致变量损坏或系统无法启动。使用专用工具efivar或efibootmgr推荐efibootmgr是管理启动项的首选工具它封装了对BootOrder、BootXXXX等变量的复杂操作。# 列出所有启动项 sudo efibootmgr -v # 创建一个新的启动项指向ESP分区第一个分区(sda1)上的grub文件 sudo efibootmgr -c -d /dev/sda -p 1 -L “My Linux” -l \\EFI\\ubuntu\\grubx64.efi # 将Boot0003项设为下一次启动项 sudo efibootmgr -n 0003 # 调整启动顺序让0003成为第一项0000成为第二项 sudo efibootmgr -o 0003,0000 # 删除Boot0002项 sudo efibootmgr -b 0002 -B对于非启动变量可以使用efivar命令行工具需要安装efivar包# 列出所有变量 sudo efivar -l # 读取一个特定变量 sudo efivar -p -n 8be4df61-93ca-11d2-aa0d-00e098032b8c-BootOrder编程接口通过libefivar或/dev/efi_vars已废弃 在C程序中可以使用libefivar库来安全地读写变量。这是最规范的方式。#include efivar/efivar.h int main() { efi_guid_t global_guid EFI_GLOBAL_VARIABLE_GUID; uint32_t attributes; uint8_t *data NULL; size_t data_size 0; // 读取变量 int ret efi_get_variable(global_guid, “BootOrder”, data, data_size, attributes); if (ret 0) { perror(“efi_get_variable”); } else { // 处理data... free(data); } // 写入变量需要root权限 uint8_t new_data[] {0x00, 0x00, 0x01, 0x00}; // 示例数据 ret efi_set_variable(global_guid, “MyTestVar”, new_data, sizeof(new_data), attributes, 0644); if (ret 0) { perror(“efi_set_variable”); } return 0; }编译时需要链接-lefivar。4.2 Windows下的操作通过Win32 APIWindows提供了GetFirmwareEnvironmentVariable和SetFirmwareEnvironmentVariable这两个API。#include Windows.h #include stdio.h int main() { DWORD bufferSize 0; UINT16 bootOrder[10]; // 假设最多10个条目 bufferSize sizeof(bootOrder); // 读取BootOrder变量 DWORD result GetFirmwareEnvironmentVariableW( L”BootOrder” L”{8BE4DF61-93CA-11D2-AA0D-00E098032B8C}” // GUID字符串 bootOrder, bufferSize ); if (result 0) { DWORD err GetLastError(); if (err ERROR_INVALID_FUNCTION) { printf(“系统可能处于传统BIOS模式或权限不足。\n”); } } else { // 成功读取处理bootOrder数据 int count result / sizeof(UINT16); for (int i 0; i count; i) { printf(“Boot%04X\n”, bootOrder[i]); } } // 注意在Windows下写入UEFI变量通常需要极高的权限如SYSTEM权限 // 并且可能被安全策略阻止操作需格外谨慎。 return 0; }重要限制在Windows中从Windows 8开始出于安全考虑对UEFI变量的写操作受到了极其严格的限制。普通应用程序甚至管理员权限的程序通常都无法修改BootOrder等关键变量。修改通常需要通过特殊的平台管理工具如bcdedit的部分功能或由固件更新程序在特定上下文中完成。5. 开发中的常见陷阱与调试技巧在实际开发和调试中操作UEFI变量会遇到各种“坑”。以下是我总结的一些常见问题和解决方法。5.1 权限与访问失败问题问题调用GetVariable或SetVariable返回EFI_SECURITY_VIOLATION,EFI_WRITE_PROTECTED或EFI_INVALID_PARAMETER。排查检查属性你是否在正确的时机访问变量一个标记为RUNTIME_ACCESS的变量在UEFI引导服务阶段ExitBootServices之前是无法通过运行时服务访问的。反过来某些只用于固件初始化的变量可能没有RUNTIME_ACCESS属性操作系统启动后自然读不到。检查GUID和名称这是最常见错误。确认GUID的值和字符串格式完全正确变量名的大小写、尾随空格都要仔细核对。建议始终使用头文件中定义好的GUID常量而不是自己手写字符串。检查安全启动状态如果要写入带有AUTHENTICATED_WRITE属性的变量如PK,KEK,db而系统开启了安全启动你必须提供正确的签名数据。否则操作会被拒绝。检查存储空间UEFI变量存储区有固定大小通常由固件决定。如果存储区已满SetVariable会失败。可以尝试删除一些不必要的、非标准的变量来释放空间。5.2 数据损坏与一致性难题问题读取到的变量数据乱码或系统启动行为异常怀疑变量被损坏。预防与解决原子操作意识SetVariable本身设计为原子操作。但对于复合操作如先读BootOrder修改后再写回你需要自己保证原子性。在UEFI环境下这可能意味着需要临时提升中断级别或使用锁。在OS环境下则需要防止多个进程同时修改。备份关键变量在对BootOrder、BootNext等关键变量进行修改前务必先读取并备份其原始值。这样在修改导致系统无法启动时可以通过UEFI Shell或其他救援环境如Linux Live CD将其恢复。# Linux下备份BootOrder sudo efibootmgr ~/efiboot_backup.txt # 或者直接备份二进制文件 sudo cp /sys/firmware/efi/efivars/BootOrder-8be4df61-93ca-11d2-aa0d-00e098032b8c ~/理解变量格式不要假设变量的数据格式。BootXXXX是复杂的EFI_LOAD_OPTION结构包含设备路径。错误地解析或构造这些数据会导致固件无法识别启动项。使用EDK II提供的库函数如DevicePathFromText/DevicePathToText来处理设备路径而不是自己拼接字节。5.3 调试与信息获取技巧使用UEFI Shell进行底层调试 如果你的设备支持进入UEFI Shell这是最强大的调试环境。Shell dmpstore -b # 以二进制格式列出所有变量 Shell dmpstore -guid 8BE4DF61-93CA-11D2-AA0D-00E098032B8C # 列出指定GUID的变量 Shell dmpstore -s BootOrder # 搜索包含”BootOrder”的变量dmpstore命令比操作系统下的工具能看到更原始的状态有助于判断是变量本身损坏还是操作系统驱动读取有问题。查看内核日志 在Linux中使用dmesg | grep -i efi可以查看内核初始化时关于EFI和变量的信息有时能发现变量读取错误或空间不足的警告。利用固件设置界面 很多主板的UEFI设置界面有“保存自定义设置”和“加载默认设置”选项。这本质上就是操作一组预定义的变量。当你怀疑变量混乱时尝试“加载优化默认值”Load Optimized Defaults可以重置大部分非用户变量到出厂状态但注意这会清除你所有的BIOS设置。5.4 一个真实案例修复因BootOrder损坏导致无法启动我曾遇到一台服务器在异常断电后无法启动固件报错“No bootable device”。进入UEFI Shell后用dmpstore查看发现BootOrder变量存在但数据长度异常不是UINT16的整数倍。推测是断电时写操作被中断导致变量数据不完整。修复步骤在UEFI Shell下用dmpstore -guid 8BE4DF61-93CA-11D2-AA0D-00E098032B8C列出所有全局变量。发现Boot0000,Boot0001等启动项变量都完好里面有正确的硬盘设备路径。由于BootOrder损坏我决定删除并重建它。Shell setvar BootOrder -guid 8BE4DF61-93CA-11D2-AA0D-00E098032B8C -bs -rt -nv -d-d参数表示删除创建一个新的BootOrder变量只包含一个有效的启动项索引例如Boot0000。Shell setvar BootOrder -guid 8BE4DF61-93CA-11D2-AA0D-00E098032B8C -bs -rt -nv -v 0000注意-v参数后的0000是十六进制值实际写入的是两个字节0x00, 0x00重启后系统成功从Boot0000对应的硬盘启动。这个案例的关键教训是关键变量如BootOrder的损坏虽然棘手但只要你能访问UEFI Shell并且知道其他启动项变量BootXXXX是完好的就可以通过手动重建依赖关系来恢复。平时备份这些变量的内容至少是efibootmgr -v的输出非常重要。