UEFI Training – UEFI DXE 阶段详细解析

1. DXE在UEFI PI架构中的角色

DXE(Driver Execution Environment, 驱动程序执行环境)阶段是UEFI平台初始化(Platform Initialization, PI)架构中的核心环节。它承接了早期硬件初始化的成果,并构建了一个功能完备、可扩展的驱动模型,为最终加载操作系统铺平了道路。本部分将深入探讨DXE阶段的架构基础,定义其在启动流程中的核心作用,并分析其与前序和后继阶段的衔接关系。

1.1. 从内存初始化到操作系统启动

根据UEFI PI规范,DXE阶段是执行绝大多数系统初始化的主要场所。它在PEI阶段成功地在平台中建立了永久性内存(如DRAM)之后启动 。

DXE阶段的核心任务是加载并执行一系列驱动程序,这些驱动程序负责初始化处理器、芯片组以及各种平台组件,并为这些硬件提供软件抽象层。平台启动过程中绝大部分复杂的配置和初始化工作都在此完成,这与PEI阶段的设计理念形成对比——PEI被有意设计为轻量级的,仅完成最基础的任务,而将复杂的算法和处理流程推迟到DXE阶段执行 。

DXE阶段的最终目标是引导至启动设备选择(Boot Device Selection, BDS)阶段。BDS阶段负责实现平台的启动策略,最终加载一个操作系统。只有当操作系统成功启动后,DXE阶段的使命才算完成并正式终止。

1.2. Foundation、Dispatcher和Drivers

DXE环境由三个紧密协作的核心组件构成:

  1. DXE Foundation: 这是DXE阶段的内核,是一个核心的可执行镜像。它负责生成最基础的UEFI启动服务(Boot Services)、运行时服务(Runtime Services)和DXE服务(DXE Services)。其设计目标是实现高度的可移植性,通过一套架构协议(Architectural Protocols)与具体硬件解耦。
  2. DXE Dispatcher(DXE分发器): 作为DXE Foundation内部的关键组件,分发器负责发现、验证并根据依赖关系以正确的顺序执行DXE驱动程序。
  3. DXE Drivers(DXE驱动程序): 这些是遵循PE/COFF格式的模块化可执行文件。它们执行特定的硬件初始化任务,或为硬件和软件组件提供服务抽象(即协议)。

1.3. 从SEC到PEI到DXE再到BDS

UEFI的启动流程是一个精心设计的、分阶段的过程,DXE位于其中承上启下的关键位置:

  • SEC (Security) 阶段:启动流程的起点。该阶段执行最基础、最少量的处理器初始化,建立信任根(Root of Trust),然后将控制权移交给PEI阶段。
  • PEI (Pre-EFI Initialization) 阶段: 该阶段的核心任务是初始化永久性内存(DRAM)。完成内存初始化后,它通过一种名为HOBs(Hand-Off Blocks)的数据结构,将发现的关键系统状态信息传递给DXE阶段。
  • DXE (Driver Execution Environment) 阶段: 如前所述,此阶段利用PEI准备好的内存环境,执行平台的主要初始化工作。
  • BDS (Boot Device Selection) 阶段: 在架构上,BDS可以看作是DXE的一部分,但在逻辑上是独立的。当DXE分发器加载完所有可用的驱动程序后,控制权便移交给BDS。BDS负责实现平台的启动策略,例如解析启动变量、初始化控制台设备(键盘、显示器)并尝试从指定的设备(如硬盘、U盘)加载操作系统。BDS本身也是通过一个架构协议来实现的。

这种分阶段的启动架构并非偶然,而是为了解决系统引导过程中的一个经典“鸡生蛋还是蛋生鸡”的难题:你需要内存来运行初始化硬件的代码,但你又必须先初始化内存控制器这个硬件才能使用内存。为了打破这个循环,PI架构设计了PEI阶段。PEI在一个临时的、资源极其有限的环境(如处理器的缓存作为RAM,即Cache-as-RAM)中运行,其唯一的核心目标就是完成DRAM的初始化。一旦这个最脆弱、最依赖具体硬件的引导部分在PEI中完成,它就为后续更大、更复杂、更具可移植性的DXE阶段创造了一个稳定和标准化的执行基础。

2. 从PEI到DXE

从PEI到DXE的切换是固件启动流程中的一个关键转折点,它标志着系统从一个资源受限的临时环境过渡到一个功能完备的执行环境。这个过程依赖于精确的接口和数据结构。

2.1. DXE初始程序加载(IPL)PPI

这个切换过程并非简单的代码跳转。PEI Foundation的最后一项任务,是在所有已安装的PEIM到PEIM接口(PEIM-to-PEIM Interfaces, PPIs)中,通过GUID进行扫描,以定位一个特殊的PPI,即EFI_DXE_IPL_PPI(DXE Initial Program Load PPI)。

一旦找到,PEI Foundation便将控制权移交给实现了该PPI的PEIM。这个DXE IPL PEIM随后承担起加载DXE Foundation镜像到内存并执行它的职责,从而正式开启DXE阶段。

2.2. 切换块(HOB)列表

在PEI阶段发现的全部系统状态,都是通过HOBs的列表传递给DXE阶段的。HOB是PI架构中用于阶段间数据传递的唯一机制。

HOB列表的设计具有以下关键特性:

  • 位置无关性: HOBs是位置无关的数据结构,可以被轻松地重定位到内存的任何位置。
  • 单向数据流: HOB列表在“HOB生产者阶段”(即PEI)被创建和填充,而在HOB消费者阶段(即DXE)被视为只读。这一设计强制实现了一种清晰的、单向的数据流,DXE Foundation在架构上被禁止回调任何PEI服务,从而实现了两个阶段的彻底解耦。
  • 标准化结构: 每个HOB都以一个通用的头部结构EFI_HOB_GENERIC_HEADER开始,该头部包含了HOB的类型(Type)和长度(Length)。这使得DXE的HOB解析器能够遍历整个列表,并安全地跳过它不认识的HOB类型。此外,所有HOB都必须是8字节对齐的。

这种设计思想的深远影响在于,它将UEFI的核心执行环境(DXE)与早期的、高度依赖特定硬件的初始化过程(PEI)分离开来。PI规范明确指出,DXE阶段的执行并不强制要求一个PEI阶段,它唯一的要求是一个有效的HOB列表。这一点是UEFI能够超越传统PC-BIOS生态系统,实现跨平台可移植性的架构基石。任何能够初始化内存并构建一个符合规范的HOB列表的引导加载程序(bootloader),如coreboot或U-Boot,都可以作为HOB生产者。因此,HOB列表不仅仅是一个数据结构,它更是一个定义了早期硬件初始化与标准化DXE环境之间边界的API。

在成功接收来自PEI阶段的系统状态后,DXE阶段便进入其核心任务:通过一个强大的驱动模型来初始化整个平台。这一部分将解构DXE Foundation的内部工作原理,深入分析DXE分发器如何编排驱动的执行,并探讨实现这一切的通信与扩展机制。

3. DXE Foundation

DXE Foundation是DXE阶段的引擎,它不仅提供服务,还负责引导整个驱动生态系统的建立。

3.1. 生成UEFI启动、运行时和DXE服务

DXE Foundation本身是一个启动服务镜像,其首要职责是创建并初始化EFI_SYSTEM_TABLE(系统表)。这个系统表是所有后续DXE驱动和UEFI应用程序访问固件服务的唯一入口点。

系统表内包含了指向以下关键服务表的指针:

  • EFI_BOOT_SERVICES_TABLE:提供在操作系统启动之前可用的服务,如内存管理、事件处理、协议和句柄操作等。
  • EFI_RUNTIME_SERVICES_TABLE:提供在操作系统运行后仍然可用的服务,如变量服务、时间服务和系统重置。
  • EFI_CONFIGURATION_TABLE:一个可扩展的表,包含指向其他重要系统配置表的GUID/指针,例如DXE服务表、HOB列表、ACPI表和SMBIOS表等。

一个关键的实现细节是,许多服务在DXE Foundation初始化之初并非完全可用。它们的功能依赖于一系列DXE架构协议(DXE Architectural Protocols),例如定时器(Timer)、CPU和看门狗(Watchdog)协议,而这些协议需要由早期的DXE驱动来产生和安装。因此,DXE Foundation首先会安装这些服务的占位符版本,这些占位符函数在被调用时会返回EFI_NOT_AVAILABLE_YET。随后,当所需的架构协议被安装时,DXE Foundation会通过事件通知机制,用完整功能的实现来更新这些服务表。

3.2. 基础组件:HOB解析器、PE/COFF加载器和架构协议

DXE Foundation是一个复杂的软件模块,其内部集成了多个协同工作的组件:

  • HOB解析器 (HOB Parser): 负责处理从PEI阶段传递过来的HOB列表。它解析资源描述符HOB来初始化全局协调域(Global Coherency Domain, GCD)中的内存和I/O空间映射,并解析内存分配HOB来构建初始的UEFI内存映射(Memory Map)。
  • PE/COFF加载器 (PE/COFF Loader): 实现了LoadImage()启动服务,能够将PE32+格式的DXE驱动和UEFI应用程序从固件卷加载到内存中,为执行做准备。
  • 解压缩协议 (Decompress Protocol): 由于固件存储空间有限,DXE驱动通常以压缩的形式存储在FFS(Firmware File System)文件中。DXE Foundation必须包含一个EFI_DECOMPRESS_PROTOCOL的实例,以便在加载驱动时对其进行解压。
  • 固件卷驱动 (Firmware Volume Driver): 为HOB列表中描述的每个固件卷(FV)生成并安装EFI_FIRMWARE_VOLUME2_PROTOCOL。这个协议使得DXE分发器能够在FV中搜索驱动文件。
  • 事件和服务存根 (Event and Service Stubs): 在早期初始化事件通知系统和服务表。特别是,它会为每个DXE架构协议创建一个通知事件。当某个架构协议被安装时,相应的事件被触发,DXE Foundation的回调函数就会执行,以完成依赖该协议的各项服务的最终初始化。

3.3. DXE Foundation的初始化序列

DXE Foundation的入口点DxeCore()接收指向HOB列表的指针HobStart作为其唯一参数。在将控制权交给DXE分发器之前,它必须完成一系列严格的初始化步骤:

  1. 解析HOBs: 使用HOB解析器处理HOB列表,初始化GCD和UEFI内存映射。
  2. 分配和初始化服务表: 从EFI_BOOT_SERVICES_MEMORY类型的内存中分配EFI_SYSTEM_TABLE、EFI_BOOT_SERVICES_TABLE和EFI_DXE_SERVICES_TABLE,并从EFI_RUNTIME_SERVICES_MEMORY类型的内存中分配EFI_RUNTIME_SERVICES_TABLE。用占位符或初始实现填充这些表。
  3. 重定位HOB列表: 如果HOB列表的当前位置不合适(例如,位于即将被用于其他目的的内存区域),则将其重定位到更安全的位置。
  4. 配置系统表: 将DXE服务表和HOB列表的指针添加到EFI_CONFIGURATION_TABLE中。
  5. 创建通知事件: 为所有必需的DXE架构协议注册通知事件。
  6. 初始化核心协议: 初始化并安装解压缩协议和固件卷协议,为分发器的工作做好准备。

只有在上述所有步骤完成后,系统才具备了加载和执行外部驱动程序的能力。此时,DXE Foundation才会调用DXE分发器,正式开始平台的驱动加载和初始化过程。

4. DXE分发器

DXE分发器(DXE Dispatcher)是DXE Foundation的核心调度引擎,它负责以一种有序且可扩展的方式,管理成百上千个DXE驱动的生命周期。

4.1. 从发现到初始化

分发器通过一个定义明确的状态机来跟踪和管理每个DXE驱动的生命周期。

整个分发过程是一个循环。分发器首先尝试执行“已调度队列”(mScheduledQueue)中的所有驱动。当该队列为空后,它会重新评估所有处于“依赖中”状态的驱动,检查它们的依赖项是否已经满足。如果满足,这些驱动就会被移入“已调度队列”。这个循环不断重复,直到没有任何驱动可以再被调度为止。此时,DXE阶段的主要驱动加载工作完成,控制权将移交给BDS阶段。

4.2. a priori文件

分发器通过遍历固件卷(Firmware Volumes, FVs)来发现驱动程序。FVs是固件镜像中的逻辑存储区域。在启动初期,FVs的位置由HOB列表描述;之后,也可能由其他DXE驱动(例如,一个SATA或NVMe驱动)动态地引入新的FVs。

每当发现一个新的FV时,分发器会首先使用安全架构协议(Security Architectural Protocol)对其进行认证,以确保其来源可信。随后,它会在该FV中搜索一个特殊的文件。

这个特殊文件被称为a priori文件。它是一个可选文件,拥有一个固定的文件GUID,其内容是一个按顺序排列的驱动文件GUID列表。

FDF文件中,a priori示例:

[FV.FvMain]
...
APRIORI {
    FILE DRIVER = edf6e409-f620-4174-8a60-b6c867a1112e {
        SECTION PEI_DEPEX = ...
        SECTION PE32 = SomeVeryImportantDriver.efi
    }
    FILE DRIVER = 4c3f5502-385a-4b5c-9671-e5c42a5342a3 {
        SECTION PEI_DEPEX = ...
        SECTION PE32 = AnotherCriticalDriver.efi
    }
}

...
# 常规驱动列表
INF MdeModulePkg/Universal/PcatPciRootBridge/PcatPciRootBridge.inf
INF MdeModulePkg/Bus/Pci/PciBus/PciBus.inf

a priori文件的作用是强制指定一个确定性的、强有序的执行序列。当分发器发现一个FV中存在a priori文件时,它会忽略这些驱动自身的依赖表达式,直接按照文件中的列表顺序,将它们放入“已调度队列”并立即执行。

这种机制至关重要。它解决了纯依赖驱动模型的一个引导性难题:如何加载那些提供基础服务(协议)的、被其他所有驱动所依赖的最早期的驱动?例如,安全架构协议或核心芯片组驱动必须在其他驱动之前运行。通过a priori文件,平台设计者可以确保这些基础驱动程序以可预测的顺序优先加载。

4.3. 依赖表达式(Depex)

对于所有未在a priori文件中列出的驱动,它们的执行顺序由其自身的依赖表达式(Dependency Expression, Depex)决定 。Depex是一种弱有序的调度机制。

Depex的本质是一段为简单的、基于堆栈的、使用后缀表示法(逆波兰表示法)的虚拟机编写的字节码。这段代码被存储在驱动FFS文件的一个特殊section中。分发器在评估一个驱动时,会解释并执行其Depex字节码。

Depex的语法和操作码如下表所示。最常见的操作是PUSH,它会检查指定的协议当前是否已经安装在句柄数据库中,并将一个布尔值(TRUE或FALSE)压入评估栈。通过AND、OR、NOT等逻辑操作,一个驱动可以精确地描述其执行所需的所有前置条件。

只有当一个驱动的Depex表达式最终评估结果为TRUE时,该驱动才会从依赖中状态转移到已调度状态。如果一个驱动没有Depex section,系统会默认其依赖为TRUE,并尽快调度它。相反,如果一个驱动的Depex被硬编码为FALSE,分发器将永远不会自动加载它,这类驱动通常需要通过其他方式(如代码中直接调用LoadImage/StartImage)来加载。开发者在模块的INF文件中的[Depex]节来定义这些表达式。

表2:Depex操作码参考

操作码
操作数
堆栈机操作
描述
BEFORE
GUID
声明此驱动必须在具有指定文件GUID的驱动之前执行。
AFTER
GUID
声明此驱动必须在具有指定文件GUID的驱动之后执行。
PUSH
GUID
将一个布尔值压入栈
检查指定的协议GUID是否存在于句柄数据库中。如果存在,压入TRUE;否则压入FALSE。
AND
从栈顶弹出两个布尔值,执行逻辑与操作,并将结果压回栈
用于组合多个依赖条件,表示所有条件都必须满足。
OR
从栈顶弹出两个布尔值,执行逻辑或操作,并将结果压回栈
用于组合多个依赖条件,表示满足任一条件即可。
NOT
从栈顶弹出一个布尔值,执行逻辑非操作,并将结果压回栈
用于表示某个协议必须不存在。
TRUE
将TRUE压入栈
声明一个无条件的TRUE依赖,使驱动尽快被调度。
FALSE
将FALSE压入栈
声明一个无条件的FALSE依赖,阻止分发器自动加载此驱动。
END
弹出栈顶的最终结果并结束评估
标记Depex表达式的结束。
SOR
(Schedule on Request) 标记该驱动为按需调度,分发器不会自动加载,需通过Schedule()服务手动触发。

a priori和Depex这两种调度模式的结合,体现了UEFI架构设计的精妙之处。它既解决了核心平台服务必须按可预测顺序初始化的引导问题,又为广大的外围设备和功能驱动提供了一个灵活、模块化和可扩展的生态系统。a priori文件为系统核心的稳定性提供了保障,如同传统BIOS中的POST表,但范围更小、目的更明确。一旦这些基础服务就位,基于Depex的分发器便接管控制,以一种近乎事件驱动的方式,动态地构建起整个平台的驱动和服务层级。这种混合模型兼具了固定启动序列的稳定性和依赖驱动模型的灵活性,是UEFI架构成功的关键之一。

5. 通信与扩展性构造

DXE阶段的强大之处不仅在于其驱动加载能力,更在于它提供了一套优雅的机制,使得成百上千个独立的驱动模块能够相互通信、协同工作,共同构建起一个完整的平台。这个机制的核心是协议(Protocols)和句柄数据库(Handle Database)。

5.1. 协议:GUID定义的UEFI接口

协议(Protocol)是UEFI中服务通信的基本单元。从概念上讲,一个协议就是一个由唯一的GUID(全局唯一标识符)标识的接口定义。它通常表现为一个C语言的结构体,其中包含了一组函数指针和相关的实例数据。

协议在UEFI中的作用类似于面向对象编程中的接口(Interface),但其实现更为轻量和高效。这种抽象机制允许一个驱动(消费者)使用由另一个驱动(生产者)提供的服务,而无需在编译时进行链接,也无需了解生产者内部的实现细节。

协议的范围非常广泛,从底层的硬件抽象(如EFI_BLOCK_IO_PROTOCOL,用于读写块设备)到高层的软件服务(如EFI_DECOMPRESS_PROTOCOL,用于数据解压)。

5.2. 句柄数据库

句柄数据库(Handle Database)是所有协议实例的中央注册表。句柄(Handle)是一个不透明的指针(VOID*),它代表了系统中的一个组件。这个组件可以是一个物理设备(如一个PCI网卡)、一个已加载的驱动镜像,或是一个逻辑服务。

一个句柄上可以安装多个协议,用以表示该组件所具备的多种能力。例如,一个代表SATA控制器的句柄上,可能同时安装了EFI_DEVICE_PATH_PROTOCOL(描述其在系统中的位置)和EFI_BLOCK_IO_PROTOCOL(提供块读写能力)。

句柄的创建和销毁是隐式的,由协议的安装和卸载过程自动管理。当一个驱动首次在一个NULL句柄上调用InstallProtocolInterface()或InstallMultipleProtocolInterfaces()时,系统会自动创建一个新的句柄,并将其添加到数据库中。反之,当最后一个协议从一个句柄上被卸载时,该句柄会自动从数据库中移除并销毁。这意味着句柄数据库中不可能存在没有任何协议的“空”句柄。

UEFI启动服务提供了一套丰富的函数来管理这个数据库,包括InstallProtocolInterface()、UninstallProtocolInterface()、LocateHandleBuffer()、LocateProtocol()、OpenProtocol()和CloseProtocol()等。

5.3. UEFI驱动绑定模型:连接驱动与设备

UEFI驱动绑定模型(Driver Binding Model)是UEFI中一套形式化的、用于将驱动程序连接到它们所能管理的设备上的规范。一个遵循该模型的UEFI驱动,必须实现EFI_DRIVER_BINDING_PROTOCOL。

该协议包含三个核心函数:

  1. Supported(): 一个轻量级函数,用于快速检查该驱动是否支持管理给定的设备控制器。
  2. Start(): 如果Supported()返回TRUE,系统便会调用此函数。该函数负责将驱动绑定到控制器上,进行必要的硬件初始化,并在该控制器的句柄上生产出相应的I/O协议。
  3. Stop(): 解除驱动与控制器的绑定,使硬件进入静默状态,并卸载之前安装的I/O协议。

ConnectController()这个启动服务负责驱动整个绑定过程。它会遍历系统中的所有设备和驱动,尝试将它们进行匹配和连接。

将句柄数据库、协议和驱动模型联系起来看,可以得出一个更深层次的理解:句柄/协议模型不仅仅是一种通信机制,它实际上是在内存中构建了一个动态的、描述整个平台当前状态和能力的图(Graph)。在这个图中,句柄是节点(Node),协议是节点上的属性(Property)或节点间连接的边(Edge)。

整个DXE和BDS阶段的执行过程,可以看作是两个引擎对这个图进行操作的过程。DXE分发器是图的构建引擎:当一个驱动的依赖(某个协议GUID)在图中出现时(即LocateProtocol成功),分发器就调度这个驱动。驱动执行后,通常会产生新的协议并安装到某个句柄上(InstallProtocolInterface),这相当于在图中添加了新的节点或边。这个过程不断迭代,使得图的规模和复杂性不断增长。

随后,BDS阶段作为图的遍历和使用引擎:它会查询这个图,寻找具备特定属性的节点。例如,它会使用LocateHandleBuffer()和EFI_SIMPLE_FILE_SYSTEM_PROTOCOL来查找所有代表可启动文件系统的节点,然后根据启动变量的配置,尝试从这些节点加载操作系统。

因此,将句柄数据库理解为一个动态图,为我们提供了一个强大的心智模型,用以理解UEFI的模块化和可扩展性的本质。系统的启动不是一个线性的脚本执行过程,而是一个动态的、自组织的系统状态图的构建过程。

6. 协议的生产与消费

6.1. 生产一个自定义协议

以下步骤展示了如何创建一个自定义协议。

  1. 定义协议: 在一个头文件(例如MyProtocol.h)中,首先使用#define和extern EFI_GUID来声明协议的GUID。然后,定义协议的接口结构体,该结构体包含一系列函数指针,每个指针代表协议提供的一项服务。
// MyProtocol.h  
#define MY_SAMPLE_PROTOCOL_GUID   
0x123456780xabcd0xefgh, { 0x110x220x330x440x550x660x770x88 } }

typedef struct _MY_SAMPLE_PROTOCOL MY_SAMPLE_PROTOCOL;
typedef EFI_STATUS (EFIAPI *MY_PROTOCOL_DO_SOMETHING)(  
     IN MY_SAMPLE_PROTOCOL *This,  
     IN UINTN              Data  
);
struct _MY_SAMPLE_PROTOCOL {  
     MY_PROTOCOL_DO_SOMETHING DoSomething;  
};
extern EFI_GUID gMySampleProtocolGuid;
  1. 实现协议: 在驱动的.c文件中,实现协议接口中定义的函数。然后,创建一个该协议结构体的全局实例,并将其函数指针初始化为刚刚实现的函数。
  2. 安装协议: 在驱动的入口点函数中,调用启动服务gBS->InstallMultipleProtocolInterfaces()来发布(即安装)这个协议。这个调用会将你的协议实例(通过指针)和其GUID关联到一个句柄上。你可以提供一个NULL句柄来让系统创建一个新的句柄,也可以在一个已有的句柄上安装协议。
  3. 在INF中声明: 在该驱动的INF文件中,在[Protocols]节下列出协议的GUID,并使用## PRODUCES标记来表明此驱动生产该协议 33。同时,为了让其他模块能够引用这个GUID,需要将GUID的定义添加到一个包级别的.dec文件中。

6.2. 从另一个模块消费协议

消费一个协议的过程与生产过程相对应。

  1. 引用协议: 消费模块必须包含定义了协议结构和GUID的头文件。在其INF文件的[Protocols]节中,需要列出该协议的GUID,并使用## CONSUMES标记。
  2. 添加依赖: 这是至关重要的一步。消费模块的INF文件必须在其[Depex]节中包含该协议的GUID。这会告诉DXE分发器,在生产者驱动成功安装该协议之前,不要加载和执行这个消费者驱动。
  3. 定位并使用协议: 在消费者驱动的C代码中,使用启动服务gBS->LocateProtocol(),并提供协议的GUID作为参数,来获取一个指向已安装协议实例的指针。一旦成功获取指针,消费者就可以通过该指针调用协议结构体中的函数,从而使用生产者提供的服务。

6.3. 使用启动服务进行高级协议管理

除了基本的生产和消费,UEFI还提供了更高级的协议管理机制:

  • OpenProtocol() / CloseProtocol(): 这对函数用于管理对一个协议的访问。当多个驱动需要访问同一个协议时,OpenProtocol会记录下访问者,并可以根据不同的打开类型(如BY_DRIVER, BY_CHILD_CONTROLLER)来实施访问控制。
  • RegisterProtocolNotify(): 这是一种更为动态和事件驱动的协议消费方式。驱动可以使用此函数注册一个回调事件,该事件会在指定的协议被安装时触发。与静态的Depex依赖相比,这种方式更加灵活,适用于热插拔等动态场景。

7. 系统管理模式(SMM)与DXE的交互

系统管理模式(System Management Mode, SMM)是x86架构中一个比操作系统(Ring 0)权限更高的执行环境(常被称为Ring -2)。DXE阶段负责加载和初始化SMM环境,并提供与非SMM世界通信的桥梁。

 

x86架构中不同特权层级的权限高低关系(由高到低排列): SMM (Ring -2) → Hypervisor (Ring -1) → OS Kernel (Ring 0) → Driver/Service (Ring 1-2) → Applications (Ring 3)

7.1. DXE SMM驱动的生命周期与约束

DXE SMM驱动是一种特殊的驱动类型(MODULE_TYPE = DXE_SMM_DRIVER)。它由DXE分发器加载,但其代码和数据最终被放置在受硬件保护的SMRAM(System Management RAM)中执行。

其生命周期被明确地划分为两个阶段,每个阶段都有截然不同的约束:

  1. 初始化阶段(Initialization): 从驱动入口点被调用开始,到入口点函数返回结束。在此阶段,SMM驱动非常特殊,它既可以访问SMRAM内的SMM服务(通过gSmst,即SMM System Table),也可以访问SMRAM外的UEFI启动服务(通过gBS)。
  2. 运行时阶段(Runtime): 从入口点函数返回之后。在此阶段,SMM驱动只能通过系统管理中断(SMI)被激活。此时,它与外部世界的联系被切断,无法再访问任何UEFI启动服务或运行时服务。所有在初始化阶段从外部获取的协议接口或事件句柄都已失效,严禁使用。SMM驱动的全部操作都必须依赖gSmst提供的服务。

7.2. SMM的通信

非SMM的DXE驱动与SMM驱动之间的标准通信机制是EFI_MM_COMMUNICATION_PROTOCOL(在PEI阶段则为对应的EFI_PEI_SMM_COMMUNICATION_PPI)。

通信流程如下:

  1. 非SMM世界的驱动(调用者)在常规内存中分配一个通信缓冲区(Communication Buffer)。
  2. 调用者将命令和数据填充到这个缓冲区中。
  3. 调用者调用EFI_MM_COMMUNICATION_PROTOCOL的Communicate()函数,并将指向通信缓冲区的指针作为参数传入。
  4. 这个调用会触发一个软件SMI,使系统进入SMM。
  5. SMM内部的SMI分发器根据通信缓冲区中的GUID或其他标识,将请求路由到已注册的、对应的SMM驱动(SMI处理器)。
  6. SMI处理器执行请求的操作,并可以将结果写回到通信缓冲区中,从而实现双向通信。

这种代理通信机制被广泛应用于将一些敏感操作(如UEFI变量的非易失性存储)放到SMM中执行,以增强安全性并避免与操作系统产生资源访问的竞争条件。

7.3. DXE-SMM边界的安全意义

SMM边界是x86平台上权限最高的安全边界。任何允许非SMM代码控制SMM内部执行流的漏洞,都意味着整个系统的彻底沦陷。

一个非常普遍且危险的漏洞类别,就是SMM处理器未能严格校验从通信缓冲区传入的指针。如果一个SMI处理器没有检查通信缓冲区内的指针是否指向SMRAM外部,那么在Ring 0运行的攻击者就可以精心构造一个包含恶意指针的通信缓冲区,然后触发SMI。SMM处理器在毫无防备的情况下使用这个恶意指针,就会导致对SMRAM的任意读写,最终升级为SMM内的任意代码执行。

因此,保护DXE-SMM边界的安全,要求对所有从非SMM世界传入SMM的数据进行严格的、无一例外的输入验证,尤其是对指针和大小等参数的合法性检查。

8. UEFI安全启动与DXE驱动验证

UEFI安全启动(Secure Boot)是旨在抵御恶意软件在引导阶段执行的核心安全特性。它的执行和策略实施,与DXE阶段紧密相关。

8.1. 信任链:PK、KEK、db和dbx

UEFI安全启动的核心是建立在公钥基础设施(PKI)上的一条信任链。该信任链的策略由存储在非易失性UEFI变量中的一组密钥和数据库定义:

  • 平台密钥(Platform Key, PK): 信任链的根。通常由平台制造商(OEM)持有私钥。PK用于授权对KEK的修改。
  • 密钥交换密钥(Key Exchange Key, KEK): 在平台和操作系统供应商(如微软)之间建立信任。KEK的私钥用于授权对签名数据库(db和dbx)的更新。
  • 签名数据库(Signature Database, db): 一个授权列表(Whitelist),包含了允许执行的程序(如UEFI驱动、OS加载程序)的哈希值或签名证书。
  • 禁止签名数据库(Forbidden Signature Database, dbx): 一个撤销列表(Blacklist),包含了已知恶意的或已泄露密钥签名的程序的哈希值或证书。

8.2. DXE驱动和Option ROM的签名验证过程

安全启动的强制执行始于DXE阶段,而非更早的SEC或PEI阶段。当DXE分发器尝试加载一个DXE驱动,或者当BDS阶段尝试加载一个UEFI应用程序或来自PCI设备的Option ROM时,固件中的PE/COFF加载器会执行签名验证。

验证过程如下:

  1. 提取可执行镜像中的数字签名。
  2. 根据db和dbx数据库进行检查。
  3. 一个镜像要被允许执行,其签名(或其签名证书的签名)必须存在于db中,且绝不能存在于dbx中。
  4. 如果验证失败,该镜像将被拒绝加载和执行。这是抵御固件级恶意软件(Bootkit)和未经授权的固件组件的关键防线。

8.3. 微软UEFI CA的实践签名要求

要在开启了安全启动的零售Windows设备上执行第三方驱动,该驱动必须经过“Microsoft 3rd Party UEFI CA”的签名。微软为此设定了严格的审核要求:

  • 代码要求: 提交的二进制文件必须是NX兼容的,即任何内存节(section)不能同时具备可写和可执行属性。此外,节对齐必须是4KB。
  • 漏洞检查: 代码中不能包含已知的安全漏洞,例如使用了存在漏洞的旧版本OpenSSL库。
  • 许可证合规: 代码不能使用GPLv3等可能被解释为要求公开签名密钥的“传染性”开源许可证。
  • Shim加载程序: 扮演“垫片”(Shim)角色的引导加载程序需要经过一个独立的、更为严格的审查流程。

安全启动的执行为何始于DXE阶段,这是一个基于架构现实的务实选择。尽管这在SEC和PEI阶段留下了一个“早期启动盲点”,但这是不可避免的。因为执行安全启动策略所需的完整基础设施——包括访问非易失性变量(用于读取db/dbx)的服务和进行密码学计算(哈希、验签)的协议——本身就是由DXE驱动提供的。你无法在一个机制被建立起来之前,就使用这个机制来保护这个机制的建立过程。

具体来说,访问UEFI变量依赖于在DXE中初始化的运行时服务EFI_VARIABLE_SERVICES。而密码学运算则依赖于像EFI_HASH_PROTOCOL这样的协议,这些协议同样由DXE驱动产生。因此,在DXE阶段的部分驱动运行起来之前,系统根本不具备检查签名和数据库的能力。这个架构上的“盲点”正是像Intel Boot Guard或AMD Hardware Verified Boot这类硬件信任根技术诞生的原因。它们将信任链向下延伸至硬件本身,以保护安全启动无法覆盖的SEC和PEI阶段。可以这样理解:安全启动保护操作系统免受固件的威胁,而硬件信任根则保护固件免受自身的威胁。

 
 

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

THE END
分享
二维码
海报
UEFI Training – UEFI DXE 阶段详细解析
1. DXE在UEFI PI架构中的角色 DXE(Driver Execution Environment, 驱动程序执行环境)阶段是UEFI平台初始化(Platform Initialization, PI)架构中的核心环……
<<上一篇
下一篇>>
文章目录
关闭
目 录