4、UEFI工程结构:EDK2目录结构、.dsc/.fdf/.dec文件解析、Package与Module概念
好,咱们今天聊点实在的。EDK2 的工程结构,说白了就是 UEFI 固件开发的「工地现场」。你拿到一个 BIOS 项目,第一件事不是写代码,而是搞清楚这个工地怎么布局的。我刚开始接触 EDK2 的时候,打开目录一看,好家伙,几百个文件夹,头皮发麻。后来摸清楚了,其实就那么几块东西。
4.1 EDK2 顶层目录结构
EDK2 的根目录,你 clone 下来之后,会看到一堆文件夹。我挑几个最核心的说说:
- MdePkg:这是基础包,定义了 UEFI 规范里的各种数据类型、协议、PPI 和 GUID。说白了就是「公共头文件库」。你写任何模块,几乎都要依赖它。
- MdeModulePkg:在 MdePkg 之上,提供了一些通用的模块实现,比如磁盘驱动、控制台输出、Boot Manager 等。属于「半成品」,拿来就能用。
- IntelFrameworkPkg:这是 Intel 的框架包,里面封装了 Framework 规范(PI 规范的前身)的一些接口。现在新项目用得少了,但老项目里还常见。
- UefiCpuPkg:CPU 相关的驱动和库,比如 MP 初始化、SMM、CpuIo 等。做 x86 平台绕不开它。
- ArmPkg / ArmPlatformPkg:ARM 架构的包。做服务器或者嵌入式 UEFI 会用到。
- OvmfPkg:QEMU 虚拟机的 UEFI 固件。我经常拿它做实验,不用硬件就能调试,非常方便。
- NetworkPkg:网络协议栈,包括 HTTP Boot、iSCSI、PXE 等。
- ShellPkg:UEFI Shell 的实现。调试的时候,进 Shell 敲命令,比看日志快多了。
Conf/target.txt 和 Conf/tools_def.txt。这两个文件决定了编译器和平台配置。很多新手编译不过,就是这里没配对。
4.2 Package 与 Module 的概念
EDK2 里有两个基本概念:Package 和 Module。
Package 是一个逻辑上的「包」,它把一组相关的模块、库、头文件、PCD 定义、甚至 GUID 声明打包在一起。每个 Package 都有一个 .dec 文件(Declaration File),用来声明这个包对外暴露了什么。
Module 是具体的可执行单元。它可以是:
- DXE 驱动(.efi 文件)
- PEIM(PEI 阶段的模块)
- 应用程序(比如 UEFI Shell 下的工具)
- 库实例(.inf 文件描述)
每个 Module 都有一个 .inf 文件(Module Information File),描述这个模块的源文件、依赖的库、PCD 设置、编译选项等。
一句话总结: Package 是「文件夹+接口声明」,Module 是「具体的 .efi 或 .lib」。一个 Package 里可以包含几十个 Module。
4.3 .dec 文件解析
.dec 文件是 Package 的「身份证」。它告诉编译器和其它 Package:我这个包里有什么东西可以给你用。
一个典型的 .dec 文件结构如下:
#
# MdePkg/MdePkg.dec
#
[Defines]
DEC_SPECIFICATION = 0x00010005
PACKAGE_NAME = MdePkg
PACKAGE_GUID = 1E73767F-8A52-4b2c-9E79-6B6B3B2B4C3E
PACKAGE_VERSION = 1.08
[Includes]
Include # 公共头文件路径
[LibraryClasses]
BaseLib|Include/Library/BaseLib.h
UefiLib|Include/Library/UefiLib.h
DebugLib|Include/Library/DebugLib.h
[Guids]
gEfiGlobalVariableGuid = { 0x8BE4DF61, 0x93CA, 0x11D2, { 0xAA, 0x0D, 0x00, 0xE0, 0x98, 0x03, 0x2B, 0x8C } }
[Protocols]
gEfiSimpleTextOutProtocolGuid = { 0x387477C1, 0x69C7, 0x11D2, { 0x8E, 0x39, 0x00, 0xA0, 0xC9, 0x69, 0x72, 0x3B } }
[PcdsFixedAtBuild]
gEfiMdePkgTokenSpaceGuid.PcdDebugPropertyMask|0x1F|UINT8|0x00000001
关键段解释:
| 段名 | 作用 |
|---|---|
[Defines] |
包的基本信息,包括 GUID、版本号。GUID 必须唯一,不能跟别的包冲突。 |
[Includes] |
声明这个包对外暴露的头文件路径。别的模块 #include 时,编译器会来这里找。 |
[LibraryClasses] |
声明这个包里有哪些库类(Library Class)。注意是「类」不是「实例」,具体实现由 .inf 决定。 |
[Guids] |
定义这个包里使用的 GUID。GUID 是 UEFI 世界的「身份证号」,协议、PPI、变量、事件都靠它区分。 |
[Protocols] |
定义这个包里提供的协议 GUID。DXE 阶段驱动之间通信,就是通过协议。 |
[PcdsFixedAtBuild] |
编译时确定的 PCD(Platform Configuration Database)。编译完就不能改了。 |
.dec 里加了一个新的 GUID,但忘了在 [Guids] 段里声明。结果编译时链接器报「未定义符号」,查了半天才发现是 .dec 没更新。记住:所有对外暴露的 GUID、Protocol、PCD,都必须先在 .dec 里注册。
4.4 .dsc 文件解析
.dsc 文件(Platform Description File)是「项目配置文件」。它定义了整个固件要包含哪些模块、库、PCD 值、编译选项等。你可以把它理解成固件的「购物清单」。
一个典型的 .dsc 文件结构:
#
# OvmfPkg/OvmfPkgX64.dsc
#
[Defines]
PLATFORM_NAME = Ovmf
PLATFORM_GUID = 5a9e7754-8a4b-4b3c-9e79-6b6b3b2b4c3e
PLATFORM_VERSION = 0.1
DSC_SPECIFICATION = 0x00010005
OUTPUT_DIRECTORY = Build/OvmfX64
SUPPORTED_ARCHITECTURES = X64
BUILD_TARGETS = DEBUG|RELEASE|NOOPT
SKUID_IDENTIFIER = DEFAULT
[LibraryClasses]
BaseLib|MdePkg/Library/BaseLib/BaseLib.inf
UefiLib|MdePkg/Library/UefiLib/UefiLib.inf
DebugLib|MdePkg/Library/BaseDebugLibSerialPort/BaseDebugLibSerialPort.inf
[LibraryClasses.common.DXE_DRIVER]
UefiBootServicesTableLib|MdePkg/Library/UefiBootServicesTableLib/UefiBootServicesTableLib.inf
[Components]
OvmfPkg/PlatformPei/PlatformPei.inf
OvmfPkg/BdsDxe/BdsDxe.inf
MdeModulePkg/Universal/Disk/DiskIoDxe/DiskIoDxe.inf
关键段解释:
| 段名 | 作用 |
|---|---|
[Defines] |
平台基本信息,包括输出目录、支持的架构、编译目标(DEBUG/RELEASE)。 |
[LibraryClasses] |
全局的库实例映射。把「库类」映射到具体的「库实现 .inf」。比如 BaseLib 这个类,用 BaseLib.inf 这个实现。 |
[LibraryClasses.common.xxx] |
按模块类型覆盖库映射。比如 DXE_DRIVER 类型的模块,使用特定的库实例。 |
[Components] |
列出所有要编译进固件的模块。每个模块对应一个 .inf 文件。 |
[PcdsFixedAtBuild] |
覆盖 PCD 的默认值。这里设置的值会覆盖 .dec 里的默认值。 |
[Components] 里只加必要的模块,把不相关的注释掉。这样编译速度快很多。等基本功能调通了,再全部打开做集成测试。
4.5 .fdf 文件解析
.fdf 文件(Flash Description File)是「固件布局文件」。它决定了最终的 BIOS 镜像(.fd 文件)里,各个模块放在 Flash 的什么位置。说白了,就是「固件在 Flash 里的地图」。
一个简化的 .fdf 文件结构:
#
# OvmfPkg/OvmfPkgX64.fdf
#
[FD.BFV]
BaseAddress = 0xFFF00000
Size = 0x00100000
ErasePolarity = 1
[FV.SECFV]
FvBaseAddress = 0xFFF00000
FvSize = 0x00020000
BlockSize = 0x1000
NumBlocks = 32
FILE FV_IMAGE = 8C8CE578-8A3D-4F1C-9935-896185C32DD3 {
SECTION PE32 = OvmfPkg/Sec/SecMain.inf
SECTION UI = "SecMain"
}
[FV.DXEFV]
FvBaseAddress = 0xFFF20000
FvSize = 0x000E0000
BlockSize = 0x1000
NumBlocks = 224
FILE DRIVER = 5B3B3B2B-4C3E-4B3C-9E79-6B6B3B2B4C3E {
SECTION PE32 = MdeModulePkg/Universal/Disk/DiskIoDxe/DiskIoDxe.inf
SECTION UI = "DiskIoDxe"
}
关键概念:
- FD (Flash Device):代表整个 Flash 芯片。定义基地址、大小、擦除极性。
- FV (Firmware Volume):Flash 里的一个逻辑分区。每个 FV 有自己的基地址、大小、块大小。
- FILE:FV 里的一个文件。每个文件对应一个模块(.efi 或 .bin)。文件类型可以是
DRIVER、FV_IMAGE、RAW等。 - SECTION:文件内部的段。比如
PE32段包含可执行代码,UI段包含用户可读的名称。
.fdf 里的 BaseAddress 必须跟硬件实际映射的 Flash 地址一致。我见过有人把基地址写错了,结果固件刷进去,CPU 根本找不到入口,直接变砖。所以,.fdf 的地址一定要跟硬件原理图对一遍。
4.6 三者的关系
这三兄弟的关系,我用一张图来展示:
你看,.dec 定义了「有什么零件」,.dsc 决定了「要装哪些零件以及怎么配置」,.fdf 决定了「零件在板子上的位置」。Build 工具就是那个装配工,把这三份图纸合起来,生成最终的固件镜像。
4.7 实战建议
最后,给刚入门的同学几个建议:
- 先从 OvmfPkg 入手:它是个完整的、可运行的平台,而且不需要硬件。你可以在 QEMU 上跑起来,然后试着改
.dsc和.fdf,看看效果。 - 学会用
build -m单独编译模块:不用每次都全量编译。比如build -m MdeModulePkg/Universal/Disk/DiskIoDxe/DiskIoDxe.inf,只编译这一个模块,快很多。 - 遇到链接错误,先查
.dsc里的库映射:很多链接错误是因为某个库类没有映射到具体的实现。去[LibraryClasses]段里补上就行。 - 调试时,在
.fdf里加RAW类型的文件:比如放一个DEBUG_IMAGE.bin,可以用来验证 Flash 布局是否正确。
核心记忆点: .dec 是「接口声明」,.dsc 是「项目配置」,.fdf 是「Flash 布局」。三者缺一不可,共同决定了固件长什么样。