3. EDK II 基础概念
EDK II 的工程体系并不是“一个工程编译一个固件”这么简单,它由一组彼此关联的基础概念共同支撑:包(Package)负责组织源码与元数据,模块(Module)负责实现具体功能,库(Library)负责提供可复用能力,协议(Protocol)、PPI、GUID、HOB、事件与启动阶段则负责运行期的发现、传递、调度与生命周期管理。理解这些概念之间的关系,是后续进行模块开发与集成的第一步。
一、包与模块:源码组织与构建单元
包(Package)是 EDK II 中源码与元数据组织的基本单位,通常以 .dec(包声明)、.dsc(平台描述)、.inf(模块信息)三类文件为核心。一个包可以包含多个模块、库、协议定义和 GUID 声明。包的作用是划定命名空间、声明对外提供的接口与依赖,使不同厂商、不同团队的代码可以按统一规则组合。
模块(Module)是构建与加载的最小功能单元,由 .inf 文件描述。模块分为多种类型,常见的有:
- DXE_DRIVER:DXE 阶段驱动,通常用于生产协议;
- PEIM:PEI 阶段模块,用于早期硬件初始化;
- DXE_RUNTIME_DRIVER:运行时驱动,操作系统运行期仍可被调用;
- UEFI_APPLICATION:UEFI 应用,运行在 UEFI Shell 或启动项中;
- LIBRARY:库模块,不独立执行,供其他模块链接使用。
模块通过 [Sources]、[Packages]、[LibraryClasses]、[Protocols]、[Ppis]、[Guids] 等段声明自己的源码、依赖与对外接口。构建系统据此解析依赖并生成固件卷。
二、库、协议、PPI 与 GUID:复用与发现机制
库(Library)是 EDK II 最重要的复用机制。库以 LibraryClass 为抽象接口,以具体库实例为实现。模块只依赖库类名,平台在 DSC 中把库类解析到具体实例。这样同一模块可以在不同平台上链接不同实现,例如 BaseLib、DebugLib、MemoryAllocationLib、UefiBootServicesTableLib 等。
协议(Protocol)是 DXE 及 UEFI 运行期的主要服务发现机制。协议由 GUID 标识,结构体包含函数指针与数据字段。生产者通过 InstallProtocolInterface() 或 InstallMultipleProtocolInterfaces() 安装协议,消费者通过 LocateProtocol()、OpenProtocol() 获取协议实例。协议是“按 GUID 查找服务”的典型实现。
PPI(PEIM-to-PEIM Interface)是 PEI 阶段的接口机制,作用类似 DXE 阶段的协议,但生命周期更早、内存资源更紧张。PPI 同样由 GUID 标识,通过 InstallPpi() 安装、LocatePpi() 查找。PEI 阶段结束后,PPI 数据库通常不再可用,因此 PPI 适合传递早期硬件信息,而不适合长期服务。
GUID是 EDK II 中广泛使用的 128 位标识符,用于标识协议、PPI、HOB、变量、文件、事件组等。GUID 保证全局唯一,使不同模块之间可以解耦地按标识符交互。常见写法如:
#define EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL_GUID \
{ 0x387477c2, 0x69c7, 0x11d2, \
{ 0x8e, 0x39, 0x00, 0xa0, 0xc9, 0x69, 0x72, 0x3b } }
三、HOB、事件与启动阶段:运行期信息传递与调度
HOB(Hand-Off Block)是 PEI 阶段向 DXE 阶段传递信息的核心数据结构。PEI 阶段把内存、CPU、平台配置等信息以 HOB 链表形式交给 DXE 阶段,DXE 通过 GetFirstHob()、GetNextHob() 遍历。HOB 是只读的、单向传递的,适合描述“平台事实”,不适合作为可变服务接口。
事件(Event)是 DXE 与 UEFI 运行期的异步调度机制。事件可被设置为定时器事件、通知事件或信号事件,通过 CreateEvent()、CreateEventEx() 创建,通过 SignalEvent() 触发,通过 WaitForEvent() 等待。事件组(Event Group)用于在启动流程的关键节点通知多个模块,例如 gEfiEventReadyToBootGuid、gEfiEventExitBootServicesGuid。
启动阶段划分是理解 EDK II 运行顺序的主线。典型阶段包括:
| 阶段 | 主要职责 | 典型模块类型 |
|---|---|---|
| SEC | 安全验证、临时内存、进入 PEI | SEC 核心 |
| PEI | 早期硬件初始化、内存发现、HOB 构建 | PEIM |
| DXE | 驱动调度、协议安装、设备初始化 | DXE_DRIVER |
| BDS | 启动策略、控制台、启动项选择 | BDS 模块 |
| TSL | 操作系统加载前的临时系统环境 | OS Loader |
| RT | 运行时服务,操作系统运行期仍可用 | DXE_RUNTIME_DRIVER |
四、核心关系图解
下图把包、模块、库、协议、PPI、GUID、HOB、事件与启动阶段放在同一张关系图中,帮助建立整体认知:包与模块是构建期概念,库是链接期复用机制,协议与 PPI 是运行期发现机制,GUID 是贯穿所有阶段的标识符,HOB 是 PEI 到 DXE 的信息桥梁,事件是 DXE 运行期的调度手段,启动阶段则决定这些机制在时间轴上的先后顺序。
五、案例与场景:一个 DXE 驱动如何串起这些概念
假设要开发一个 DXE 驱动,用于初始化某 PCIe 设备并对外提供协议。开发过程会依次涉及:
- 在某个包中新增模块目录,编写
.inf,声明模块类型为DXE_DRIVER,并在[LibraryClasses]中依赖UefiDriverEntryPoint、UefiLib、DebugLib等库类; - 在平台 DSC 中把库类解析到具体库实例,并把该模块加入
[Components]; - 驱动入口中通过
gBS->LocateProtocol()获取EFI_PCI_IO_PROTOCOL,完成设备访问; - 初始化完成后调用
InstallProtocolInterface()安装自定义协议,GUID 在.dec或头文件中声明; - 若需要在
ReadyToBoot时执行收尾动作,可创建事件并注册到gEfiEventReadyToBootGuid事件组; - 若该驱动还依赖 PEI 阶段传递的平台信息,则通过 HOB 读取,而不是重新探测硬件。
六、常见误区与复盘
- 把库当模块:库不能独立执行,必须被模块链接;在 DSC 中只解析库类,不把库加入
[Components]作为可执行模块。 - 在 PEI 阶段使用 DXE 协议:PEI 阶段只有 PPI,协议数据库尚未建立,错误调用会导致启动失败。
- 用 HOB 传递可变服务:HOB 是只读快照,适合描述平台事实;需要动态服务时应使用协议或 PPI。
- GUID 重复或写错:GUID 必须全局唯一,复制粘贴后未修改会造成协议安装冲突或查找失败。
- 事件注册过晚:若在
ReadyToBoot之后才注册该事件组,通知不会触发,需在 DXE 早期完成注册。
掌握这些基础概念后,再阅读 DSC、INF、DEC 文件时,就能看清每个字段背后的运行期含义:哪些是构建期依赖,哪些是运行期发现,哪些是跨阶段传递,哪些是生命周期回调。这是后续进行模块开发、库替换、协议扩展与平台集成的共同基础。