4、EDK II工程结构:包(Package)、模块(Module)、平台(Platform)、构建描述文件(.dsc、.inf、.dec)的作用与关系
EDK II 的工程结构并非简单的目录堆叠,而是一套以“描述文件”为骨架、以“依赖解析”为灵魂的声明式构建体系。理解 Package、Module、Platform 三个抽象层次以及 .dec、.inf、.dsc 三种描述文件的协作关系,是驾驭 UEFI 固件工程的第一步。本章将拆解这些核心概念,并揭示它们如何通过“元数据”驱动构建系统完成从源码到固件镜像的转换。
上图展示了 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 来读取板级信息。
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 文件通常遵循以下步骤:
- 定义
[Defines]:设置INF_VERSION、BASE_NAME、FILE_GUID、MODULE_TYPE(如 DXE_DRIVER、PEIM、BASE)。 - 列出
[Sources]:包括 .c、.h、.asm 文件,路径相对于.inf所在目录。 - 声明
[Packages]:列出本模块需要引用的所有.dec文件路径。 - 声明
[LibraryClasses]:列出本模块直接调用的库类名(如 UefiLib、DebugLib)。 - 声明
[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 文件进行符号解析。
操作流程:一次完整的构建过程如下:
- 读取
.dsc,确定平台配置和模块列表。 - 对每个模块,读取其
.inf,收集源文件、库类、PCD 需求。 - 根据
.inf中的[Packages]找到.dec文件,解析 GUID、PCD 和 Include 路径。 - 根据
.dsc中的[LibraryClasses]映射,为每个模块解析库依赖。 - 生成 AutoGen.c/.h 文件(包含 PCD 变量、GUID 定义等),然后调用编译器(如 GCC、VS)进行编译链接。
- 最后根据
.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 中声明。这个流程环环相扣,任何一步遗漏都会导致构建失败或运行时错误。
.dsc 出发,找到模块,再找到其依赖的包和库。这种“自顶向下”的阅读方法远比直接看源码更高效。