EDKII的Build系统基于Python和C代码,支持跨平台编译。其底层逻辑是:将DEC、DSC、INF、FDF等用户元数据文件通过Build工具转换成Makefile和AutoGen.c、Autogen.h等中间文件。然后基于Makefile进行构建,生成最终FD镜像。本文主要介绍DEC、DSC、INF、FDF等用户元数据文件的写法,以及Build系统的主要执行流程。实现细节请阅读《edk2-BuildSpecification-release》。
下图给出了EDKII build系统的工作流程。描述了在EDKII中工程的配置文件(元数据)在构建过程中的作用和Build工程的处理流程。
总的来说,工作流程分为3个阶段。
元数据(meta-data)解析阶段
Build工具将分析元数据文件(DSC, DEC和FDF、INF等),生成一个顶层的Makefile; 并且针对各个模块也生成单独的Makefile文件Makefile和AutoGen.c/Autogen.h。
另外,在此过程中,Build工具还会将Moudule所需要的Guids、Protocols、Ppis、PCDs等数据经过处理输出到Autogen文件中。该文件最后会被编译进指定的二进制文件,如*.efi、.acpi、.aml等。
编译阶段
该阶段主要工作是编译源码。基于上个阶段生成的Makefile文件和AutoGen.c/Autogen.h,从顶层Makefile开始执行构建规则,调用编译工具将源码编译成符合UEFI 规范的PE32/PE32+/COFF 几种镜像文件。即各种*.efi、.acpi、.aml等。
固件生成阶段
该阶段主要是为了生成最终的FD 镜像。根据FDF 文件和源文件定义的相关规则,通过GenFw工具、GenFV工具,将上个阶段生成的EFI 格式的文件(.efi .acpi .aml .bin缀名文件), 处理成二进制镜像文件(.fd 文件)。
上面提到的大部分Build工具都位于BaseTools目录下,它是我们在搭建环境时生成的。当然,也可以直接使用编译好的工具。
上一节介绍了基本的Build流程,让我们大致知道,镜像文件FD是怎么编译来的。下面是本篇的重点,将对各个元数据文件进行介绍。
在介绍元数据文件之间,先对UEFI规范中两个重要的概念——模块(module)和包(package)进行下简单介绍。以便理解后文。
模块(module),是UEFI中最小的可单独编译的代码单元,或是预编译的二进制文件,比如*.efi可执行文件。一个模块,包括INF文件、源代码和二进制文件,其中INF文件用来描述模块的编译行为,比如产生库(Library)、产生协议(Protocol)、该模块所依赖的库、该模块的唯一标识符(GUID)等。
包(package),由平台描述文件(DSC文件)和包声明文件(DEC)组成。包主要是为了体现工程的独立性,比如在edk2源码中一个公司的代码可以作为一个独立的包存在。它可以不包含模块,也可以包含多个模块。可以看到edk2目录下有很多现成的包,比如MdeModulePkg、ArmPlatformPkg等。
对于大型工程而言,开发者都需要基于系统的构建系统创建自己的工程文件。针对不同的平台架构、芯片、目标设备,进行不同的配置,以指导构建系统选择编译适配的源码和模块。对于工程的配置,通常以配置文件的形式存在,对于构建系统来说就是元数据。
EDKII 采用模块化、层次化的概念对源代码和各种文件按照依赖关系和规则进行封装。配置文件声明了:有效包、模块之间的依赖关系;中间文件的生成规则和变量定义。
EDKII 内的包和模块描述文件称为元数据文件,主要有四种类型:
下面逐个介绍这四种文件的作用和语法。
该文件主要描述,包中的哪些库(Library)、组件(Componet)和模块(Module)参与编译。每个包内会有一个DSC 文件,Build工具可以根据DSC文件的内容将用到的模块编译进FD镜像。
DSC 文件中有如下几种字块:
该文件主要声明,当前包中提供了哪些有效变量和描述信息。主要作用是声明包内定义的数据和接口,以对外公开,供其他模块使用。
对外提供的信息有:
每个包必须有一个DEC 文件。包内的所有模块可以获得DEC 内定义的内容,但是没有修改权限。DEC 文件中的字块和字块定义与DSC 文件内相同。
INF 文件用来定义一个模块,类似于Android中的Android.mk或LK中的rule.mk。该文件定义:模块的名称,模块需要的源文件,模块依赖的其他包,模块依赖的其他库、Protocols、和PCDs等。查看源码可以知道,edk2中每个模块目录下都有一个*.inf文件。
FDF的全称是Flash Description File。用于生成image,描述(定义)Image(FD)的内容和布局信息,地位相当于Bootloader中的*.ld链接脚本,但是要比ld文件复杂很多。
它由[Defines],[FD],[FV],[Rule]等几个部分组成。
这里所谓的工程配置文件,就是我们上面所说的几种元数据文件。本节主要介绍这些文件的语法,以及在具体工程中的使用。
DSC文件描述了包中的模块、库和组件整体构建规则,其中包括很多字块,用来定义编译的内容和规则。其中包括很多字块,必要的字块有[Defines]和[Components]块。可选字块有[Libraries],[LibraryClasses],[Pcds],[SkuIds] ,[UserExtensions],[BuildOptions]等。
注意事项:
所有字块的名称,都置于中括号中,而且大小写敏感。
同一个中括号中,可以包含多个字符串,以逗号隔开。比如,
[LibraryClassess.X64,LibraryClassess.IPF]
这种LibraryClassess.(ARCH)指定CPU架构的字块要比不指定ARCH的LibraryClassess通用字块优先级高。
在DSC文件中,可以使用!include来包含其他文件,!include可以在DSC文件的任意字块出现,例如,
[Components]
!include StdLib/StdLib.inc
!include sharkl3/Sharkl3.dsc.inc
使用!include需要注意
(1)!include包含的文件必须符合所在字块的要求
(2)!include包含的文件必需有完整的一个或多个字块
[Defines]
[Defines]用于设置Build相关的全局变量,这些变量可以被!include 包含的其他文件和FDF文件引用,通过$(MACRO)的方式引用。
[Defines]必须是DSC文件的第一个部分,可以按任意顺序排布,其语法格式如下,
[Defines]
宏变量名 = 值
DEFINE 宏变量名 = 值
EDK_GLOBAL 宏变量名 = 值
示例:
// /EDK2/ShellPkg/ShellPkg.dsc 示例
[Defines]
PLATFORM_NAME = Shell
PLATFORM_GUID = E1DC9BF8-7013-4c99-9437-795DAA45F3BD
PLATFORM_VERSION = 1.0
DSC_SPECIFICATION = 0x00010006
OUTPUT_DIRECTORY = Build/Shell
SUPPORTED_ARCHITECTURES = IA32|IPF|X64|EBC|ARM|AARCH64
BUILD_TARGETS = DEBUG|RELEASE
SKUID_IDENTIFIER = DEFAULT
[LibraryClasses]
[LibraryClasses]用来列出当前包包含哪些模块。主要是为了声明,将模块名和其INF文件一一对应。
在[LibraryClasses]块定义了库的名字以及库INF的路径。这些库可以被[Components]块内的模块引用。
如果DSC文件中的模块不需要使用库,可以不定义该字块。
语法:
[LibararyClasses.$(Arch).$(MODULE_TYPE),LibararyClasses.$(Arch1).$(MODULE_TYPE1)]
LibraryName | path/LibraryName.inf
其中, ( A r c h ) 和 (Arch)和 (Arch)和(MODULE_TYPE)是可选项,字块内的库对指定的架构和模块都有效。其实, [LibraryClasses]有6种表示方法,依据模块优先搜索的顺序,从前到后(优先级从高到底)分别如下:
(1) LibraryClasses, 在DSC文件的[Components]字块定义,和INF文件联系在一起。
MdeModulePkg/Universal/PCD/Pei/Pcd.inf {
<LibraryClasses>
PcdLib|MdePkg/Lirbary/BasePcdLibNull/BasePcdLibNull.inf
(2) [LibararyClasses. ( A r c h ) . (Arch). (Arch).(MODULE_TYPE),LibararyClasses. ( A r c h 1 ) . (Arch1). (Arch1).(MODULE_TYPE1)]
(3) [LibararyClasses. ( A r c h ) . (Arch). (Arch).(MODULE_TYPE)]
(4) [LibararyClasses.common. ( M O D U L E T Y P E ) ] ( 5 ) [ L i b a r a r y C l a s s e s . (MODULE_TYPE)] (5) [LibararyClasses. (MODULETYPE)](5)[LibararyClasses.(Arch)]
(6) [LibararyClasses.common]
- A r c h 和 Arch和 Arch和MODULE_TYPE是可选项。不使用表示通用。
- $Arch表示体系结构,可以是下列值之一:IA32,X64,IPF,EBC,ARM,common。common表示对所有体系结构有效。
- M O D U L E T Y P E 表 示 模 块 的 类 别 , 块 内 列 出 的 库 只 能 提 供 MODULE_TYPE表示模块的类别,块内列出的库只能提供 MODULETYPE表示模块的类别,块内列出的库只能提供(MODULE_TYPE)类别的模块连接。它可以是下列值:SEC、PEI_CORE、PEIM、DEX_CORE、DEX_SAL_DRIVER、BASE、DXE_SMM_DRIVER、DXE_DRIVER、\ DXE_RUNTIME_DRIVER、UEFI_DRIVER、UEFI_APPLICATION、USER_DEFINED。
Note: [Components]中的模块在寻找所需要的库时,按上面6种方法以优先级递减,以此寻找。如果找到,继续编译;如果没找到,则库会报无法找到的错误。
另外:
该块中支持!include 包含其他inc文件。但是,所包含的inc文件必须有完整的[LibararyClasses]
实例:
[LibraryClasses.common]
UefiApplicationEntryPoint|MdePkg/Library/UefiApplicationEntryPoint/UefiApplicationEntryPoint.inf
···
[LibraryClasses.ARM]
NULL|ArmPkg/Library/CompilerIntrinsicsLib/CompilerIntrinsicsLib.inf
···
[LibraryClasses.AARCH64]
NULL|ArmPkg/Library/CompilerIntrinsicsLib/CompilerIntrinsicsLib.inf
···
3.[Components]
配置该包中哪些模块参与编译。该区块内定义的模块都会被build工具编译并生成.efi文件。
语法:
[Components]
path/Exectuables.inf
或者
[Components]
path/Exectuables.inf{
< LibraryClasses> # 做嵌套
LibraryName | Path/LibraryName.inf
< BuildOptions> # 嵌套块
#字块中还可以包含< Pcds*>
}
注意,path使用相对于EDK2根目录的相对路径。另外,也可以通过DEFINE来定义定INF文件的路径,比如:
[Components]
DEFINE XX_PATH=/home/xx/edk2/...../
$(XX_PATH)/xx.inf
4.[Pcds]
PCD的全称是Platform Configuration Database,用于定义平台配置数据。它的目的是在不改动.inf文件的情况下完成对平台的配置。也就是说,我们在自己的平台上,可以对某个模块的inf定制化配置。
关于PCD我们在其他的专题再详细介绍。
在DSC文件的Pcds]块中,PCD变量的格式如下:
[Pcds]
TokenSpaceGuidCName.PcdCName|DefaultValue|DatumType|Token
其中,[Pcds]块有下述类型(主要区别体现在访问方式):
- FeatureFlag PCD:它最终返回的是TRUE或者FALSE,用于判断条件中。
- PatchableInModule PCD:这种变量的值可以在编译的时候确定,这个不算特别,特别的是它可以在编译完成的二进制文件上通过工具来修改值。
- FixedAtBuild PCD:静态值,在编译的时候确定,整个UEFI阶段不可变。
- Dynamic PCD:前面的三种类型可以认为是静态的PCD,而这里以及之后的是动态的PCD;它的特点是可以在UEFI运行的过程中通过Set宏来修改值;在《edk-ii-build-specification.pdf》中有说明该种类型的PCD必须在DSC中在列一遍,但是实际使用似乎并不是必须的。
- DynamicEx PCD:跟Dynamic PCD类似,算是加强版,使用宏PcdGetEx/PcdSetEx来访问变量。
TokenSpaceGuidCName是一个GUID,PcdCName是一个变量名,两者合起来构成了唯一的PCD变量。TokenSpaceGuidCName在DEC文件中声明并赋值。
[Guids]
gUefiOemPkgTokenSpaceGuid = { 0x6bb4bec8, 0x23d8, 0x40af, { 0x8f, 0x24, 0x99, 0xe7, 0xac,
DefaultValue是PCD的默认值,DatumType是PCD的类型,Token是一个32位的整型,在DEC中每个PCD都有一个独一无二的Token。
在DSC中设置PCD的值。这个设置并不是必须的,如果没有整个设置,就使用在DEC中默认的值。
# // OEM defined dynamic PCD.
gUefiOemPkgTokenSpaceGuid.PcdOemVersion|0x1234567
该Pcd如果在模块中用到,需要在模块INF文件的[Pcds]中声明。
DSC中,各类[Pcds]的声明示例:
[PcdsDynamic]
gUefiOemPkgTokenSpaceGuid.PcdOemVersion|0xFFFFFFFF|UINT32|0x40000001
[PcdsFixedAtBuild]
gUefiOemPkgTokenSpaceGuid.PcdTestVar1|0xA5|UINT8|0x20000001
gUefiOemPkgTokenSpaceGuid.PcdTestVar2|0xA5A5|UINT16|0x20000002
[PcdsFeatureFlag]
gUefiOemPkgTokenSpaceGuid.PcdTestFeatureVar|FALSE|BOOLEAN|0x30000001
5.[BuildOptions]
**[BuildOptions]**指定编译器相关的参数,它会覆盖为模块准备的默认参数。如果是为了替换编译参数,则可以使用“==”;如果为了增加编译参数,可以使用“=”。其典型语法格式如下:
[BuildOptions]
$(FAMILY):$(TARGET)_$(TAGNAME)_$(ARCH)_$(TOOLCODE)_FLAGS[=|==] 编译参数
例如,
[BuildOptions]
*_*_*_PLATFORM_FLAGS = -march=armv8-a -DCONFIG_DEBUG=true
[BuildOptions.common.EDKII.DXE_SMM_DRIVER, BuildOptions.common.EDKII.SMM_CORE]
GCC:*_*_*_DLINK_FLAGS = -z common-page-size=0x1000
XCODE:*_*_*_DLINK_FLAGS = -seg1addr 0x1000 -segalign 0x1000
XCODE:*_*_*_MTOC_FLAGS = -align 0x1000
CLANGPDB:*_*_*_DLINK_FLAGS = /ALIGN:4096
FAMILY,TARGET,TAGNAME,ARCH,TOOLCODE,这5个变量是由Conf/tools_def.txt中定义的。
DEC文件定义了公开的数据和接口,供其他模块使用。每个包只要一个DEC文件,一般和DSC文件成对出现。文件中的语法,和DSC文件中对应的基本相同。它包含了必需区块[Defines]以及可选区块[Includes]、[LibraryClasses]、[Guids]、[Protocol]、[Ppis]和[PCD]几个部分。
1.[Defines]块
[Defines]块用于提供Package的名称、GUID、版本号等信息。语法和DSC文件相同。
## /EDK2/MdePkg/MdePkg.dec
[Defines]
DEC_SPECIFICATION = 0x00010005 #规范的版本号
PACKAGE_NAME = MdePkg #包名称
PACKAGE_UNI_FILE = MdePkg.uni #包使用的字符串资源文件
PACKAGE_GUID = 1E73767F-8F52-4603-AEB4-F29B510B6766 #包GUID
PACKAGE_VERSION = 1.06 #包版本号
2.[Includes]块
列出本Package提供的头文件所在的目录。DSC文件中无此块。
格式:
[Includes.$(Arch)]
path
头文件的路劲为相对路径,其根目录为DEC文件所在目录。
指定架构的时候,可以使用“private”限定符,限定包含的头文件只能在本包中的模块使用。例如,
[Includes.common]
include
[Includes.common.Private]
xx/include
[Includes.AARCH64.Private]
xx/arm64/include
Note:
不带private标志的项不能和带Private标志的项混合。比如,下列情况出错
[Includes.common, Includes.AARCH64.Private]
include
同一文件目录,不能同时指定带private和不带private标志的项。例如,
[Includes.common.Private]
xx/include
[Includes.AARCH64.Private]
xx/include
3.[LibraryClasses]
对外提供的库都会提供头文件,这些头文件位于包下的Include/Library目录下。这个块用来规定库和头文件的对应关系。语法格式如下:
[LibraryClasses.$(Arch)]
LibraryName | Path/LibraryHeader.h
例如,
## /EDK2/MdePkg/MdePkg.dec
[LibraryClasses]
UefiUsbLib|Include/Library/UefiUsbLib.h
UefiRuntimeServicesTableLib|Include/Library/UefiRuntimeServicesTableLib.h
···
4.[Guids]块
在Package中源文件,我们会定义一个或几个GUID
## /EDK2/MdePkg/Include/Guid/Gpt.h
extern EFI_GUID gEfiPartTypeUnusedGuid;
extern EFI_GUID gEfiPartTypeSystemPartGuid;
extern EFI_GUID gEfiPartTypeLegacyMbrGuid;
这些定义只是声明,常量真正定义在AutoGen.c中,它的值定义在DEC文件的[Guids]区块。
## /EDK2/MdePkg/MdePkg.dec
[Guids]
## Include/Guid/Gpt.h
gEfiPartTypeLegacyMbrGuid = { 0x024DEE41, 0x33E7, 0x11D3, { 0x9D, 0x69, 0x00, 0x08, 0xC7, 0x81, 0xF3, 0x9F }}
## Include/Guid/Gpt.h
gEfiPartTypeSystemPartGuid = { 0xC12A7328, 0xF81F, 0x11D2, { 0xBA, 0x4B, 0x00, 0xA0, 0xC9, 0x3E, 0xC9, 0x3B }}
## Include/Guid/Gpt.h
gEfiPartTypeUnusedGuid = { 0x00000000, 0x0000, 0x0000, { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }}
当在模块工程文件INF的[Guids]中引用这些Guid时,这些值就会复制到AutoGen.c中。
5.[Protocols]块
与GUIDS类似,在包中我们会自定义一些协议。例如,在MdePkg/Include/Protocol 目录下的BlockIo.h定义了BlockIo Protocol。
extern EFI_GUID gEfiBlockIoProtocolGuid;
gEfiBlockIoProtocolGuid 的值就定义在MdePkg.dec 的[Protocols]块内
## Include/Protocol/BlockIo.h
gEfiBlockIoProtocolGuid = { 0x964E5B21, 0x6459, 0x11D2, { 0x8E, 0x39, 0x00, 0xA0, 0xC9, 0x69, 0x72, 0x3B }}
6.其它块
INF文件是模块的工程文件,其后缀名为.inf,描述了模块的属性。其内容包括模块名称、模块类型、GUID,以及模块的build规则,包括模块由哪些源文件组成,提供了什么、依赖什么库等信息。
对ODM厂商(第三方开发者)而言,可以针对自家设备发布二进制形式的模块,不必提供源代码。
1.[Defines]
与DSC文件的[Defines]类似,定义各种变量,供后续编译中使用。
如下是必须定义的变量,其余是可选的。
例如: helloword 应用程序模块的INF文件Defines
[Defines]
INF_VERSION = 0x00010005
BASE_NAME = main #输出文件的名字为 main.efi
FILE_GUID = 6987936E-ED34-ffdb-AE97-1FA5E4ED2117
MODULE_TYPE = UEFI_APPLICATION #模块类型
VERSION_STRING = 1.0
ENTRY_POINT = UefiMain #入口函数
2.Sources
列出文件内所有的源文件和资源文件。
[Sources]
a.c
[Sources.ARM]
b.c
另外,还可以对源文件指定编译工具链。即只要使用此工具链时,此源文件才会被编译。
[Sources.ARM]
Gicv3/Arm/ArmGicV3.S |Gcc
Gicv3/Arm/ArmGicV3.asm |RVCT
3.Packages
列出本模块所引用的包的DEC文件。如果source块内包含了源文件,必须将MdePkg/MdePkg.dec放在首行。
[Packages]
MdePkg/MdePkg.dec
MdeModulePkg/MdeModulePkg.dec
4.LibraryClasses
列出本模块需要链接的库。应用工程文件必须包含UefiApplicationEntryPoint库,驱动文件必须包含UEFIDriverEntryPoint库。
其语法格式如下:
[LibraryClasses.$(Arch)]
LibraryName [|FeatureFlagExpression]
如果FeatureFlagExpression为TRUE,必须保证库已经存在。库的编译在DSC文件指定;如果FeatureFlagExpression为FALSE,编译工具会忽略添加的该库。
#要链接的库
[LibraryClasses]
UefiApplicationEntryPoint
UefiLib
5.Protocols
[Protocols]列出了本模块使用的协议。在INF中列出的时协议的GUID,通过edk2工具,GUID会被输出到AutoGen.c中。
语法格式如下:
[Protocols]
gEfiPciRootBridgeIoProtocolGuid ## CONSUMES
6.Depex
本模块要运行入口函数,需要哪些协议被安装。主要是列出协议的GUID。例如,
[Depex]
gEfiPciRootBridgeIoProtocolGuid
[Depex.common.DXE_DRIVER, Depex.common.DXE_RUNTIME_DRIVER, Depex.common.DXE_SAL_DRIVER, Depex.common.DXE_SMM_DRIVER]
gEfiPciRootBridgeIoProtocolGuid
需要注意的是,多个GUID需要使用AND连接。
7.BuildOptions和Pcd。
与DSC文件完全一样。
关于FDF文件,更细节的地方请查看手册《edk2-FdfSpecification-release-1.28.01.pdf》
FDF的全称是Flash Description File。用于描述(定义)FD镜像文件的内容和布局信息。
前面介绍过,在UEFI Build流程的最后一个阶段,是生成一个可以固化在Flash的镜像文件FD。FD文件是Build工具根据FDF文件的描述生成的。下面详细介绍一下FD文件中的内容都是怎么存放的。
固件文件FD 的内部组织结构如图所示。
为方便理解,先梳理下FD、FV、FFS三者之间的关系:
1.一个FD(Flash Device binary image)文件,就是最终的二进制镜像,由多个固件卷文件(FV)和代码数据段,日志段等内容组成。
2.一个FV(Firmware Volumes),可以看作FD的一个段。每一个FV段都有一个特定的功能,比如FVMAIN通常包含的是DXE、BDS阶段的代码,它通常是被压缩过的;FV Recovery包含SEC和PEI阶段的代码,未经压缩,以XIP方式执行。
3.每一个FV 包含多个FFS(Firmware File System)文件。FFS是FV内部的一个结构,用来表示各个Firmware的组成。一个FFS文件 由一个或者多个.efi 文件封装组成。
下面是FD创建的一个示意图:
每一个模块的源码经过编译生成一个.efi文件。Build工具按照配置文件指定的规则将一个或多个.efi文件封装成FFS文件,FFS文件又被封装到FV文件中,多个FV 最后组成了FD文件。
FD文件烧写到ROM后,首先知道secFv的位置,然后通过SecFv找到代码段的起始入口开始执行。执行到SEC后期,会将PeiFv的位置传递过去,从PeiFv的位置找到PeiCore的File,也就是PeiCoreImage,然后再从Image中找到PE32 的Section,并找到Entry point 开始执行PeiCore的代码。剩下的段就是经过压缩的了。
注释使用#;
赋值使用SET,而使用=表示的是TOKEN,是一种特殊的类似与常量的值;
可以使用整型,布尔值,EFI_GUID等值
$()获取DEFINE语句定义的宏的值;
SET语句:对PCD进行赋值,如下所示
SET gPlatformModuleTokenSpaceGuid.PcdFlashAreaBaseAddress = $(FLASH_AREA_BASE_ADDRESS)
SET gPlatformModuleTokenSpaceGuid.PcdFlashAreaSize = $(FLASH_AREA_SIZE)
DEFINE语句:定义一个宏,如下所示
定义的宏可以通过$()来访问。注意DSC文件中也会使用DEFINE语句,且定义的宏可以在FDF文件中访问。
DEFINE DEBUG_ENABLE_OUTPUT = FALSE # Set to TRUE to enable debug output
DEFINE DEBUG_PRINT_ERROR_LEVEL = 0x80000040 # Flags to control amount of debug output
DEFINE DEBUG_PROPERTY_MASK = 0
!if语句。判断语句,为TRUE时才包含其内部的组件,其语法如下:
!if $(MACRO)
或者
!if $(MACRO) == "Literal String"
或者
!if $(MACROALPHA) == $(MACROBETA)
或者
!if $(MACRONUM) == 数字
或者
!if $(MACROBOOL) == 布尔值
注意需要与!endif语句一起使用,中间也可以有!else语句。下面是例子:
!if $(SECURE_BOOT_ENABLE)
# Signature: gEfiAuthenticatedVariableGuid = { 0xaaf32c78, 0x947b, 0x439a, { 0xa1, 0x80, 0x2e, 0x14, 0x4e, 0xc3, 0x77, 0x92 } }
0x78, 0x2c, 0xf3, 0xaa, 0x7b, 0x94, 0x9a, 0x43,
0xa1, 0x80, 0x2e, 0x14, 0x4e, 0xc3, 0x77, 0x92,
!else
# Signature: gEfiVariableGuid = { 0xddcf3616, 0x3275, 0x4164, { 0x98, 0xb6, 0xfe, 0x85, 0x70, 0x7f, 0xfe, 0x7d }}
0x16, 0x36, 0xcf, 0xdd, 0x75, 0x32, 0x64, 0x41,
0x98, 0xb6, 0xfe, 0x85, 0x70, 0x7f, 0xfe, 0x7d,
!endif
其它类似的还有!ifdef语句和!ifndef语句。
以上是比较通用的语句,还有一些需要在特定的Section中使用的语句将在之后介绍。
常用的Section。Section的大致格式如下:
[oo.xx.zz]
上述的代码描述中,oo是必选的,而xx、zz等需要根据oo的值来确定是否存在以及具体是什么。
Section是FDF文件中最重要的组件,因为它们将FDF文件分成了若干个部分,各个部分由不同的内容构成,最终组成整个FD。
下面就介绍这些常用的Section关键字。
可选的块。用来跟踪FDF文件的版本、DEFINE全局宏以及设定PCD的值,其语法格式如下:
[Defines]
Name=value
DEFINE MACRO=Value
关于FD这种Section关键字在之前也已经提到,它表示了一个完整的image布局。一个FDF文件里面可以有多个FD。
[FD]关键字中的FD之后还可以接一个后缀,格式如下:
[FD.FdUiName]
这里的FdUiName表示FD的名称,可以是随意的值。
当FDF文件中只存在一个[FD] Section的时候,这个FdUiName是可选的,如果不选,则使用定义在[Defines]中的PLATFORM_NAME作为名称。
例如,S9863a1h10.fdf中:
[FD.EDK2]
BaseAddress = 0x9F000000|gArmTokenSpaceGuid.PcdFdBaseAddress # The base address of the Firmware in NOR Flash.
Size = 0x000F0000|gArmTokenSpaceGuid.PcdFdSize # The size in bytes of the FLASH Device
ErasePolarity = 1
只有一个FD段,[FD.EDK2],因此只编译一个image—EDK2.bin。
[FD]中的声明包括如下内容
1) 令牌申明(TOKEN Statements)。 其语法格式如下:
Token = Value [| PcdName]
其中,Token可以为如下变量:
2) 定义声明(DEFINE Statements)。 前面已经介绍,其语法格式如下:
DEFINE MACRO=PATH
这里用来定义宏的声明,所定义的宏在整个FDF文件都有效。使用的时候可以通过$(MACRO)来引用。
前面我们在介绍DSC文件的时候提到过,在DSC文件的[Defines]中定义的变量和宏,可以在FDF文件中引用。
示例如下:
DEFINE FV_DIR = $(OUT_DIR)/$(TARGET)_$(TOOL_CHAIN_TAG)/$(ARCH)
DEFINE MDE_MOD_TSPG = gEfiMdeModulePkgTokenSpaceGuid
DEFINE FV_HDR_SIZE = 0x66
3)设置声明(SET Statements)。前面已经介绍过,其语法格式如下:
SET PcdName = Value
设置声明一般用来设置PCD变量的值,如
SET gPlatformModuleTokenSpaceGuid.PcdFlashAreaBaseAddress = $(FLASH_AREA_BASE_ADDRESS)
SET gPlatformModuleTokenSpaceGuid.PcdFlashAreaSize = $(FLASH_AREA_SIZE)
4)包含INF文件。[FD]中还可以直接包含INF文件,格式如下:“INF [Options] PathAndInfFileName”。目前没有发现哪个Pkg这么使用。
5)区域布局(Region Layout)。 用来指明各种区域类型的数据在FD中的布局,其语法格式如下。
Offset|Size
[TokenSpaceGuidCName.PcdOffsetCName | TokenSpaceGuidCName.PcdSizeCName] ?
[RegionType] ?
上述代码的含义是:在FD中开辟一块空间,用来放置[RegionType] 所指明的内容。
(1) Offset和Size, 是这段空间的对于整个FD的偏移和大小。
(2) Pcd, 是可选的,其实就是初始化了PCD来表示这段空间偏移和大小供后续使用,并不是必须的,它更像是C语言中的宏,你只需要设置一次,就可以在FDF文件的各处使用。
(3) RegionType, 表示这段空间的类型,可以是FV(Firmware Volume)、DATA(裸数据)、FILE(文件)、INF(INF文件)和CAPSULE(更新固件的image)等内容。RegionType不是必须的,如果没用定义则表示这段空间不能去动它,它是有其它用处的,会有其它的机制(比如非易失日志)用到。
I. 开辟FV (Firmware Volume)类型的空间
例子中使用了FV,采用“FV = UiFvName”的形式。这里的UiFvName是在[FV]这个Section中定义的,用于控制将哪个[FV]段编译进[FD]定义的image。
[FD.EDK2]
0x00000000|0x000F0000
gArmTokenSpaceGuid.PcdFvBaseAddress|gArmTokenSpaceGuid.PcdFvSize
FV = FVMAIN_COMPACT
$(FVMAIN_SIZE)|$(SECFV_SIZE)
FV = SECFV
表示从0地址开始开辟0x000F0000个字节的空间,使用gArmTokenSpaceGuid.PcdFvBaseAddress和gArmTokenSpaceGuid.PcdFvSize标识这块空间的而地址和size,供其他模块使用。定义这块空间用来放FV数据。
II. 开辟DATA(裸数据)类型的空间
使用格式如“DATA = {xx}”,xx可以是16进制的字节数组,下面是一个例子:
0x0058e000|0x00002000
gEmulatorPkgTokenSpaceGuid.PcdEmuFlashNvStorageFtwWorkingBase|gEfiMdeModulePkgTokenSpaceGuid.PcdFlashNvStorageFtwWorkingSize
#NV_FTW_WORKING
DATA = {
# EFI_FAULT_TOLERANT_WORKING_BLOCK_HEADER->Signature = gEdkiiWorkingBlockSignatureGuid =
# { 0x9e58292b, 0x7c68, 0x497d, { 0xa0, 0xce, 0x65, 0x0, 0xfd, 0x9f, 0x1b, 0x95 }}
0x2b, 0x29, 0x58, 0x9e, 0x68, 0x7c, 0x7d, 0x49,
0xa0, 0xce, 0x65, 0x0, 0xfd, 0x9f, 0x1b, 0x95,
# WriteQueueSize: UINT64
0xE0, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
}
xx还可以是一个!include语句,用来包含外部的数据,下面是一个例子:
0x0CA000 | 0x002000
gEfiMyTokenSpaceGuid.PcdFlashNvStorageBase | gEfiMyTokenSpaceGuid.PcdFlashNvStorageSiz
e
DATA = {
!include NvStoreInit.txt
}
III. 开辟FILE(文件)类型的空间
FILE指向一个二进制文件,生成FD的之后这个二进制文件就会被包含进来,使用格式如“FILE = $(FILE_DIR)/Filename.bin”
例如树莓派的FDF文件中,将BL1、BL2 、BL31 3个启动阶段的bootloader image连同UEFI本image都打包到一个FD文件。
[Defines]
...
DEFINE TFA_BUILD_BL1 = Platform/RaspberryPi/$(PLATFORM_NAME)/TrustedFirmware/bl1.bin
DEFINE TFA_BUILD_FIP = Platform/RaspberryPi/$(PLATFORM_NAME)/TrustedFirmware/fip.bin
[FD.RPI_EFI]
# ATF primary boot image #
0x00000000|0x00010000
FILE = $(TFA_BUILD_BL1)
# ATF secondary boot image in FIP format (BL2 + BL31)
0x00020000|0x00010000
FILE = $(TFA_BUILD_FIP)
# UEFI image
0x00030000|0x001a0000
gArmTokenSpaceGuid.PcdFvBaseAddress|gArmTokenSpaceGuid.PcdFvSize
FV = FVMAIN_COMPACT
IV. 使用CAPSULE(更新固件的image)类型空间
使用格式如“CAPSULE = UiCapsuleName”。其中的UiCapsuleName是[Capsule] Section的名称。目前在代码中没有看到具体的使用。
FV是FD固件的逻辑区块,相当于FD的一个分区。FV的主要作用就是列出要将哪些文件或信息放在这个Fv。
它的格式如下:
[FV.UiFvName]
UiFvName是自定义的名称,在FD中会用到,它在文件中必须是唯一的。
FV是可以嵌套的,FV中也可以通过UiFvName来包含另外一个FV。
下面介绍[FV] Section中包含的内容。
1)TOKEN
[FV]的最开始是几个TOKEN。如下是一个例子:
BlockSize = $(FLASH_BLOCK_SIZE)
FvAlignment = 16
ERASE_POLARITY = 1
MEMORY_MAPPED = TRUE
STICKY_WRITE = TRUE
LOCK_CAP = TRUE
LOCK_STATUS = TRUE
WRITE_DISABLED_CAP = TRUE
WRITE_ENABLED_CAP = TRUE
WRITE_STATUS = TRUE
WRITE_LOCK_CAP = TRUE
WRITE_LOCK_STATUS = TRUE
READ_DISABLED_CAP = TRUE
READ_ENABLED_CAP = TRUE
READ_STATUS = TRUE
READ_LOCK_CAP = TRUE
READ_LOCK_STATUS = TRUE
2)DEFINE声明
可选。和[FD]中一样。
3)SET声明
可选。和[FD]中一样。
4)Block声明
Block声明用来定义FV所需要的Block size和Block数量。这类FV不放在Flash物理分区,而是放在其他地方。
例如:
BLOCK_SIZE = VALUE
NUM_BLOCKS = VALUE
5)文件声明
可选。[FV]中可以直接包含文件,使用的是FILE语句。主要目的是:直接将FFS文件包含进FV。
其格式如下:
FILE Type $(NAMED_GUID) [Options] FileName
或者
FILE Type $(NAMED_GUID) [Options] {
SECTION SECTION_TYPE = FileName
SECTION SECTION_TYPE = FileName
}
(1)Type指的是FV 文件 Types。
常用的FV文件类型有:
(2)NAMED_GUID 为所要包含的文件设定一个ID,我们可以在代码中通过这个GUID获取文件。
$(NAMED_GUID)通常是由INF文件的[define]节FILE_GUID元素决定的。
(3)Options有如下可用的值:Fixed、Alignment和Checksum等,这里不一一介绍。
例如:
FILE FREEFORM = PCD(gEfiIntelFrameworkModulePkgTokenSpaceGuid.PcdLogoFile) {
SECTION RAW = MdeModulePkg/Logo/Logo.bmp
}
例如:
#Encapsulation - Compress
FILE FOO = 12345678-0000-AAAA-FFFF-0123ABCD12BD {
SECTION COMPRESS {
SECTION PE32 = $(WORKSPACE)/EdkModulePkg/Core/Dxe/DxeMain.inf
SECTION VERSION = "1.2.3"
}
}
# Encapsulation - GUIDED
FILE FV_IMAGE = 87654321-FFFF-BBBB-2222-9874561230AB {
SECTION GUIDED gEfiTianoCompressionScheme {
SECTION PE32 = $(WORKSPACE)/EdkModulePkg/Core/Dxe/DxeMain.inf
}
}
# LEAF Section
FILE DXE_CORE = B5596C75-37A2-4b69-B40B-72ABD6DD8708 {
SECTION VERSION $(BUILD_DIR)/$(ARCH)/D6A2CB7F-6A18-4E2F-B43B-9920A733700A-DxeMain.ver
}
6) INF声明
INF 块主要是在FV中声明一个模块或组件,通过其INF文件。
解析工具将扫描INF文件以确定组件或模块的类型。组件或模块类型用于引用FDF文件中其他地方定义的标准规则。因此,在此声明INF是为了引用rule块中定义的东西。
格式如下:
INF [Options] PathAndInfFileName
其中PathAndInfFileName就是一个普通的inf文件,Options是可选项。常见内容如下:
根据文档:
使用INF语句,使得构建工具基于INF 模块的MODULE_TYPE和内容,隐式的构建带有EFI_FV_FILETYPE 的FFS文件。
例如,在FV中指定以下sections将生成带有EFI_FV_FILETYPE_DRIVER的FFS文件,该文件包含三个section: EFI_SECTION_PE32和一个EFI_SECTION_VERSION和一个EFI_SECTION_DXE_DEPEX。虽然在INF中没有定义version文件,但它由VERSION选项指定;并且在INF文件的源文件中指定了一个依赖文件列表。
DEFINE MMP_U_MT = MdeModulePkg/Universal/MemoryTest
INF VERSION = "1.1" $(MMP_U_MT)/NullMemoryTestDxe/NullMemoryTestDxe.inf
INF文件使用示例:
DEFINE IFMP = IntelFrameworkModulePkg
INF USE = IA32 $(EDK_SOURCE)/Sample/Universal/Network/Ip4/Dxe/Ip4.inf
INF $(EDK_SOURCE)/Sample/Universal/Network/Ip4Config/Dxe/Ip4Config.inf
INF RULE_OVERRIDE = MyCompress $(IFMP)/Bus/Pci/IdeBusDxe/IdeBusDxe.inf
7) 关联规则设定(APRIORI Scoping)
在一个FV中,可以创建一个APRIORI文件,它由多个模块的GUID组成。编译后,这些模块的.efi文件会按照顺序打包进该APRIORI文件。一个FV,只允许出现一个PEI 和一个 DXE 。总的来说,主要用来指定INF的执行顺序。
FV嵌套是允许的,Apriori文件仅限于指定文件,而不包括位于FV映像范围内的FVs。作用域在大括号{}范围内有效。
APRIORI DXE {
INF MdeModulePkg/Universal/DevicePathDxe/DevicePathDxe.inf
INF MdeModulePkg/Universal/PCD/Dxe/Pcd.inf
!if $(SMM_REQUIRE) == FALSE
INF OvmfPkg/QemuFlashFvbServicesRuntimeDxe/FvbServicesRuntimeDxe.inf
!endif
}
如上例子中3个INF按照顺序执行。
这个Section用来描述FFS文件的构成,FFS文件就是通过INF文件编译得到,然后组成二进制。规则与[FV]节的模块INF类型一起使用,定义如何通过一个给定的INF 文件来生成FFS文件。
如果不通过INF显示指定rule, 构建系统会使用默认的规则隐式的创建FFS。隐式规则遵循 PI Specification and UEFI Specification.
规则可以有多个变种,如:
[Rule.ARCH.MODULE_TYPE.TEMPLATE_NAME]
下面是一些具体例子:
[Rule.Common.SEC]
FILE SEC = $(NAMED_GUID) {
PE32 PE32 $(INF_OUTPUT)/$(MODULE_NAME).efi
UI STRING ="$(MODULE_NAME)" Optional
VERSION STRING ="$(INF_VERSION)" Optional BUILD_NUM=$(BUILD_NUMBER)
}
[Rule.Common.PEI_CORE]
FILE PEI_CORE = $(NAMED_GUID) {
PE32 PE32 Align=Auto $(INF_OUTPUT)/$(MODULE_NAME).efi
UI STRING ="$(MODULE_NAME)" Optional
VERSION STRING ="$(INF_VERSION)" Optional BUILD_NUM=$(BUILD_NUMBER)
}
[Rule.Common.PEIM]
FILE PEIM = $(NAMED_GUID) {
PEI_DEPEX PEI_DEPEX Optional $(INF_OUTPUT)/$(MODULE_NAME).depex
PE32 PE32 Align=Auto $(INF_OUTPUT)/$(MODULE_NAME).efi
UI STRING="$(MODULE_NAME)" Optional
VERSION STRING="$(INF_VERSION)" Optional BUILD_NUM=$(BUILD_NUMBER)
c
}
其内部是一个FILE语句,它跟之前在FV中使用的FILE语句结构是一样的。
此外还有[VTF]、[OptionRom]、[Capsule]等Section,因为用的不多,这里不再介绍。
此文档只是一个使用文档,主要介绍FDF、DSC、INF、DEC几种配置文件的写法。读者可能对这些文件的具体作用,以及它们之间的关系,如何影响整个编译过程一头雾水。下一篇将会对中提到的FD、FV、FFS、efi等文件的关系,以及如何作用于build,做一个梳理。