UEFI开发探索101 – PCD探究

请保留-> 【原文:  https://blog.csdn.net/luobing4365 和 http://yiiyee.cn/blog/author/luobing/】

从《UEFI编程实践》出版后,一系列的事情接踵而来,终于在今天,算是告一段落了。

这段时间,有不少机会和业内人员讨论UEFI和BIOS。反思自己对这个领域的理解,深感自己理论的不足。

看过《UEFI编程实践》的网友,应该能了解,书中对于概念的理论部分,阐述得相对较少。我一般都是遵循“提出问题-介绍UEFI相关知识-提供实例”的框架,围绕某一课题进行研究。

其中的一个原因,是因为我本来就是奔着实践为目的,将平时开发中所遇到的课题逐渐展开讨论。另一个原因,是没有深入到EDK2的具体实现去。

因此,从UEFI开发探索第101篇开始,我想逐渐转向对EDK2的代码研究了。如之前研究PCI Option ROM开发一样,这次设立的目标包括:

  1. 对OvmfPkg源代码进行研究,搞清楚固件架构、编译过程、各阶段代码实现等;
  2. 了解Qemu怎么使用固件启动的,以及如何启动操作系统;
  3. 了解Ovmf固件如何提供操作系统所需要的各种Table、Runtime Services,甚至SMI Handler等。搞清楚一个UEFI操作系统如何与BIOS结合的。

我了解这是一个不小的目标,会遇到不少的困难,兴趣所在,倒不是特别畏惧。本来的想法,是在树莓派上进行实验,在lab-z(博客:https://www.lab-z.com/)的建议下,觉得OvmfPkg做实验更方便些。

主题确定,后续想到哪里不熟就补足哪里的知识。嗯,先从EDK2全局配置的关键核心-PCD开始研究。

1 PCD简介

Platform Configuration Database(PCD)是EDK2用来进行全局配置的机制,可在代码复用、模块化方面发挥巨大作用。

PCD是把代码里面的可配置选项抽取出来,在platform需要修改的时候,可以不用去修改源代码。其参数的配置,可以在编译过程中、运行时中都可以配置,甚至在二进制文件中也可以配置。

这种设计方式就比较让人着迷,这也使得定制化更为容易,代码更容易维护。

早期我一直以为PCD如同C/C++中的宏,用来提取公用代码,这是错误的。它提供的功能更为广泛,也更复杂。

首先直观地看下平常程序中用到的PCD,以前几篇中的Diskdump工程为例,使用如下命令编译,提取出其所用的PCD信息:

C:\vUDK2018\edk2>build -Y PCD -y pcd.log -p RobinPkg\RobinPkg.dsc -m RobinPkg\Applications\Diskdump\Diskdump.inf -a IA32

输出的信息,存在了pcd.log中。查看下log信息:

图1 Diskdump中用到的PCD

观察一下,可以看到许多编程时压根就没注意到的PCD参数。Diskdump使用了MdePkg和ShellPkg中的Protocol,因此也用到了其相关的PCD。

对于PCD的文档,可以参考:
EDK2代码:
MdeModulePkg\Universal\PCD\Dxe\Pcd.inf
https://github.com/tianocore/tianocore.github.io/wiki/EDK-II-Documents:
《EDK II Platform Configuration Database Infrastructure Description》
《EDK II Platform Description(DSC) File Specification》
《EDK II Package Declaration(DEC) File Format Specification》
《EDK II Build Specification》
https://uefi.org/specifications:
《Platform Initialization(PI) Specification》

2 如何使用PCD

PCD可以使用于UEFI存在的大部分时间,除了在SEC阶段、早期的PEI和DXE阶段,基本都可以访问。在使用前,我们需要搞清楚PCD的结构和类型。

2.1 PCD的类型

PCD变量的格式有点像结构体:

TokenSpaceGuidCName.PcdCName

其中,TokenSpaceGuidCName是GUID,而PcdCName是变量名,两者组合构成了一个PCD变量。

PCD有如下的类型。

FixedAtBuild类型

它在编译阶段确定,是静态值,在运行阶段或二进制形态下都不可改。可以认为它就是一个宏了。

FeatureFlag类型

它实际上和FixedAtBuild是同一类型,返回一个Bool类型(True或False),可用于判断条件。

PatchableInModule类型

此类型的变量值在编译的时候确定,它在编译后的二进制文件上使用工具修改。与FixedAtBuild不同,它只能影响一个模块(作用域在一个模块)。

Dynamic类型、DynamicHii类型和DynamicVpd类型

Dynamic类型变量的作用域是整个系统,它是动态的PCD,可以在UEFI运行过程中修改。

DynamicHii类型与Dynamic类型存储的位置不同,Dynamic类型可以认为是存在于Memory中,再加载是会失去原始设置的;而DynamicHii类型是存在Efi variable中的(NVRAM中),其修改时非易失性的。

而DynamicVpd类型变量是只读的,不可写的,一般出厂确定。

DynamicEx类型

与Dynamic类型类似,相当于加强版。其与Dynamic类型的区别,在于是否使用二进制文件中的PCD。比如FSP,如果要使用其中的PCD变量,则FSP中的PCD类型必须设置为### DynamicEx类型。

2.2 访问PCD变量

为管理PCD变量,PEI提供了PCD_PPI和EFI_PEI_PCD_PPI;DXE提供了PCD_PROTOCOL和EFI_PCD_PROTOCOL。

不过,为了方便使用,EDK2中引入了PCD Library,把这些访问细节隐藏了起来。(MdePkg\Include\Library\PcdLib.h)

库中包含如下函数:

PcdGetXX()
PcdSetXX()
PcdGetExXX()
PcdSetExXX()
PcdToken()
PCDSetSku()
PcdGetNextToken()
PcdGetNextTokenSpace()
CallBackOnSet()
CancelCallBack()

其中,XX可以为8、16、32、Size、Ptr或者Boolean。

2.3 PCD的声明和使用

PCD的使用,基本可以按照如下流程进行。

1 DEC文件中声明PCD变量的基本信息

比如:

[PcdsFixedAtBuild, PcdsPatchableInModule, PcdsDynamic, PcdsDynamicEx]
	 gEfiMdeModulePkgTokenSpaceGuid.PcdHelloWorldPrintTimes|1|UINT32|0x40000005
	 gEfiMdeModulePkgTokenSpaceGuid.PcdHelloWorldPrintString|L"UEFI Hello World!\n"|VOID*|0x40000004

其格式为:

  TokenSpaceGuidCname.PcdCname|DefaultValue|DatumType|Token

如前所述,PcdCname为变量名,DefaultValue为其默认值,DatumType是PCD的数据类型,Token是一个32位的整型,在DEC中每个PCD都有一个独有的Token。

DatumType可以是BOOLEAN、UINT8、UINT16、UINT32、UINT64或VOID *型。

2 DSC文件中设置PCD变量的值

可以在DSC文件中设置相应PCD变量的值,比如:

[PcdsFixedAtBuild]
  gEfiMdePkgTokenSpaceGuid.PcdDebugPropertyMask|0x0f

此设置过程不是必须的,如果没有设置,则使用DEC文件中的默认值。

3 INF文件中声明

在模块的INF文件中,需要声明PCD变量,才可以在源码中使用。比如:

[Pcd]
  gEfiMdeModulePkgTokenSpaceGuid.PcdHelloWorldPrintString   
  gEfiMdeModulePkgTokenSpaceGuid.PcdHelloWorldPrintTimes

只需要列出PCD变量名就可以了,其他信息不用列出。

完成上述工作后,就可以在源代码中,使用PCD库函数访问PCD变量了。示例如下:(摘自MdeModulePkg\Application\HelloWorld\HelloWorld.c)

 if (FeaturePcdGet (PcdHelloWorldPrintEnable)) {
  	for (Index = 0; Index < PcdGet32 (PcdHelloWorldPrintTimes); Index ++) {
  	  //
  	  // Use UefiLib Print API to print string to UEFI console
  	  //
    	Print ((CHAR16*)PcdGetPtr (PcdHelloWorldPrintString));
    }
  }

3 试着写个例子

在UEFI应用开发或者Option ROM开发中,基本上不用PCD变量。但并不妨碍在UEFI应用上使用它们,我们试着在RobinPkg的某个Application上,来使用PCD变量。

我选择之前开发的Diskdump工程,改名为Pcdtouch,尝试使用PCD变量。当然,随便选一个其他的工程也可以,刚好这个工程就在眼前,就随手在它上面改造了。

修改步骤如下:

1. 修改RobinPkg.dec
添加如下语句:

[Guids]
  gRobinPkgPcdSampleGuid = { 0xe7e1efa6, 0x7607, 0x3a78, { 0xc7, 0xdd, 0x43, 0xe4, 0xbd, 0x72, 0xc1, 0x19 }}
# [PcdsFixedAtBuild, PcdsPatchableInModule, PcdsDynamic, PcdsDynamicEx]
[PcdsPatchableInModule, PcdsDynamic, PcdsDynamicEx]
  gRobinPkgPcdSampleGuid.PcdtouchValue|12345|UINT32|0x90000005
  gRobinPkgPcdSampleGuid.PcdtouchStr|L"Hello,UEFI World, this is robin!\n"|VOID*|0x90000004

2. 修改Pcdtouch.inf
DSC文件中可以修改PCD变量的值,这里我们不需要修改,不用去改DSC文件。

直接修改INF文件就可以了,添加将要在源程序中用到的PCD变量:

[FeaturePcd]
  gEfiMdeModulePkgTokenSpaceGuid.PcdHelloWorldPrintEnable   ## CONSUMES

[Pcd]
  gEfiMdeModulePkgTokenSpaceGuid.PcdHelloWorldPrintString   ## SOMETIMES_CONSUMES
  gEfiMdeModulePkgTokenSpaceGuid.PcdHelloWorldPrintTimes    ## SOMETIMES_CONSUMES
  gRobinPkgPcdSampleGuid.PcdtouchStr
  gRobinPkgPcdSampleGuid.PcdtouchValue

除了在DEC文件中添加的两个PCD变量外,还把MdeModulePkg中的几个PCD变量也声明了,待会在程序中要用。

3. 在源程序Pcdtouch.c中添加代码
主要是修改main程序:

int
main (
  IN int Argc,
  IN char **Argv
  )
{
  UINT32 Index,myValue;
  
  Index = 0;
  myValue = 0;
 
  // 测试MdeModulePkg中的PCD变量,参考HelloWorld
  if (FeaturePcdGet (PcdHelloWorldPrintEnable)) {
    for (Index = 0; Index < PcdGet32 (PcdHelloWorldPrintTimes); Index ++) {
      Print ((CHAR16*)PcdGetPtr (PcdHelloWorldPrintString));
    }
  }
  
  Print ((CHAR16*)PcdGetPtr (PcdtouchStr)); //打印PCD变量
  Print(L"\n");
  myValue = PcdGet32(PcdtouchValue);
  Print(L"PcdtouchValue = %d\n", myValue);
  PcdSet32(PcdtouchValue,321);
  // LibPcdSet32(PcdtouchValue,321);
  myValue = PcdGet32(PcdtouchValue);
  Print(L"now,PcdtouchValue = %d\n", myValue);
}

至此修改完成。编译后在模拟器中运行,结果如下:

图2 Pcdtouch运行结果

在编写过程中,得到的一些经验:

  1. VOID *型(字符串类型)的PCD变量不能只定义为PcdsDynamic型,会编译不通过的。(是因为不能修改吗,具体原因不清楚);
  2. 使用PcdSet32的PCD变量,不能定义为PcdsFixedAtBuild型,编译会提示找不到此PCD变量。(这个倒是很好理解,因为PcdsFixedAtBuild型是编译时确定的)

后续再继续加深理解。

Gitee地址:https://gitee.com/luobing4365/uefi-exolorer
项目代码位于:/ FF RobinPkg/RobinPkg/Applications/Pcdtouch下

9,191 total views, 9 views today

《UEFI开发探索101 – PCD探究》有7个想法

  1. 此设置过程不是必须的,如果没有设置,则使用DEC文件中的默认值。

    請問一下, 您提到的這段, 假設DSC沒有設置, DEC 內容如下
    [PcdsFixedAtBuild, PcdsPatchableInModule, PcdsDynamic, PcdsDynamicEx]
    gEfiMdeModulePkgTokenSpaceGuid.PcdHelloWorldPrintTimes|1|UINT32|0x40000005

    TokenSpaceGuidCname.PcdCname|DefaultValue|DatumType|Token
    Value 的話會是 1 , 那麼類型的話呢?

  2. 你好,NT32 模拟器还能用么?我重新Git了2017代码编译后运行NT32模拟器,可以进去,但是进去就卡在了Shell,鼠标键盘皆不可用。不知大佬是否有遇到此类情况

    1. 嗯,没有试过,也许会有问题。后面的模拟器都用EmulatorPkg了,注意看下它们里面的说明。我遇到过的是EmluatorPkg使用VS2019编译,模拟器不接受鼠标键盘消息。本来怀疑是公司内部监控软件的副作用,没有去深究其原因。现在转到Linux下编译了

  3. 你好,我重新Git了edk2017的代码,编译后运行NT32模拟器,可以进去,但是还是卡在了Shell处,键盘鼠标皆不可用。不知你是否有遇到过此类情况。

罗冰(Robin)进行回复 取消回复

电子邮件地址不会被公开。