4、EDK II工程结构:包(Package)、模块(Module)、平台(Platform)、构建描述文件(.dsc、.inf、.dec)的作用与关系

EDK II 的工程结构并非简单的目录堆叠,而是一套以“描述文件”为骨架、以“依赖解析”为灵魂的声明式构建体系。理解 Package、Module、Platform 三个抽象层次以及 .dec、.inf、.dsc 三种描述文件的协作关系,是驾驭 UEFI 固件工程的第一步。本章将拆解这些核心概念,并揭示它们如何通过“元数据”驱动构建系统完成从源码到固件镜像的转换。

EDK II 工程结构核心关系图 平台层 (Platform) *.dsc — 平台构建描述:定义平台包含哪些模块、PCD值、库实例、构建标志 *.fdf — Flash描述文件:决定固件卷(FV)、固件文件(FFS)在Flash中的布局 *.fdf.inc / *.dsc.inc — 公共配置片段,可被多个平台复用 包层 (Package) *.dec — 包声明:定义包内所有模块共享的 GUID、Protocol、PPI、PCD、Include路径 MdePkg / MdeModulePkg / UefiCpuPkg / 自定义包 — 逻辑分组与依赖边界 Include/ 目录 — 公共头文件,通过 [Includes] 段暴露给其他模块 模块层 (Module) *.inf — 模块构建信息:源文件列表、依赖的包、库类、PCD声明、架构类型 DXE驱动 / PEIM / Shell应用 / 库实例 — 最小构建单元 Build/ 输出目录 — 编译产物按模块名+架构+工具链组织 引用模块 依赖包 构建流程 1. 解析 .dsc 2. 解析 .inf → 收集依赖 3. 解析 .dec → 解析GUID/PCD 4. 生成AutoGen + 编译链接 关键:.dec 定义“接口契约”,.inf 声明“实现需求”,.dsc 完成“平台装配”

上图展示了 EDK II 工程中三个抽象层次与三种描述文件的协作关系。在实际开发中,这种分层设计使得代码复用、平台定制和模块独立测试成为可能。下面我们将逐一深入剖析每个元素的职责与交互细节。

4.1 包(Package):接口契约与代码分组的容器

核心原理:Package 是 EDK II 中最高级别的逻辑容器,它不仅仅是一个目录,更是一个“契约边界”。一个 Package 通常对应一个功能域,例如 MdePkg 定义了 UEFI 规范的基础数据类型、协议接口和库类;MdeModulePkg 则提供通用的模块实现。Package 通过 .dec 文件向外界声明其“能提供什么”,包括 GUID、Protocol GUID、PPI GUID、PCD 以及公开的头文件路径。

判断方法:如何判断一个目录是否构成一个 Package?关键看两点:第一,根目录下是否存在 .dec 文件;第二,该 .dec 文件是否被其他模块的 .inf 通过 [Packages] 段引用。如果一个目录只是存放源码但没有 .dec,它就不是一个 Package,而只是普通源码目录。

案例拆解:假设我们开发一个名为 AcmeBoardPkg 的板级支持包。其 .dec 文件片段如下:

[Defines]
  DEC_SPECIFICATION              = 0x0001001F
  PACKAGE_NAME                   = AcmeBoardPkg
  PACKAGE_GUID                   = 1B2F3C4D-5E6F-4A8B-9C0D-1E2F3A4B5C6D
  PACKAGE_VERSION                = 0.1

[Includes]
  Include

[Guids]
  gAcmeBoardTokenSpaceGuid      = { 0x2A3B4C5D, 0x6E7F, 0x4A8B, { 0x9C, 0x0D, 0x1E, 0x2F, 0x3A, 0x4B, 0x5C, 0x6D } }

[PcdsFixedAtBuild]
  gAcmeBoardTokenSpaceGuid.PcdBoardRev|0x01|UINT8|0x00000001
  gAcmeBoardTokenSpaceGuid.PcdDebugUartBase|0xFE200000|UINT64|0x00000002

这里我们声明了包 GUID、Include 路径、一个 TokenSpace GUID 以及两个固定编译期 PCD。任何模块只要在 .inf 中声明 AcmeBoardPkg/AcmeBoardPkg.dec,就可以使用 gAcmeBoardTokenSpaceGuid.PcdBoardRev 来读取板级信息。

经验提醒:Package 的命名应遵循“厂商/功能”惯例,例如 IntelSiliconPkg、ArmPlatformPkg。避免创建过于细碎的 Package,否则会导致依赖关系混乱。一个 Package 内的模块应具有高内聚性。

4.2 模块(Module)与 .inf 文件:构建的最小单元

核心原理:Module 是 EDK II 中可独立编译的最小单元,它可以是驱动程序(DXE、PEI、SMM)、应用程序(Shell 命令)、库实例(Library Instance)或甚至是一个纯二进制封装。每个模块必须有一个 .inf 文件,该文件是模块的“身份证”和“购物清单”。.inf 通过 [Sources] 列出源文件,通过 [Packages] 声明依赖的包,通过 [LibraryClasses] 声明需要的库类,通过 [Pcd] 声明要使用的 PCD。

操作流程:编写一个模块的 .inf 文件通常遵循以下步骤:

  1. 定义 [Defines]:设置 INF_VERSION、BASE_NAME、FILE_GUID、MODULE_TYPE(如 DXE_DRIVER、PEIM、BASE)。
  2. 列出 [Sources]:包括 .c、.h、.asm 文件,路径相对于 .inf 所在目录。
  3. 声明 [Packages]:列出本模块需要引用的所有 .dec 文件路径。
  4. 声明 [LibraryClasses]:列出本模块直接调用的库类名(如 UefiLib、DebugLib)。
  5. 声明 [Pcd]:列出使用的 PCD 名称及访问类型(FixedAtBuild、PatchableInModule、Dynamic)。

实战场景:以编写一个简单的 DXE 驱动为例,其 .inf 可能如下:

[Defines]
  INF_VERSION                    = 0x0001001B
  BASE_NAME                      = AcmeHelloDriver
  FILE_GUID                      = 3A4B5C6D-7E8F-4A9B-8C0D-1E2F3A4B5C6D
  MODULE_TYPE                    = DXE_DRIVER
  VERSION_STRING                 = 1.0
  ENTRY_POINT                    = AcmeHelloDriverEntry

[Sources]
  AcmeHelloDriver.c

[Packages]
  MdePkg/MdePkg.dec
  AcmeBoardPkg/AcmeBoardPkg.dec

[LibraryClasses]
  UefiDriverEntryPoint
  UefiLib
  DebugLib

[Pcd]
  gAcmeBoardTokenSpaceGuid.PcdBoardRev

这里模块声明了入口函数、依赖的包和库,以及一个 PCD。构建系统会根据 [Packages] 中的声明去解析 .dec 文件,从而获得 gAcmeBoardTokenSpaceGuid 的定义。

风险说明:常见的错误是忘记在 [Packages] 中声明实际使用的包,或者声明了未使用的包。前者会导致编译报错“无法打开文件”,后者虽然不报错,但会增加不必要的依赖,降低构建速度并可能引入版本冲突。

4.3 平台(Platform)与 .dsc 文件:系统级装配中心

核心原理:Platform 是面向最终固件产品的描述层。一个平台对应一个具体的硬件产品(如某款服务器主板、某款开发板)。.dsc 文件是平台构建的“总指挥”,它决定了:要构建哪些模块([Components])、每个模块使用哪个库实例([LibraryClasses])、全局 PCD 的默认值([PcdsFixedAtBuild] 等)、以及编译选项和宏定义([BuildOptions])。

判断方法:如何区分 .dsc 和 .inf 的职责?简单来说,.inf 回答“这个模块需要什么”,而 .dsc 回答“这个平台给模块提供什么”。例如,模块 AcmeHelloDriver.inf 声明需要 DebugLib,但具体使用 MdePkg/Library/BaseDebugLibSerialPort/BaseDebugLibSerialPort.inf 还是 BaseDebugLibNull.inf,由 .dsc 中的 [LibraryClasses] 段决定。

案例拆解:一个典型的 .dsc 文件结构如下:

[Defines]
  PLATFORM_NAME                  = AcmeBoard
  PLATFORM_GUID                  = 4B5C6D7E-8F9A-4B0C-8D1E-2F3A4B5C6D7E
  DSC_SPECIFICATION              = 0x0001001F
  OUTPUT_DIRECTORY               = Build/AcmeBoard
  FLASH_DEFINITION               = AcmeBoardPkg/AcmeBoard.fdf
  BUILD_TARGETS                  = DEBUG|RELEASE
  SKUID_IDENTIFIER               = DEFAULT

[LibraryClasses]
  DebugLib|MdePkg/Library/BaseDebugLibSerialPort/BaseDebugLibSerialPort.inf
  UefiLib|MdePkg/Library/UefiLib/UefiLib.inf

[PcdsFixedAtBuild]
  gAcmeBoardTokenSpaceGuid.PcdBoardRev|0x03

[Components]
  AcmeBoardPkg/Drivers/AcmeHelloDriver/AcmeHelloDriver.inf
  MdeModulePkg/Application/HelloWorld/HelloWorld.inf

在这个例子中,平台将 DebugLib 映射到串口输出版本,并覆盖了 PcdBoardRev 的默认值(从 0x01 改为 0x03)。[Components] 列出了两个需要构建的模块。注意,HelloWorld.inf 来自 MdeModulePkg,这展示了跨包复用模块的能力。

常见误区:初学者常误以为 .dsc 中列出的模块都会被包含进最终的固件镜像。实际上,.dsc 只决定“构建哪些模块”,而“哪些模块被放进 Flash”由 .fdf 文件决定。一个模块可以被编译但不被 .fdf 引用,从而不会出现在最终固件中。

4.4 三者关系与构建解析流程

核心原理:三者的关系可以概括为“自底向上依赖,自顶向下配置”。.dec 定义了接口和常量,.inf 声明了模块对接口的需求,.dsc 则负责将需求绑定到具体的实现(库实例)并设定全局参数。构建系统(如 build 命令)的执行顺序是:先解析 .dsc,再根据 [Components] 逐个解析 .inf,最后通过 [Packages] 找到对应的 .dec 文件进行符号解析。

操作流程:一次完整的构建过程如下:

  1. 读取 .dsc,确定平台配置和模块列表。
  2. 对每个模块,读取其 .inf,收集源文件、库类、PCD 需求。
  3. 根据 .inf 中的 [Packages] 找到 .dec 文件,解析 GUID、PCD 和 Include 路径。
  4. 根据 .dsc 中的 [LibraryClasses] 映射,为每个模块解析库依赖。
  5. 生成 AutoGen.c/.h 文件(包含 PCD 变量、GUID 定义等),然后调用编译器(如 GCC、VS)进行编译链接。
  6. 最后根据 .fdf 文件将编译产物打包成固件镜像(FD 文件)。

知识结构表格:

描述文件所属层次核心作用关键段(节选)类比
.dec包声明接口契约(GUID/PCD/Include)[Defines]、[Guids]、[PcdsFixedAtBuild]API 文档 + 头文件
.inf模块描述模块自身构建需求[Sources]、[Packages]、[LibraryClasses]Makefile + 依赖清单
.dsc平台平台级模块装配与参数配置[Components]、[LibraryClasses]、[PcdsFixedAtBuild]总装图纸 + 物料清单

总结复盘:在实际项目中,我们经常需要新增一个模块。正确的操作路径是:先在包内创建模块目录并编写 .inf,然后在平台的 .dsc 的 [Components] 中添加该 .inf 路径,最后在 .fdf 中决定是否将其打包进固件。如果模块需要新的 PCD,则需先在 .dec 中声明。这个流程环环相扣,任何一步遗漏都会导致构建失败或运行时错误。

重点提示:理解三者的关系是 EDK II 开发的“分水岭”。建议初学者在阅读任何代码之前,先画出目标模块的依赖树:从 .dsc 出发,找到模块,再找到其依赖的包和库。这种“自顶向下”的阅读方法远比直接看源码更高效。