EDK2工程结构与构建系统
EDK2(EFI Development Kit II)是UEFI固件开发的事实标准框架,其工程组织方式与传统的嵌入式工程(如Keil、IAR工程)或Linux内核工程有显著差异。理解EDK2的目录结构、元数据文件(DSC/DEC/INF)以及构建工具链的工作机制,是进行平台移植、驱动开发和固件集成的第一道门槛。本章将系统拆解EDK2的工程骨架,从文件语法到构建流程,再到实际编译排障,帮助您建立完整的构建系统认知。
3.1 EDK2目录结构与包(Package)组织
EDK2的源码组织以包(Package)为逻辑单元,每个包是一个完整的模块集合,包含头文件、库、驱动和工具。一个标准EDK2工作区(Workspace)通常包含多个包,例如MdePkg(基础协议定义)、MdeModulePkg(通用模块实现)、UefiCpuPkg(CPU相关驱动)以及平台专属包(如OvmfPkg、IntelMinPlatformPkg)。
| 目录/文件 | 作用 | 典型内容 |
|---|---|---|
MdePkg/ | 基础包,定义UEFI规范中的协议、PPI、GUID和基础库 | Include/(头文件)、Library/(库实例)、MdePkg.dec |
MdeModulePkg/ | 通用模块包,包含磁盘驱动、控制台、BDS等核心模块 | Universal/(通用驱动)、Bus/(总线驱动) |
Platform/<Vendor>/ | 平台专属包,存放具体板卡的DSC/FDF和定制驱动 | <Board>Pkg.dsc、<Board>Pkg.fdf、Include/ |
BaseTools/ | 构建工具源码,包括build主程序、GenFv、GenFds等 | Bin/(编译好的工具)、Source/C/(工具源码) |
Conf/ | 构建配置文件目录 | target.txt、tools_def.txt、build_rule.txt |
Build/ | 构建输出目录(自动生成) | <Platform>/DEBUG_GCC5/X64/等 |
每个包必须包含一个.dec文件(Package Declaration),它声明了该包对外提供的协议GUID、PPI GUID、库类(Library Class)以及包含的子目录。例如,MdePkg.dec中定义了gEfiSimpleTextOutProtocolGuid等关键GUID。包内的每个模块(驱动或应用)必须有一个.inf文件(Module Information),描述模块的源文件、依赖的库、编译选项和入口函数。
.dec定义接口,.inf声明使用哪些接口,.dsc决定构建哪些模块并指定库实例,.fdf决定固件镜像的Flash布局。这种分层设计使得模块可以独立编译和复用,但初学者常因混淆这些文件的作用而导致构建失败。
3.2 DSC、DEC、INF文件格式详解
这三种文件都是基于INI格式的文本文件,使用方括号[ ]定义节(Section),使用#注释。理解它们的语法是编写和修改平台配置的基础。
3.2.1 INF文件(模块定义)
INF文件描述一个独立模块的构建信息。一个典型的驱动INF包含以下节:
[Defines]
INF_VERSION = 0x0001001B
BASE_NAME = MyUsbDriver
FILE_GUID = 12345678-9ABC-DEF0-1234-56789ABCDEF0
MODULE_TYPE = UEFI_DRIVER
VERSION_STRING = 1.0
ENTRY_POINT = MyUsbDriverEntryPoint
[Sources]
MyUsbDriver.c
MyUsbDriver.h
UsbHwInit.c
[Packages]
MdePkg/MdePkg.dec
MdeModulePkg/MdeModulePkg.dec
[LibraryClasses]
UefiDriverModelLib
UefiLib
DebugLib
MemoryAllocationLib
[Protocols]
gEfiUsb2HcProtocolGuid
gEfiDevicePathProtocolGuid
[Depex]
TRUE
关键点:MODULE_TYPE决定了模块的类型(如BASE、SEC、PEIM、DXE_DRIVER、UEFI_DRIVER等),不同类型链接的库和入口函数规范不同。FILE_GUID是模块的唯一标识,在FDF中用于定位模块。[Depex]节声明模块的依赖表达式,只有依赖满足时模块才会被加载。
3.2.2 DEC文件(包声明)
DEC文件定义包级别的接口,包括GUID、协议、PPI、库类。例如:
[Defines]
DEC_SPECIFICATION = 0x0001001B
PACKAGE_NAME = MyPlatformPkg
PACKAGE_GUID = ABCD1234-1234-5678-9ABC-DEF012345678
PACKAGE_VERSION = 0.1
[Includes]
Include
[LibraryClasses]
MyPlatformLib|Include/Library/MyPlatformLib.h
[Protocols]
gMyPlatformProtocolGuid = { 0x11111111, 0x2222, 0x3333, { 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB } }
当其他模块的INF中[Packages]引用了该DEC,就可以使用其中声明的库类和GUID。DEC中的[Includes]指定了头文件搜索路径。
3.2.3 DSC文件(平台配置)
DSC文件是平台构建的“总指挥”,它定义了构建哪些模块、使用哪个库实例、全局编译选项以及PCD(Platform Configuration Database)设置。一个简化的DSC结构如下:
[Defines]
PLATFORM_NAME = MyBoard
PLATFORM_GUID = 22222222-3333-4444-5555-666677778888
DSC_SPECIFICATION = 0x0001001B
OUTPUT_DIRECTORY = Build/MyBoard
FLASH_DEFINITION = MyBoardPkg/MyBoardPkg.fdf
BUILD_TYPE = DEBUG
SUPPORTED_ARCHITECTURES = IA32|X64
[LibraryClasses.common]
DebugLib|MdePkg/Library/BaseDebugLibSerialPort/BaseDebugLibSerialPort.inf
PrintLib|MdePkg/Library/BasePrintLib/BasePrintLib.inf
[PcdsFixedAtBuild]
gEfiMdePkgTokenSpaceGuid.PcdDebugPrintErrorLevel|0x80000040
[Components]
MdeModulePkg/Universal/Console/ConPlatformDxe/ConPlatformDxe.inf
MyPlatformPkg/Drivers/MyUsbDriver/MyUsbDriver.inf
[Components]节列出所有需要构建的模块,每个模块可以附加{ ... }块来覆盖库实例或PCD。[LibraryClasses]节将库类映射到具体的库实例,这是EDK2实现模块可配置性的核心机制。
[Components]节中对该模块使用<LibraryClasses>覆盖即可,无需修改模块源码。
3.3 Build工具链与编译流程
EDK2的构建系统基于build命令,它负责解析DSC/DEC/INF文件,生成C代码(AutoGen),调用编译器编译,最后使用GenFv等工具生成固件卷(FV)和固件设备(FD)。
3.3.1 构建流程的四个阶段
- 解析阶段:
build读取Conf/target.txt获取默认平台、工具链和架构,然后解析DSC文件及其引用的所有DEC和INF文件,构建内部依赖数据库。 - 生成阶段:为每个模块生成
AutoGen.c和AutoGen.h,这些文件包含模块的GUID、入口点、PCD访问函数等。同时生成模块的Makefile。 - 编译链接阶段:调用编译器(如GCC、MSVC、ICC)编译源文件,使用链接器生成EFI可执行文件(
.efi)。此阶段还生成.map映射文件和.debug调试信息。 - 固件生成阶段:使用GenFv将多个
.efi打包成固件卷(FV),再使用GenFds根据FDF文件将多个FV和二进制文件组合成最终的固件镜像(.fd)。
3.3.2 常用构建命令
# 全平台构建(使用target.txt中的默认配置)
build
# 指定平台和工具链
build -p MyPlatformPkg/MyBoard.dsc -t GCC5
# 仅构建单个模块(快速迭代)
build -p MyPlatformPkg/MyBoard.dsc -m MyPlatformPkg/Drivers/MyUsbDriver/MyUsbDriver.inf -t GCC5
# 清理构建产物
build -p MyPlatformPkg/MyBoard.dsc -t GCC5 -clean
# 增量构建(默认模式,只编译变更的模块)
build -p MyPlatformPkg/MyBoard.dsc -t GCC5 -j 8
target.txt中的关键配置项包括ACTIVE_PLATFORM(默认DSC路径)、TARGET(DEBUG/RELEASE)、TARGET_ARCH(IA32/X64/ARM等)和TOOL_CHAIN_TAG(如GCC5、VS2019)。
3.4 平台配置与模块编译实战
下面通过一个实际场景演示如何从零开始配置一个简单的平台并编译一个模块。假设我们有一个自定义平台MyBoard,需要添加一个串口驱动。
步骤1:创建平台包目录结构
MyPlatformPkg/
├── MyPlatformPkg.dec
├── MyBoard.dsc
├── MyBoard.fdf
├── Include/
│ └── MyPlatform.h
└── Drivers/
└── MySerialDxe/
├── MySerialDxe.c
├── MySerialDxe.h
└── MySerialDxe.inf
步骤2:编写DEC文件,声明平台特有的GUID和库类。
步骤3:编写DSC文件,引用必要的包和模块。关键是要包含MdePkg、MdeModulePkg,并添加串口驱动模块。同时需要配置串口相关的PCD,如波特率、基地址:
[PcdsFixedAtBuild]
gEfiMdeModulePkgTokenSpaceGuid.PcdSerialUseMmio|TRUE
gEfiMdeModulePkgTokenSpaceGuid.PcdSerialRegisterBase|0xFE034000
gEfiMdeModulePkgTokenSpaceGuid.PcdSerialBaudRate|115200
步骤4:编写FDF文件,定义固件卷布局,将串口驱动放入DXE固件卷。
步骤5:执行构建
cd edk2
source edksetup.sh
build -p MyPlatformPkg/MyBoard.dsc -t GCC5 -b DEBUG
结果与排障:首次构建可能失败,常见错误包括:PCD not found(PCD未在DEC中声明)、Library instance not found(DSC中缺少库映射)、GUID conflict(FILE_GUID重复)。通过查看Build/MyBoard/DEBUG_GCC5/X64/MySerialDxe/目录下的AutoGen.h和.map文件,可以快速定位问题。
build -clean后重新构建,或删除Build/目录下的对应平台文件夹。
3.5 常见误区与构建优化
初学者在EDK2构建中常犯以下错误,需要特别注意:
- 误区1:认为修改
.c文件后只需重新编译该模块。实际上,如果修改了.inf中的源文件列表或依赖库,必须重新运行build以重新生成AutoGen。 - 误区2:在DSC的
[Components]中重复添加同一模块的不同实例,导致GUID冲突。 - 误区3:忽略
Conf/target.txt中的ACTIVE_PLATFORM设置,导致build命令构建了错误的平台。 - 误区4:在64位平台上使用
-m编译32位模块时,未指定-a IA32,导致链接错误。
对于大型平台,构建时间可能长达数十分钟。优化策略包括:使用-j参数启用并行编译;将不常修改的模块预先编译为二进制文件,在DSC中使用BINARY方式引用;使用--hash选项启用增量构建的哈希校验,避免不必要的重编译。
build -p ... -t ... -v的详细输出,观察解析过程是否报错。
通过本章的学习,您应该能够独立阅读一个EDK2平台的目录结构,理解DSC/DEC/INF文件的作用,并能够使用build命令完成全平台或单模块的编译。在实际项目中,建议从复制一个现有的简单平台(如OvmfPkg)开始修改,逐步替换为自己的驱动和配置,这是最稳妥的学习路径。