4. EDK II工程结构

EDK II 的工程结构不是简单的“源码目录 + Makefile”,而是一套以包(Package)为复用边界、以模块(Module)为编译单元、以库(Library)为功能共享层、以PCD为配置参数通道、以DEC/DSC/INF/FDF四类元数据文件为组织骨架的构建体系。理解这套结构,是读懂 UEFI 固件工程、进行驱动开发和平台移植的前提。

一、核心对象与职责划分

EDK II 把“代码”和“描述代码如何被构建、如何被复用”的信息分离。代码放在模块和库中,而构建信息放在元数据文件里。四类文件各司其职:

  • DEC(Package Declaration):声明一个包的对外接口,包括包名、GUID、版本、公开头文件目录、库类(Library Class)、PCD 声明、协议/PPI/GUID 声明。它是包的“名片”,其他包通过它来引用本包资源。
  • INF(Information):描述一个模块或库实例的构建信息,包括源文件、依赖的库类、使用的 PCD、协议、PPI、GUID、编译选项等。每个可编译单元对应一个 INF。
  • DSC(Description):平台描述文件,决定“这个平台要构建哪些模块、用哪些库实例、PCD 取什么值、编译器和架构是什么”。它是构建的顶层入口。
  • FDF(Flash Description):固件镜像布局描述文件,定义 FD、FV、FFS 文件、Section 的层次结构,决定最终烧录到 Flash 中的二进制如何排布。

一句话记忆:DEC 管“有什么”,INF 管“怎么编”,DSC 管“编哪些、怎么配”,FDF 管“怎么装进 Flash”。

二、包、模块、库、PCD 的组织关系

包是最大的复用单位,一个包可以包含多个模块和多个库实例。模块通过 INF 声明自己依赖哪些库类,DSC 在平台层面把库类“绑定”到具体的库实例。PCD 则贯穿 DEC、INF、DSC 三层:DEC 声明 PCD 的存在和类型,INF 声明模块使用哪些 PCD,DSC 为平台设定 PCD 的实际值。

Package(包)— 复用边界 由 DEC 文件声明对外接口:头文件、库类、PCD、协议/GUID Module A INF 描述源文件 依赖库类 / PCD 协议 / PPI / GUID 编译选项 Module B INF 描述源文件 依赖库类 / PCD 协议 / PPI / GUID 编译选项 Library Instances BaseLib / DebugLib MemoryAllocationLib UefiBootServicesTableLib 每个库实例对应一个 INF PCD(Platform Configuration Database)— 配置参数通道 DEC 声明 PCD 名称/类型/默认值 → INF 声明模块使用哪些 PCD → DSC 设定平台实际值 类型:FixedAtBuild / PatchableInModule / Dynamic / DynamicEx DSC — 平台描述文件 决定构建哪些模块、用哪些库实例 设定 PCD 实际值、编译器和架构 构建的顶层入口 [Components] [LibraryClasses] [PcdsFixedAtBuild] FDF — Flash 布局描述文件 定义 FD / FV / FFS / Section 层次 决定二进制在 Flash 中的排布 最终生成可烧录固件镜像 [FD] [FV] [Rule] [Capsule]

三、构建流程与文件协作

一次典型的 EDK II 构建,文件之间的协作顺序如下:

  1. 构建工具读取 DSC,确定目标平台、架构、工具链和模块列表。
  2. 对每个模块,读取其 INF,获取源文件、库类依赖和 PCD 使用声明。
  3. 根据 DSC 中的 [LibraryClasses] 段,把 INF 中声明的库类解析为具体的库实例 INF。
  4. 读取各包的 DEC,解析头文件路径、PCD 声明和协议/GUID 定义。
  5. 编译所有模块和库,链接生成 EFI 可执行文件。
  6. 读取 FDF,把模块按规则打包成 FV、FFS,最终生成 FD 镜像。
# DSC 中库类绑定示例
[LibraryClasses]
  DebugLib|MdePkg/Library/BaseDebugLibSerialPort/BaseDebugLibSerialPort.inf
  MemoryAllocationLib|MdePkg/Library/UefiMemoryAllocationLib/UefiMemoryAllocationLib.inf

# INF 中声明依赖
[LibraryClasses]
  DebugLib
  MemoryAllocationLib

# DEC 中声明 PCD
[PcdsFixedAtBuild]
  gEfiMdePkgTokenSpaceGuid.PcdDebugPrintErrorLevel|0x80000000|UINT32|0x00000006

四、PCD 的四种类型与选择

PCD 是 EDK II 中传递配置参数的核心机制,按生效时机分为四类:

类型 生效时机 是否可运行时修改 典型用途
FixedAtBuild 编译时 否 调试开关、功能裁剪、固定常量
PatchableInModule 编译时,但二进制中保留可修补位置 否(可二进制修补) 需要后期用工具修改的配置
Dynamic 运行时,通过 PCD 协议读写 是 驱动运行时需要读取的平台参数
DynamicEx 运行时,通过 PCD 协议读写,支持跨模块 是 跨模块共享的动态配置

选择原则:能用 FixedAtBuild 就不用 Dynamic,因为编译期常量可以被编译器优化,代码更小、启动更快。只有确实需要在运行时改变的参数,才使用 Dynamic 或 DynamicEx。

五、常见误区与复盘

  • 误区一:把 DSC 当成 Makefile。DSC 只描述“构建什么、怎么配”,不描述“怎么编译”。编译规则由构建工具和工具链文件决定。
  • 误区二:在 INF 中写死库实例路径。INF 只应声明库类,具体用哪个库实例由 DSC 决定。这样才能实现同一模块在不同平台上复用不同库实现。
  • 误区三:PCD 在 DEC 中声明后忘记在 DSC 中赋值。如果 PCD 没有默认值且 DSC 未赋值,构建会报错。建议在 DEC 中给合理默认值。
  • 误区四:FDF 中模块顺序随意。FV 中模块的排列顺序会影响依赖解析和启动顺序,尤其是 PEI 阶段和 DXE 阶段的调度依赖。
  • 误区五:包之间循环依赖。DEC 中通过 [Packages] 引用其他包,循环依赖会导致构建失败。应通过提取公共包或使用库类解耦。

调试构建失败时,优先检查三处:DSC 中库类是否绑定完整、INF 中声明的库类是否都在 DSC 中有对应实例、PCD 是否在 DEC 中声明且在 DSC 中赋值。这三处覆盖了大多数 EDK II 构建错误。

掌握 EDK II 工程结构的关键,是建立“包—模块—库—PCD—四类文件”之间的映射关系。DEC 定义接口,INF 定义单元,DSC 定义平台,FDF 定义镜像,PCD 贯穿配置。理解这套分层协作机制,才能在驱动开发和平台移植中快速定位问题、合理组织代码。