本文将要介绍的是在UEFI中经常使用到的一种特殊的数据结构PCD,参考内容如下:
PCD的全称是Platform Configuration Database,它是一个存放了UEFI下可访问数据的数据库。
它的特点是可以在UEFI存在的大部分时间内访问,在UEFI实战——在库中使用全局变量中就曾经介绍过通过PCD来传递全局变量。这里说是大部分时间内,那是因为在诸如SEC阶段,以及PEI、DXE阶段的早期,某些PCD相关的模块还没有加载起来之前,这些类型的PCD还是不能访问的。
PCD变量的格式如下:
TokenSpaceGuidCName.PcdCName
TokenSpaceGuidCName是一个GUID,PcdCName是一个变量名,两者合起来构成了唯一的PCD变量。
PCD有下述的类型(主要的不同是在访问的方式):
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来访问变量;
需要注意的是上面的类型并不是在一个SPEC中定义的,前面的4中是满足EDKII规范,而最后一个满足的是PI规范,这个对使用的影响还不是很确定。
上述说的PCD是按访问的方式来分类的。
另外还需要关注的是PCD变量的值可能的类型,有BOOLEAN类型,整型(UINT8,UINT16等)以及VOID *类型。
需要注意的是BOOLEAN这种类型FixedAtBuild PCD和FeatureFlag PCD中都可以存在,但是两者实际上是不同的,访问使用的宏也是不同的,前者是FixedPcdGetBool()而后者是FeaturePcdGet()。但是后面会看到,从实现上来讲两者是一致的......
下面先介绍一个简单的示例。
1. 在dec文件中声明PCD的基本信息,如下所示:
[PcdsDynamic]
gUefiOemPkgTokenSpaceGuid.PcdOemVersion|0xFFFFFFFF|UINT32|0x40000001
在《edk-ii-dec-specification.pdf》中有说明上述内容的意义。
它分为两个部分,第一个部分[PcdsDynamic]声明了PCD的类型,PCD的多种类型后续会一一介绍。
第二部分的内容如下:
#加注释部分这里没有用到;
TokeSpaceGuidCname是一个GUID,也是在DEC中声明的,如下所示:
[Guids]
# // {6BB4BEC8-23D8-40AF-8F24-99E7AC8601FD}
gUefiOemPkgTokenSpaceGuid = { 0x6bb4bec8, 0x23d8, 0x40af, { 0x8f, 0x24, 0x99, 0xe7, 0xac, 0x86, 0x01, 0xfd } }
PcdCname是PCD的名称,后续使用的时候就需要用到它。
DefaultValue是PCD的默认值,DatumType是PCD的类型,Token是一个32位的整型,在DEC中每个PCD都有一个独一无二的Token。
2. 在DSC中设置PCD的值:
# // OEM defined dynamic PCD.
gUefiOemPkgTokenSpaceGuid.PcdOemVersion|0x12345678
这个设置并不是必须的,如果没有整个设置,就使用在DEC中默认的值。
3. 使用:
DEBUG ((EFI_D_ERROR, "[beni]Version: 0x%x.\n", PcdGet32(PcdOemVersion)));
要使用PcdGet32这种的宏,需要包含PcdLib这个库。
还需要在inf中包含该PCD:
[Pcd]
gUefiOemPkgTokenSpaceGuid.PcdOemVersion
以上的代码就会打印出[beni]Version: 0x12345678。
该部分代码可以在https://gitee.com/jiangwei0512/vUDK2017找到。
下面说明各种PCD的实现。
不同的PCD类型实现方式有差异,这里首先介绍最简单的FixedAtBuild PCD。
下面是一个例子:
1. 首先声明一系列的PCD:
[PcdsFixedAtBuild]
gUefiOemPkgTokenSpaceGuid.PcdTestVar1|0xA5|UINT8|0x20000001
gUefiOemPkgTokenSpaceGuid.PcdTestVar2|0xA5A5|UINT16|0x20000002
gUefiOemPkgTokenSpaceGuid.PcdTestVar3|0xA5A5A5A5|UINT32|0x20000003
gUefiOemPkgTokenSpaceGuid.PcdTestVar4|0xA5A5A5A5A5A5A5A5|UINT64|0x20000004
2. 然后在一个模块中使用:
EFI_STATUS
EFIAPI
PcdTestDriverEntry (
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
)
{
EFI_STATUS Status = EFI_SUCCESS;
DEBUG ((EFI_D_ERROR, "[beni]PcdTestDriverEntry Start.\n"));
DEBUG ((EFI_D_ERROR, "[beni]PcdTestVar1: 0x%x.\n", PcdGet8 (PcdTestVar1)));
DEBUG ((EFI_D_ERROR, "[beni]PcdTestVar2: 0x%x.\n", PcdGet16 (PcdTestVar2)));
DEBUG ((EFI_D_ERROR, "[beni]PcdTestVar3: 0x%x.\n", PcdGet32 (PcdTestVar3)));
DEBUG ((EFI_D_ERROR, "[beni]PcdTestVar4: 0x%x.\n", PcdGet64 (PcdTestVar4)));
DEBUG ((EFI_D_ERROR, "[beni]PcdTestDriverEntry End.\n"));
return Status;
}
3. 编译后查看该模块编译目录中的AutoGen.c文件,可以看到如下的内容:
// Definition of PCDs used in this module
GLOBAL_REMOVE_IF_UNREFERENCED const UINT8 _gPcd_FixedAtBuild_PcdTestVar1 = _PCD_VALUE_PcdTestVar1;
GLOBAL_REMOVE_IF_UNREFERENCED const UINT16 _gPcd_FixedAtBuild_PcdTestVar2 = _PCD_VALUE_PcdTestVar2;
GLOBAL_REMOVE_IF_UNREFERENCED const UINT32 _gPcd_FixedAtBuild_PcdTestVar3 = _PCD_VALUE_PcdTestVar3;
GLOBAL_REMOVE_IF_UNREFERENCED const UINT64 _gPcd_FixedAtBuild_PcdTestVar4 = _PCD_VALUE_PcdTestVar4;
4. 这里的_PCD_VALUE_PcdTestVarx的值在AutoGen.h中声明:
// Definition of PCDs used in this module
#define _PCD_TOKEN_PcdTestVar1 107U
#define _PCD_SIZE_PcdTestVar1 1
#define _PCD_GET_MODE_SIZE_PcdTestVar1 _PCD_SIZE_PcdTestVar1
#define _PCD_VALUE_PcdTestVar1 0xA5U
extern const UINT8 _gPcd_FixedAtBuild_PcdTestVar1;
#define _PCD_GET_MODE_8_PcdTestVar1 _gPcd_FixedAtBuild_PcdTestVar1
//#define _PCD_SET_MODE_8_PcdTestVar1 ASSERT(FALSE) // It is not allowed to set value for a FIXED_AT_BUILD PCD
#define _PCD_TOKEN_PcdTestVar2 108U
#define _PCD_SIZE_PcdTestVar2 2
#define _PCD_GET_MODE_SIZE_PcdTestVar2 _PCD_SIZE_PcdTestVar2
#define _PCD_VALUE_PcdTestVar2 0xA5A5U
extern const UINT16 _gPcd_FixedAtBuild_PcdTestVar2;
#define _PCD_GET_MODE_16_PcdTestVar2 _gPcd_FixedAtBuild_PcdTestVar2
//#define _PCD_SET_MODE_16_PcdTestVar2 ASSERT(FALSE) // It is not allowed to set value for a FIXED_AT_BUILD PCD
#define _PCD_TOKEN_PcdTestVar3 109U
#define _PCD_SIZE_PcdTestVar3 4
#define _PCD_GET_MODE_SIZE_PcdTestVar3 _PCD_SIZE_PcdTestVar3
#define _PCD_VALUE_PcdTestVar3 0xA5A5A5A5U
extern const UINT32 _gPcd_FixedAtBuild_PcdTestVar3;
#define _PCD_GET_MODE_32_PcdTestVar3 _gPcd_FixedAtBuild_PcdTestVar3
//#define _PCD_SET_MODE_32_PcdTestVar3 ASSERT(FALSE) // It is not allowed to set value for a FIXED_AT_BUILD PCD
#define _PCD_TOKEN_PcdTestVar4 110U
#define _PCD_SIZE_PcdTestVar4 8
#define _PCD_GET_MODE_SIZE_PcdTestVar4 _PCD_SIZE_PcdTestVar4
#define _PCD_VALUE_PcdTestVar4 0xA5A5A5A5A5A5A5A5ULL
extern const UINT64 _gPcd_FixedAtBuild_PcdTestVar4;
#define _PCD_GET_MODE_64_PcdTestVar4 _gPcd_FixedAtBuild_PcdTestVar4
//#define _PCD_SET_MODE_64_PcdTestVar4 ASSERT(FALSE) // It is not allowed to set value for a FIXED_AT_BUILD PCD
5. 再回到2中代码里面我们访问PCD的方式如下:
PcdGet8 (PcdTestVar1))
将它展开后的结果就是_PCD_GET_MODE_8_PcdTestVar1,它在上述的AutoGen.h就已经定义了,最终的值就是0xA5。
也就是说,对应FixedAtBuild PCD来说,它就是在编译的时候通过宏的方式生成的。
也因此它是固定不变的一个值。
FeatureFlag PCD跟FixedAtBuild是一样的,它相当于类型是BOOELAN的FixedAtBuild PCD,从下面的示例可以看出来:
1. 创建一个FeatureFlag PCD:
[PcdsFeatureFlag]
gUefiOemPkgTokenSpaceGuid.PcdTestFeatureVar|FALSE|BOOLEAN|0x30000001
2. 然后在模块中使用它:
DEBUG ((EFI_D_ERROR, "[beni]PcdTestBooleanVar: %d\n", FeaturePcdGet (PcdTestFeatureVar)));
DEBUG ((EFI_D_ERROR, "[beni]PcdTestBooleanVar: %d\n", PcdGetBool (PcdTestFeatureVar)));
3. 注意这里使用了两种方式来访问这个变量,查看它的AutoGen.c文件:
GLOBAL_REMOVE_IF_UNREFERENCED const BOOLEAN _gPcd_FixedAtBuild_PcdTestFeatureVar = _PCD_VALUE_PcdTestFeatureVar;
4. 再查看AutoGen.h文件:
#define _PCD_TOKEN_PcdTestFeatureVar 111U
#define _PCD_SIZE_PcdTestFeatureVar 1
#define _PCD_GET_MODE_SIZE_PcdTestFeatureVar _PCD_SIZE_PcdTestFeatureVar
#define _PCD_VALUE_PcdTestFeatureVar ((BOOLEAN)0U)
extern const BOOLEAN _gPcd_FixedAtBuild_PcdTestFeatureVar;
#define _PCD_GET_MODE_BOOL_PcdTestFeatureVar _gPcd_FixedAtBuild_PcdTestFeatureVar
//#define _PCD_SET_MODE_BOOL_PcdTestFeatureVar ASSERT(FALSE) // It is not allowed to set value for a FIXED_AT_BUILD PCD
5. 可以从前缀看出来,它其实就是一个FixedAtBuild PCD。
6. 察看FeaturePcdGet和PcdGetBool两个宏:
#define FeaturePcdGet(TokenName) _PCD_GET_MODE_BOOL_##TokenName
#define PcdGetBool(TokenName) _PCD_GET_MODE_BOOL_##TokenName
也可以看到,他们指向的是同一种东西。
下面介绍Dynamic PCD,首先看一个例子:
1. 创建一个Dynamic PCD(其实就是最开始的那个示例):
[PcdsDynamic]
gUefiOemPkgTokenSpaceGuid.PcdOemVersion|0xABCDDCBA|UINT32|0x40000001
2. 然后在模块中使用:
DEBUG ((EFI_D_ERROR, "[beni]PcdOemVersion: 0x%x\n", PcdGet32 (PcdOemVersion)));
3. 编译后查看AutoGen.c文件,发现在该文件中没有找到相关的内容;
4. 再查看AutoGen.h文件,可以看到如下内容:
#define _PCD_TOKEN_PcdOemVersion 13U
#define _PCD_GET_MODE_32_PcdOemVersion LibPcdGet32(_PCD_TOKEN_PcdOemVersion)
#define _PCD_GET_MODE_SIZE_PcdOemVersion LibPcdGetSize(_PCD_TOKEN_PcdOemVersion)
#define _PCD_SET_MODE_32_PcdOemVersion(Value) LibPcdSet32(_PCD_TOKEN_PcdOemVersion, (Value))
#define _PCD_SET_MODE_32_S_PcdOemVersion(Value) LibPcdSet32S(_PCD_TOKEN_PcdOemVersion, (Value))
这里有几个明显的区别:
1)AutoGen.c中没有了PCD变量的定义;
2)PCD访问方式变了,Dynamic对应的是函数;
3)Set函数出现了;
关于PCD库函数的实现,这里以LibPcdGet32()为例(就是上例中用到的),它的实现如下:
/**
This function provides a means by which to retrieve a value for a given PCD token.
Returns the 32-bit value for the token specified by TokenNumber.
@param[in] TokenNumber The PCD token number to retrieve a current value for.
@return Returns the 32-bit value for the token specified by TokenNumber.
**/
UINT32
EFIAPI
LibPcdGet32 (
IN UINTN TokenNumber
)
{
return GetPcdProtocol()->Get32 (TokenNumber);
}
也就是说这里要依赖于Protocol(DXE阶段是Protocol,而PEI阶段是PPI)来完成操作。
这Protocol在Pcd.info模块中安装。
Pcd.inf模块分为PEI和DXE两个版本,分别放在PEI阶段和DXE阶段的最前面,只有整个模块初始化完成之后,才能够开始正常使用PCD宏来访问Dynamic PCD变量。
以DXE阶段的Pcd.inf模块为例,它主要做了两件事情:
1. 初始化该阶段使用的PCD数据库;
2. 安装各种处理PCD需要的Protocol;
在这个模块中,安装的主要的Protocol如下:
///
/// This service abstracts the ability to set/get Platform Configuration Database (PCD).
///
typedef struct {
PCD_PROTOCOL_SET_SKU SetSku;
PCD_PROTOCOL_GET8 Get8;
PCD_PROTOCOL_GET16 Get16;
PCD_PROTOCOL_GET32 Get32;
PCD_PROTOCOL_GET64 Get64;
PCD_PROTOCOL_GET_POINTER GetPtr;
PCD_PROTOCOL_GET_BOOLEAN GetBool;
PCD_PROTOCOL_GET_SIZE GetSize;
PCD_PROTOCOL_GET_EX_8 Get8Ex;
PCD_PROTOCOL_GET_EX_16 Get16Ex;
PCD_PROTOCOL_GET_EX_32 Get32Ex;
PCD_PROTOCOL_GET_EX_64 Get64Ex;
PCD_PROTOCOL_GET_EX_POINTER GetPtrEx;
PCD_PROTOCOL_GET_EX_BOOLEAN GetBoolEx;
PCD_PROTOCOL_GET_EX_SIZE GetSizeEx;
PCD_PROTOCOL_SET8 Set8;
PCD_PROTOCOL_SET16 Set16;
PCD_PROTOCOL_SET32 Set32;
PCD_PROTOCOL_SET64 Set64;
PCD_PROTOCOL_SET_POINTER SetPtr;
PCD_PROTOCOL_SET_BOOLEAN SetBool;
PCD_PROTOCOL_SET_EX_8 Set8Ex;
PCD_PROTOCOL_SET_EX_16 Set16Ex;
PCD_PROTOCOL_SET_EX_32 Set32Ex;
PCD_PROTOCOL_SET_EX_64 Set64Ex;
PCD_PROTOCOL_SET_EX_POINTER SetPtrEx;
PCD_PROTOCOL_SET_EX_BOOLEAN SetBoolEx;
PCD_PROTOCOL_CALLBACK_ONSET CallbackOnSet;
PCD_PROTOCOL_CANCEL_CALLBACK CancelCallback;
PCD_PROTOCOL_GET_NEXT_TOKEN GetNextToken;
PCD_PROTOCOL_GET_NEXT_TOKENSPACE GetNextTokenSpace;
} PCD_PROTOCOL;
这里就可以看到上文中LibPcdGet32()里面调用的Get32()函数了。
在上述GetX和SetX的函数实现中,最重要的是如下的函数:
GetWorker():
/**
Get the PCD entry pointer in PCD database.
This routine will visit PCD database to find the PCD entry according to given
token number. The given token number is autogened by build tools and it will be
translated to local token number. Local token number contains PCD's type and
offset of PCD entry in PCD database.
@param TokenNumber Token's number, it is autogened by build tools
@param GetSize The size of token's value
@return PCD entry pointer in PCD database
**/
VOID *
GetWorker (
IN UINTN TokenNumber,
IN UINTN GetSize
)
SetWorker():
/**
Set value for an PCD entry
@param TokenNumber Pcd token number autogenerated by build tools.
@param Data Value want to be set for PCD entry
@param Size Size of value.
@param PtrType If TRUE, the type of PCD entry's value is Pointer.
If False, the type of PCD entry's value is not Pointer.
@retval EFI_INVALID_PARAMETER If this PCD type is VPD, VPD PCD can not be set.
@retval EFI_INVALID_PARAMETER If Size can not be set to size table.
@retval EFI_INVALID_PARAMETER If Size of non-Ptr type PCD does not match the size information in PCD database.
@retval EFI_NOT_FOUND If value type of PCD entry is intergrate, but not in
range of UINT8, UINT16, UINT32, UINT64
@retval EFI_NOT_FOUND Can not find the PCD type according to token number.
**/
EFI_STATUS
SetWorker (
IN UINTN TokenNumber,
IN VOID *Data,
IN OUT UINTN *Size,
IN BOOLEAN PtrType
)
GetWorker()的作用就是在PCD数据库里面找到对应的PCD的指针,而SetWork()的作用就是找到指针然后赋值。
到这里为止,最需要了解的就是PCD数据库是如何建立的?
从BuildPcdDxeDataBase()这个函数中可以找到答案。
查看以下的代码:
//
// Assign PCD Entries with default value to PCD DATABASE
//
mPcdDatabase.DxeDb = LocateExPcdBinary ();
ASSERT(mPcdDatabase.DxeDb != NULL);
PcdDxeDbLen = mPcdDatabase.DxeDb->Length + mPcdDatabase.DxeDb->UninitDataBaseSize;
PcdDxeDb = AllocateZeroPool (PcdDxeDbLen);
ASSERT (PcdDxeDb != NULL);
CopyMem (PcdDxeDb, mPcdDatabase.DxeDb, mPcdDatabase.DxeDb->Length);
FreePool (mPcdDatabase.DxeDb);
mPcdDatabase.DxeDb = PcdDxeDb;
这里主要进行了PCD数据库的创建,它分为两个步骤:
1. 通过LocateExPcdBinary()从FFS中获取PCD已初始化变量;
2. 为未初始化的变量(默认全0)分配空间(这样做的目的应该是为了减少生成二进制的大小);
接下来需要关注的就是这个放在FFS中的PCD数据是从何而来的?
查看LocateExPcdBinary()函数里面的代码:
//
// Search the External Pcd database from one section of current FFS,
// and read it to memory
//
Status = GetSectionFromFfs (
EFI_SECTION_RAW,
0,
(VOID **) &DxePcdDbBinary,
&DxePcdDbSize
);
ASSERT_EFI_ERROR (Status);
从中可以看到PCD数据是存放在当前FFS的第一个Section开始的数据。
关于FFS,在之前的文章BIOS/UEFI基础——EDK编译生成的二进制的结构中介绍过,这里直接打开下面的文档查看:
使用二进制查看工具打开该文件,搜索PCD对应的GUID(gPcdDataBaseSignatureGuid),结果如下:
因此Dynamic PCD的数据是在编译的时候初始化并存放在BIOS二进制中的,然后在BIOS运行过程中会获取这些数据,并存放到内存中,后续就可以修改了。
DynamicEx类型的基本上一致,这里不再说明。
最后剩下的是PatchableInModule PCD,下面是一个例子:
1. 在DEC中声明:
[PcdsPatchableInModule]
gUefiOemPkgTokenSpaceGuid.PcdPatchableVar|0x00ABCDEF|UINT32|0x50000001
2. 在模块中使用:
DEBUG ((EFI_D_ERROR, "[beni]PcdPatchableVar: 0x%x\n", PcdGet32 (PcdPatchableVar)));
3. 查看AutoGen.c文件:
volatile UINT32 _gPcd_BinaryPatch_PcdPatchableVar = _PCD_PATCHABLE_VALUE_PcdPatchableVar;
GLOBAL_REMOVE_IF_UNREFERENCED UINTN _gPcd_BinaryPatch_Size_PcdPatchableVar = 4;
4. 这里定义了PCD相关的两个变量,一个是值,注意它的类型修饰符是volatile,另一个是PCD的大小。
5. 进一步查看AutoGen.h文件,可以看到_gPcd_BinaryPatch_PcdPatchableVar当前的值:
#define _PCD_TOKEN_PcdPatchableVar 112U
#define _PCD_PATCHABLE_VALUE_PcdPatchableVar ((UINT32)0x00ABCDEFU)
extern volatile UINT32 _gPcd_BinaryPatch_PcdPatchableVar;
#define _PCD_GET_MODE_32_PcdPatchableVar _gPcd_BinaryPatch_PcdPatchableVar
#define _PCD_PATCHABLE_PcdPatchableVar_SIZE 4
#define _PCD_GET_MODE_SIZE_PcdPatchableVar _gPcd_BinaryPatch_Size_PcdPatchableVar
extern UINTN _gPcd_BinaryPatch_Size_PcdPatchableVar;
#define _PCD_SET_MODE_32_PcdPatchableVar(Value) (_gPcd_BinaryPatch_PcdPatchableVar = (Value))
#define _PCD_SET_MODE_32_S_PcdPatchableVar(Value) ((_gPcd_BinaryPatch_PcdPatchableVar = (Value)), RETURN_SUCCESS)
6. 它的访问方式与FixedAtBuild PCD没有什么两样。
对于PatchableInModule PCD来说,唯一的差别就在于,变量是volatile的,但是这个volatile具体体现在哪里呢?我们如何对它进行Patch?
因为该PCD是可以通过外部工具来修改的,那么有一点是可以肯定的,即它是可见的,通过查看BIOS二进制就可以找到。
首先找OVMF.fd(这个是最终生成的BIOS二进制)中寻找,但是没有找到,再在DXEFV.Fv中寻找,发现了它的踪迹:
它的位置是在当前模块(即打印该PCD的模块)的只读数据区中。
同时在对应的EFI文件中也能找到:
其地址在与AutoGen同目录的map文件中也有说明:
上图中也可以发现,其它类型的PCD是没有这个。
因此理论上可以通过工具来修改这个值(在生成最终的OVMF.fd之前)。
而实际上,在EDK的源代码中,提供了操作PatchableInModule PCD的工具:
因此可以利用这两个工具来修改PCD。
这里将这两个工具(以及依赖文件)放到下述的编译中间目录:
首先是使用GenPatchPcdTable.exe来生成文件,该文件中包含所有的PatchableInModule PCD:
使用该工具之后生成了一个文件:
其内容如下:
(会看到跟之前二进制里面的偏移不一样,因为是重新编译过,不要在意...)
然后通过PatchPcdValue.exe来修改PCD:
这里将PcdPatchableVar的值修改成了0x88888888,比较修改前后的值:
可以看到确实是修改成功了!
具体GenPatchPcdTable.exe和PatchPcdValue.exe可以参考源代码中BaseTools\UserManuals目录下的说明。
以上的例子只是手动的修改efi文件,但是如果在编译的时候自动完成这个动作,或者在整个二进制生成之后修改PatchableInModule PCD呢?
目前这还是个疑问。
以上,就是对于UEFI PCD的所有内容的介绍。