UEFI Training – UEFI BDS 阶段详细解析

1. 背景与目标

1.1 UEFI与平台初始化(PI)

一个遵循PI规范的引导过程由六个主要阶段组成:安全(SEC)、预EFI初始化(PEI)、驱动执行环境(DXE)、引导设备选择(BDS)、瞬时系统加载(TSL)和运行时(RT)。理解BDS在这一序列中的位置对其功能至关重要。

  • SEC阶段:作为硬件信任根,SEC阶段负责建立一个临时的内存存储(通常是“缓存作为RAM”或CAR),并验证PEI阶段的完整性,为后续阶段奠定安全基础。
  • PEI阶段:此阶段的核心任务是初始化主内存(DRAM)。完成内存初始化后,它通过一系列被称为“HOBs”(Hand-Off Blocks)的数据结构,将系统状态信息,如内存映射和固件卷位置,传递给下一阶段。
  • DXE阶段:这是主要的驱动程序执行环境,系统的大部分初始化工作在此完成。DXE分发器(DXE Dispatcher)根据依赖关系加载驱动程序,这些驱动程序提供了BDS阶段后续将要使用的各种服务和协议,例如文件系统协议(EFI_SIMPLE_FILE_SYSTEM_PROTOCOL)、块I/O协议(EFI_BLOCK_IO_PROTOCOL)以及网络协议栈。
  • BDS阶段:作为本文的焦点,BDS充当固件的策略引擎。它利用在DXE阶段初始化完成的各种服务,根据预设的策略来选择并尝试引导一个目标操作系统或应用程序。
  • TSL与RT阶段:TSL阶段通常由操作系统加载程序执行,它会调用ExitBootServices()来终止预引导环境。之后,系统进入RT(运行时)阶段,此时只有一小部分UEFI运行时服务保留下来,供操作系统使用。

为了清晰地展示各阶段的职责,下表进行了总结。

表1:UEFI引导阶段摘要

阶段
主要职责
环境上下文
关键输出/交接
SEC
建立临时内存,作为信任根,验证PEI
资源受限,通常在闪存中执行,使用CAR
包含临时内存、栈和平台状态的HOB列表
PEI
初始化主内存(DRAM),分发PEIMs
拥有临时内存,但DRAM尚未完全可用
描述完整内存布局和固件卷位置的HOB列表
DXE
分发DXE驱动,初始化大部分硬件,提供引导和运行时服务
DRAM可用,功能丰富的执行环境
一套完整的引导服务和运行时服务,以及大量已注册的协议接口
BDS
执行平台引导策略,连接控制台和引导设备,加载并执行引导选项
继承DXE阶段的所有服务
将控制权移交给操作系统加载程序
TSL
由OS加载程序执行,准备OS环境
引导服务可用,但即将终止
调用ExitBootServices()
RT
操作系统运行,仅保留UEFI运行时服务
引导服务已终止,仅运行时服务可用
为OS提供基础的固件服务(如变量服务、时间服务)

此表提供了一个快速参考,使读者能够立即将BDS阶段置于整个引导流程的背景中,这对于理解其输入和职责至关重要。

1.2 机制与策略的分离

DXE与BDS阶段之间的划分并非随意为之,它体现了UEFI一项根本性的架构原则:机制(Mechanism)与策略(Policy)的分离

这种设计是相对于传统BIOS的单体式结构的一大进步。在传统BIOS中,硬件初始化的逻辑与决定从何处引导的逻辑紧密耦合。UEFI通过将这两者解耦,实现了前所未有的模块化和可扩展性。

具体而言,DXE阶段通过其分发器和驱动程序,负责提供机制。它发现系统中的硬件,并通过标准化的协议接口(如EFI_SIMPLE_FILE_SYSTEM_PROTOCOL、EFI_BLOCK_IO_PROTOCOL等)将其功能暴露出来。DXE本身并不关心应该从哪个硬盘或网络位置启动,它只负责确保“能够”从这些设备启动的工具准备就绪。

与此相对,BDS阶段是策略的体现。它消费DXE提供的机制,并利用一套可配置的规则(主要存储在NVRAM变量中)和平台特定的逻辑来决定引导顺序、连接必要的设备,并最终加载和启动一个引导加载程序。这种分离使得能够通过修改策略(例如,调整BootOrder变量或实现自定义的PlatformBdsLib)来定制引导体验,而无需触及或重新验证核心的固件基础设施(即DXE核心和驱动程序)。这极大地提高了固件的可维护性、稳定性和跨平台的可移植性。

2. 交接与EFI_BDS_ARCH_PROTOCOL

2.1 DXE分发器的角色与BDS的进入条件

DXE分发器的主要工作是循环地从固件卷(Firmware Volumes, FVs)中发现DXE驱动,并根据它们的依赖表达式(Dependency Expressions)以及一个可选的a priori文件来决定执行顺序。

只有当DXE分发器完成了一个完整的调度周期,即所有当前可发现且其依赖项已满足的驱动程序都已被加载和执行后,控制权才会移交给BDS。这个严格的进入条件确保了BDS在开始其工作时,拥有一个稳定且功能最丰富的预引导服务环境,所有可用的设备协议都已准备就绪。

2.2 EFI_BDS_ARCH_PROTOCOL规范

EFI_BDS_ARCH_PROTOCOL是一个架构协议(Architectural Protocol),这是UEFI中一类特殊的协议。与普通协议不同,架构协议由固件的核心(即DXE Foundation)直接消费,而非供其他驱动程序使用。这标志着它在引导流程中的基础性地位。

  • GUID:
    根据PI规范,其全局唯一标识符(GUID)定义如下:
  #**define** EFI_BDS_ARCH_PROTOCOL_GUID \  
    {0x665E3FF6,0x46CC,0x11d4, {0x9A,0x38,0x00,0x90,0x27,0x3F,0xC1,0x4D}}
  • 协议接口结构:
    该协议的结构异常简洁,仅包含一个函数指针成员Entry。
  typedef struct {  
    EFI_BDS_ENTRY Entry;  
  } EFI_BDS_ARCH_PROTOCOL;
  • 生产者:
    此协议必须由一个DXE驱动程序产生。在TianoCore EDK II的参考实现中,这个生产者是BdsDxe模块。

3. 通过NVRAM进行配置

3.1 全局引导变量概述

UEFI引导管理器被定义为一个固件策略引擎,其行为通过一组架构定义的全局非易失性随机访问内存(NVRAM)变量进行配置。这些变量的标准化创建了一个稳定的应用程序编程接口(API),使得操作系统和各种工具(如Linux下的efibootmgr或Windows下的bcdedit)能够与固件的引导行为进行交互和配置。

所有这些全局引导变量共享同一个EFI_GLOBAL_VARIABLE GUID:

{8BE4DF61-93CA-11d2-AA0D-00E098032B8C}

3.2 加载选项顺序变量:BootOrder与DriverOrder

  • BootOrder: 一个UINT16值的有序数组。数组中的每个UINT16值对应一个Boot####变量的编号(####部分)。固件按照此数组中指定的顺序尝试引导这些选项。
  • DriverOrder: 一个与BootOrder类似的UINT16数组,用于指定Driver####变量的加载顺序。这些驱动程序在处理BootOrder之前被加载。与BootOrder在第一次成功后即停止不同,DriverOrder列表中的所有驱动程序都会被尝试加载。
  • SysPrepOrder: 一个类似的列表,用于SysPrep####应用程序。这些应用程序在驱动加载之后、引导选项执行之前运行,旨在完成系统准备工作。

3.3 瞬时引导变量:BootNext与BootCurrent

  • BootNext: 一个单独的UINT16值,指定了仅在下一次启动时首先尝试的引导选项。规范要求固件在处理完此变量后必须将其删除,以防止无限的引导循环。这是操作系统实现“重启到高级选项”等功能的底层机制。
  • BootCurrent: 一个由固件设置的单独UINT16值,用于指示当前引导所使用的引导选项编号。

3.4 EFI_LOAD_OPTION结构

Boot####(以及Driver####等)变量包含了实际的引导选项数据,这些数据被格式化为一个紧凑的、可变长度的EFI_LOAD_OPTION结构。

  • 结构定义:
  typedef struct {  
    UINT32                    Attributes;  
    UINT16                    FilePathListLength;  
    CHAR16                    Description;  
    EFI_DEVICE_PATH_PROTOCOL  FilePathList;  
    UINT8                     OptionalData;  
  } EFI_LOAD_OPTION;
  • Attributes (UINT32): 一个位掩码,定义了该加载选项的属性。
  • FilePathListLength (UINT16): FilePathList成员的总长度(以字节为单位)。
  • Description (CHAR16): 一个以null结尾的UTF-16字符串,用于在引导菜单UI中向用户显示,例如“Windows Boot Manager”。
  • FilePathList (EFI_DEVICE_PATH_PROTOCOL): 一个或多个设备路径的紧凑数组。第一个设备路径指向要加载的UEFI应用程序(例如,引导加载程序的.efi文件)。
  • OptionalData (UINT8): 一个可变长度的二进制缓冲区,当镜像启动时,它会作为参数传递给镜像的入口点。内核命令行参数或其他配置数据就是通过这种方式传递给引导加载程序的。

为了给需要创建或解析Boot####变量的开发者提供一个关键的、一站式的参考,下表详细分解了EFI_LOAD_OPTION结构及其属性。

表2:EFI_LOAD_OPTION结构与属性详解

字段
类型
描述
Attributes
UINT32
定义加载选项属性的位掩码。
 
LOAD_OPTION_ACTIVE (0x01)
如果设置,引导管理器将尝试引导此选项。这允许在不删除条目的情况下禁用它。
 
LOAD_OPTION_FORCE_RECONNECT (0x02)
对于Driver####,强制断开并重新连接所有驱动,使新加载的驱动能覆盖现有驱动。
 
LOAD_OPTION_HIDDEN (0x08)
如果设置,该选项将不会出现在固件的引导菜单UI中。
 
LOAD_OPTION_CATEGORY (0x1F00)
将Boot####选项分组。_BOOT (0x00) 用于正常OS,_APP (0x0100) 用于工具类应用(如Shell。
FilePathListLength
UINT16
FilePathList字段的字节长度。
Description
CHAR16
在UI中显示给用户的、以null结尾的描述字符串。
FilePathList
EFI_DEVICE_PATH_PROTOCOL
指向要加载的UEFI应用程序(.efi文件)的设备路径。
OptionalData
UINT8
传递给已加载镜像的二进制数据,如内核参数。

4. LoadImage()与StartImage()

4.1 EFI_BOOT_SERVICES.LoadImage()

LoadImage()是UEFI中负责将一个可执行镜像从存储介质加载到内存并为其执行做准备的核心服务。

  • 详细操作流程:
  1. 内存分配: LoadImage()首先会根据PE/COFF镜像头中的信息,分配一块类型合适的内存(例如,EfiLoaderCode或EfiBootServicesCode)来存放即将加载的镜像。
  2. 镜像加载: 它通过底层的I/O协议(如EFI_SIMPLE_FILE_SYSTEM_PROTOCOL或EFI_LOAD_FILE_PROTOCOL)从传入的设备路径(DevicePath)读取文件内容到新分配的内存中。
  3. 重定位(Relocation): 由于UEFI镜像是位置无关的,LoadImage()会解析PE/COFF头中的重定位节(relocation section),并对代码和数据中的地址引用进行修正,使其能够在其新分配的内存基地址上正确运行。
  4. 安全验证: 这是与安全引导(Secure Boot)集成的关键节点。如果安全引导处于激活状态,LoadImage()在加载镜像后、返回之前,必须在内部执行签名或哈希验证。它会根据db(允许数据库)和dbx(禁止数据库)中的条目来检查镜像的有效性。如果验证失败,LoadImage()必须返回EFI_SECURITY_VIOLATION状态码。此过程的细节将在第6章中深入探讨。
  5. 协议安装: 如果以上步骤全部成功,LoadImage()会为这个已加载的镜像创建一个新的EFI_HANDLE,并在这个句柄上安装一个EFI_LOADED_IMAGE_PROTOCOL实例。这个协议为镜像自身提供了关于其加载信息的元数据,包括它的内存基地址(ImageBase)、大小(ImageSize)以及传递给它的加载选项(LoadOptions)。

4.2 EFI_BOOT_SERVICES.StartImage()

StartImage()负责将执行控制权转移给一个已经由LoadImage()成功加载到内存中的镜像。

  • 详细操作流程:
  1. 它接收已加载镜像的句柄(ImageHandle)作为输入。
  2. 它通过该句柄找到EFI_LOADED_IMAGE_PROTOCOL,并从PE/COFF头中解析出镜像的入口点地址。
  3. 它调用该入口点,并传递两个标准参数:镜像自身的句柄和指向EFI_SYSTEM_TABLE的指针。
  4. StartImage()会等待被调用的镜像返回。对于一个普通的UEFI应用程序(如UEFI Shell或诊断工具),它在执行完毕后会返回,此时StartImage()也会随之返回。然而,对于一个操作系统加载程序,它通常会调用ExitBootServices()来接管整个系统,因此永远不会返回到StartImage()。

4.3 BdsDxe引导循环的实践

在EDK II的实现中,EfiBootManagerBoot函数封装了LoadImage() -> StartImage()的调用序列 2。

  • 它首先根据EFI_LOAD_OPTION中的设备路径调用LoadImage()。
  • 如果LoadImage()成功返回EFI_SUCCESS,它接着调用StartImage()。
  • StartImage()的行为决定了后续流程。如果它返回EFI_SUCCESS(意味着一个应用程序成功运行并退出了),并且平台配置了引导管理器菜单,那么BDS逻辑可能会停止处理BootOrder列表,转而向用户呈现引导菜单。这种设计允许了“运行一个工具后返回菜单”的用户体验。如果
    StartImage()返回错误,或者LoadImage()本身就失败了(例如,因为文件未找到或安全验证失败),EfiBootManagerBoot就会向上层调用者(如BootBootOptions)报告失败,引导管理器随即会尝试BootOrder中的下一个选项。

5. BDS与UEFI安全引导的交互

5.1 安全引导基础:PK, KEK, db, dbx

安全引导(Secure Boot)是UEFI规范定义的一个特性,它通过公钥基础设施(PKI)来确保在引导过程中只执行经过授权的代码。其核心是存储在NVRAM中的四个数据库:

  • 平台密钥 (PK): 信任链的根。通常由OEM持有,用于控制对KEK数据库的更新。
  • 密钥交换密钥 (KEK): 用于授权对db和dbx数据库的更新。
  • 授权签名数据库 (db): 包含允许执行的镜像的哈希或X.509证书的白名单。
  • 禁止签名数据库 (dbx): 包含已知被泄露或恶意的镜像的哈希或证书的黑名单。

当UEFI变量SetupMode为0且SecureBoot为1时,安全引导功能被激活。一个重要的架构事实是,安全引导并不保护固件的早期阶段(如SEC或PEI)。它的保护范围始于DXE阶段,覆盖所有后续的镜像加载,包括PCI Option ROM、DXE驱动以及由BDS加载的操作系统引导加载程序。

5.2 LoadImage()作为强制执行点

UEFI架构的精妙之处在于,它将安全引导的验证逻辑直接嵌入到LoadImage()这个基础引导服务中,而不是要求每个调用者(如BDS)都去实现一遍安全检查。

这种设计确保了安全策略的强制性和一致性。其工作流程如下:

  1. BDS阶段的核心职责是执行引导策略,它通过调用LoadImage()来尝试加载BootOrder中指定的引导选项。
  2. 当LoadImage()被调用且安全引导处于激活状态时,UEFI规范强制要求它在加载镜像到内存后、返回给调用者之前,必须执行一次安全验证。
  3. 验证过程包括计算待加载镜像的哈希值,并检查其内嵌的PE/COFF签名。然后,固件会将这个哈希值和签名与db和dbx数据库中的条目进行比对。一个镜像被认为是可信的,当且仅当它的哈希或签名证书存在于db中,并且不存在于dbx中。
  4. 如果验证失败,LoadImage()必须返回EFI_SECURITY_VIOLATION状态码。
  5. 因此,BDS的引导循环天然地处理了安全引导失败的情况。一个失败的LoadImage()调用(无论是由于文件未找到还是安全验证失败)都会被BDS引导管理器视为一次普通的加载失败,它会简单地放弃当前选项,然后继续尝试BootOrder中的下一个选项。

这种设计确保了安全执行是不可绕过的(通过标准引导服务),并且将复杂的密码学验证逻辑与高层的引导策略逻辑解耦。

5.3 实践意义与漏洞

这个模型的健壮性完全依赖于一个前提:所有加载可执行代码的组件都必须使用标准的LoadImage()服务。

近期的固件漏洞,如“LogoFAIL”,恰恰利用了对这一前提。这些攻击的共同模式是:一个经过合法签名、能够通过LoadImage()验证的可信UEFI应用程序(例如,一个用于显示启动Logo的驱动或一个第三方诊断工具),其内部却包含了一个自定义的、不安全的PE/COFF加载器。这个自定义加载器随后会从磁盘或网络加载第二个、未经签名的恶意二进制文件,但它在加载时没有调用标准的LoadImage()服务,从而完全绕过了UEFI固件内置的安全引导检查。

这揭示了信任链的脆弱性:只要链条中的任何一个可信环节未能履行其安全职责,整个安全模型就可能被攻破。这也强调了对所有第三方固件驱动和应用程序进行严格代码审查的重要性,以确保它们不会成为特洛伊木马,破坏安全引导提供的保护。

6. 后备与恢复机制

6.1 BootOrder

BDS的主要流程是依次尝试BootNext变量(如果存在)和BootOrder变量中列出的所有活动选项。如果一个选项失败——无论是LoadImage()返回错误(如EFI_NOT_FOUND或EFI_SECURITY_VIOLATION),还是启动的应用程序本身返回了错误状态码——引导管理器都会简单地放弃该选项,并继续尝试列表中的下一个选项。这个过程会一直持续到列表中的所有选项都被尝试过为止。

6.2 可移动介质后备路径

如果所有在NVRAM中定义的引导选项都已尝试并失败,UEFI规范要求引导管理器必须实现一个后备(fallback)机制。这个机制是确保UEFI系统具有基本可恢复性的关键。

  • 处理流程:
  1. 引导管理器必须扫描所有已发现的、支持EFI_SIMPLE_FILE_SYSTEM_PROTOCOL的设备。这包括系统中的所有硬盘分区、USB驱动器、CD/DVD驱动器等。
  2. 在每个发现的文件系统上,它必须查找一个位于标准化、体系结构特定的后备路径下的文件。对于x86-64系统,这个路径是EFIBOOTBOOTX64.EFI。对于IA32系统,则是EFIBOOTBOOTIA32.EFI。
  3. 一旦找到这个文件,引导管理器就会像处理一个普通的Boot####选项一样,尝试使用LoadImage()和StartImage()来加载并执行它。

这个后备机制不仅仅是为了方便,它是UEFI架构的基石,使得在任何符合规范的机器上进行操作系统安装和使用实时/恢复环境成为可能。操作系统安装U盘或恢复光盘正是依赖于固件能够正确实现这一后备发现流程。

然而,在实践中,一些固件的实现存在缺陷。有的固件可能错误地完全忽略NVRAM中定义的变量,总是优先尝试后备路径;而另一些固件则可能无法正确地扫描固定介质(如内置硬盘)上的后备路径,这给多重引导和系统恢复带来了巨大的挑战和不确定性。

6.3 平台与操作系统定义的恢复

除了通用的文件后备路径,UEFI还提供了更结构化的恢复机制。

  • 平台恢复: 平台供应商可以定义一个或多个PlatformRecovery####变量。这些变量指向平台特定的恢复应用程序。当特定条件满足时(例如,OsIndications变量中的EFI_OS_INDICATIONS_START_PLATFORM_RECOVERY位被置位),BDS会优先尝试执行这些恢复选项。这提供了一种比通用文件后备更强大、更有针对性的恢复方式。
  • 操作系统定义的恢复: 类似地,OsRecovery####变量允许操作系统注册自己的恢复加载程序。当OsIndications中的EFI_OS_INDICATIONS_START_OS_RECOVERY位置位时,固件会尝试这些选项,从而启动操作系统的恢复环境(如Windows RE)。

7. 结语

通过对UEFI引导设备选择(BDS)阶段的深入剖析,我们可以清晰地看到,BDS不仅仅是引导流程中的一个简单环节,而是扮演着一个核心协调者的角色。它站在一个承前启后的关键节点上:承接了由SEC、PEI、DXE阶段精心构建的、功能完备的硬件抽象和预引导服务环境;开启了通往具体操作系统加载和执行的路径。

BDS的架构设计精妙地体现了“机制与策略分离”的原则。DXE阶段负责提供“能做什么”的机制,而BDS则根据NVRAM中的变量和平台特定逻辑来决定“该做什么”的策略。这种分离赋予了UEFI固件前所未有的灵活性和可扩展性。从处理BootOrder、DriverOrder等标准化变量,到执行LoadImage和StartImage等核心服务,再到与安全引导的无缝集成以及为平台定制化预留的PlatformBdsLib挂钩,BDS的每一个设计细节都服务于其作为引导策略引擎的最终目标:将一个由硬件和驱动服务构成的集合,转化为一个连贯的、可预测的、安全的、并最终成功启动操作系统的引导序列。

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

THE END
分享
二维码
海报
UEFI Training – UEFI BDS 阶段详细解析
1. 背景与目标 1.1 UEFI与平台初始化(PI) 一个遵循PI规范的引导过程由六个主要阶段组成:安全(SEC)、预EFI初始化(PEI)、驱动执行环境(DXE)、引导设备……
<<上一篇
下一篇>>
文章目录
关闭
目 录