【技术】UEFI基础服务:系统表

转自:http://blog.csdn.net/sevensevensevenday/article/details/71158858

一、前言

  对UEFI应用程序和驱动程序开发人员来讲,系统表是最重要的数据结构之一,它是用户空间通往内核空间的通道。有了它,UEFI应用程序和驱动才可以访问UEFI内核、硬件资源和I/O设备。
  (1)在应用程序和驱动中访问系统表
  计算机系统进入DXE阶段后系统表被初始化,因而系统表只能用于DXE阶段以及以后的应用程序和驱动中。系统表是UEFI内核的一个全局结构体,其指针作为程序映像(Image)入口函数的参数传递到用户空间。程序映像(包括UEFI应用程序、DXE驱动程序以及UEFI驱动程序)的入口函数有统一的格式,其函数原型如下:

typedef
EFI_STATUS
(EFIAPI *EFI_IMAGE_ENTRY_POINT) (
    IN EFI_HANDLE ImageHandle,        //程序映像(Image)的句柄
    IN EFI_SYSTEM_TABLE *SystemTable  //系统表指针
);

返回值描述
EFI_SUCCESSThe driver was initialized
EFI_OUT_OF_RESOURCESThe request could not be completed due to a lack of resources

  (2)系统表指针从内核传递到用户空间的过程
  通常,程序映像的入口函数是_ModuleEntryPoint(当一个Image被启动服务的StartImage服务启动时,执行的就是这个入口函数)。当应用程序或驱动加载到内存形成Image后(ImageHandle是这个Image的句柄),_ModuleEntryPoint函数地址被赋值给Image对象的EntryPoint,然后Image->EntryPoint(ImageHandle,SystemTable)会被执行,最终会从Image的入口函数_ModuleEntryPoint执行到模块的入口函数(模块的入口函数是通过.inf文件中ENTRY_POINT指定的那个函数)。

二、系统表的构成

  系统表可分为如下6部分:

  • 表头:包括表的版本号、表的CRC校验码等。
  • 固件信息:包括固件开发商名字的字符串和固件的版本号。
  • 标准输入控制台、标准输出控制台、标准错误控制台。
  • 启动服务表
  • 运行时服务表
  • 系统配置表

    typedef struct {
        EFI_TABLE_HEADER  Hdr; ///标准UEFI表头
        CHAR16  *FirmwareVendor; //固件提供商
        UINT32  FirmwareRevision; //固件版本号
        EFI_HANDLE  ConsoleInHandle; //输入控制台设备句柄
        EFI_SIMPLE_TEXT_INPUT_PROTOCOL  *ConIn;
        EFI_HANDLE  ConsoleOutHandle; //输出控制台设备句柄
        EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL  *ConOut;
        EFI_HANDLE  StandardErrorHandle; //标准错误控制台设备
        EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL  *StdErr;
        EFI_RUNTIME_SERVICES  *RuntimeServices; //运行时服务表
        EFI_BOOT_SERVICES  *BootServices; //启动时服务表
        UINTN  NumberOfTableEntries;  // CongratulationTable数组的大小
        EFI_CONFIGURATION_TABLE  *ConfigurationTable; //系统配置表数组
    } EFI_SYSTEM_TABLE;

      系统表数据结构:

  

typedef struct {
    EFI_TABLE_HEADER  Hdr; ///标准UEFI表头
    CHAR16  *FirmwareVendor; //固件提供商
    UINT32  FirmwareRevision; //固件版本号
    EFI_HANDLE  ConsoleInHandle; //输入控制台设备句柄
    EFI_SIMPLE_TEXT_INPUT_PROTOCOL  *ConIn;
    EFI_HANDLE  ConsoleOutHandle; //输出控制台设备句柄
    EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL  *ConOut;
    EFI_HANDLE  StandardErrorHandle; //标准错误控制台设备
    EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL  *StdErr;
    EFI_RUNTIME_SERVICES  *RuntimeServices; //运行时服务表
    EFI_BOOT_SERVICES  *BootServices; //启动时服务表
    UINTN  NumberOfTableEntries;  // CongratulationTable数组的大小
    EFI_CONFIGURATION_TABLE  *ConfigurationTable; //系统配置表数组
} EFI_SYSTEM_TABLE;

      简要介绍系统表重要组成部分:
  (1) 表头 EFI_TABLE_HEADER
  UEFI中的表通常都是以EFI_TABLE_HEADER开头,EFI_TABLE_HEADER的数据结构如下:

typedef struct {
    UINT64 Signature;
    UINT32 Revision;
    UINT32 HeaderSize;
    UINT32 CRC32;
    UINT32 Reserved;
} EFI_TABLE_HEADER;

  UEFI中的Signature为64位的无符号整数,为了帮助开发者的使用,EDK2提供了宏SIGNATURE_64(A,B,C,D,E,F,G,H),它用于将ASCII码串转化为64位的无符号整数。例如,EFI_SYSTEM_TABLE的Signature为SIGNATURE_64(‘I’,’B’,’I’,’ ‘,’S’,’Y’,’S’,’T’)。
  HeaderSize是整个表的长度,对系统表来讲,就是sizeof(EFI_SYSTEM_TABLE)。
  CRC32是标的校验码。计算CRC32校验码时,首先将数据结构中CRC32域清零,然后计算整张表(表大小为HeaderSize)的CRC32码,计算后将校验码填充到CRC32域。
  (2)标准的输入控制台、标准的输出控制台和标准错误控制台。
  系统表还提供了三个控制台设备以及操作三个控制台的Protocol。ConIn用于从输入控制台ConsoleInHandle读取字符,通常输入控制台为键盘。ConOut用于向输出控制台ConsoleOutHandle输出字符串,通常输出控制台为屏幕。stdErr用于向标准错误控制台StandardErrorHandle输出字符串,通常这个标准错误控制台为屏幕。这三个控制台设备以及ConIn、ConOut、stdErr三个Protocol在驱动ConSplitterDxe中被初始化。
  (3)系统配置表
  ConfigurationTable是系统配置表,它指向EFI_CONFIGURATION_TABLE数组,数组中的每一项是一个表。
  EFI_CONFIGURATION_TABLE的数据结构:

typedef struct{
    EFI_GUID VendorGuid; //配置表标识符
    VOID *VendorTable;   //指向配置表的数据
} EFI_CONFIGURATION_TABLE;

  例如,ConfigurationTable通常会包含APCI(Advance Configuration and Power Interface)表,ACPI在系统配置表中可以表示为:{gEfiAcpiTableGuid,EFI_ACPI_3_0_ROOT_SYSTEM_DESCRIPTION_POINTER*}

三、使用系统表

  系统表是UEFI内核的全局数据结构,应用程序运行在用户空间。那么用户空间是如何获得内核空间的系统表指针的?其实在UEFI中只有一个地址空间,所有的程序都运行在RING0优先级,应用程序地址空间占用UEFI地址空间的一部分。既然用户空间和内核空间是一个整体,在应用程序内也就可以直接使用内核空间的任何地址了,那么在应用程序内,只要得到了系统表的地址,就可以使用系统表了。
  (1)在用户空间使用系统表
  系统表的地址可以通过模块的入口函数的参数得到。

#include<Uefi.h>
EFI_Status
UefiMain(IN EFI_HANDLE ImageHandle,IN EFI_SYSTEM_TABLE *SystemTable)
{
    EFI_STATUS Status;
    UINTN Index;
    EFI_INPUT_KEY Key;
    CHAR16 StrBuffer[3] = {0};
    SystemTable->BootServices->WaitForEvent(1,&SystemTable->ConIn->WaitForKey,&Index);
    Status = SystemTable->ConIn->ReadKeyStroke(SystemTable->ConIn,&Key);
    StrBuffer[0] = Key.UnicodeChar;
    StrBuffer[1] = '\n';
    SystemTable->ConOut->OutputString(SystemTable->ConOut,StrBuffer);
    return EFI_SUCCESS;
}

代码解析:
  • SystemTable->BootServices指向系统的启动服务表。
  • SystemTable->ConIn 指向安装在标准输入设备上的EFI_SIMPLE_TEXT_INPUT_PROTOCOL.
  • SystemTable->ConOut 指向安装在标准输出设备上的EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL.
      该示例首先用WaitForEvent等待键盘事件,然后调用ConIn的ReadKeyStroke读取键盘,最后调用ConOut的OutputString服务将按键显示到屏幕。
      启动服务BootServices提供的EFI_STATUS WaitForEvent(IN UINTN NumberOfEvents, IN EFI_EVENT * Event,OUT UINTN *Index)服务用于等待Event数组中任一事件的发生。Event数组(NumberOfEvent是数组大小)中任一事件被触发时该函数返回,Index返回被触发事件在Event数组的下标。该函数是阻塞函数。
      EFI_STATUS ReadKeyStroke(EFI_SIMPLE_TEXT_INPUT_PROTOCOL *This,OUTEFI_INPUT_KEY *Key)用于读取按键,它是ConInProtocol的成员函数,第一个参数是This指针,第二个参数用于返回按键。
      EFI_STATUS OutputString(EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *This,CHAR16 String)用于向屏幕打印字符串,它是ConOut Protocol的成员函数,第一个参数同样是This指针,第二个参数是打印到屏幕的字符串。
      (2)在用户空间使用gST访问系统表
      上面(1)中的示例中的模块入口函数UefiMain中使用传入参数SystemTable访问系统表。EDK2为了方便开发者,提供了UefiBootServicesTableLib,在UefiLib定义了全局变量gST(指向SystemTable),gBS(指向SystemTable->BootServices)和gImageHandle(ImageHandle)。这三个全局变量在函数UefiBootServicesTableLibConstructor中被初始化,这个函数是库UefiBootServicesTableLib的构造函数,在AutoGen.c中的ProcessLibraryConstructorList被调用。而ProcessLibraryConstructorList是在UefiMain之前被调用的。
      构造函数UefiBootServicesTableLibConstructor源码:
///\EDK2\MdePkg\Library\UefiBootServicesTableLib\UefiBootServicesTableLib.c
EFI_STATUS
EFIAPI
UefiBootServicesTableLibConstructor (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
      //
      // Cache the Image Handle
      //
      gImageHandle = ImageHandle;
      ASSERT (gImageHandle != NULL);

      //
      // Cache pointer to the EFI System Table
      //
      gST = SystemTable;
      ASSERT (gST != NULL);

      //
      // Cache pointer to the EFI Boot Services Table
      //
      gBS = SystemTable->BootServices;
      ASSERT (gBS != NULL);

      return EFI_SUCCESS;
}

  gST变量定义在用户空间,它指向的系统表定义在UEFI内核中。在应用程序或驱动工程文件的[LibraryClasses]里引用UefiBootServicesTableLib后,就可以使用gST访问系统表了。

/// 示例:使用了gST
#include<Uefi.h>
#include<Library/UefiBootServicesTableLib.h>
EFI_Status
UefiMain(IN EFI_HANDLE ImageHandle,IN EFI_SYSTEM_TABLE *SystemTable)
{
    EFI_STATUS Status;
    UINTN Index;
    EFI_INPUT_KEY Key;
    CHAR16 StrBuffer[4] = {0};
    gST -> ConOut -> OutputString(gST -> ConOut,L"Please enter any key\n");
    gBS->WaitForEvent(1,&gST->ConIn->WaitForKey,&Index);
    Status = gST->ConIn->ReadKeyStroke(gST->ConIn,&Key);
    StrBuffer[0] = Key.UnicodeChar;
    StrBuffer[1] = '\n';
    gST->ConOut->OutputString(gST->ConOut,StrBuffer);
    return EFI_SUCCESS;
}

  从代码上看,其实就是使用gST和gBS替换掉SystemTable和SystemTable -> BootServices。

四、总结

  本文主要系统表的构成、系统表从内核空间传递到用户空间的过程,以及在用户空间访问系统表的两种方法。得到系统表就可以在用户空间访问UEFI内核了,应用程序和驱动对内核的控制是通过两个核心成员BootServices和RuntimeServices。EDK2在用户空间分配了全局变量gBS和gRT指代这两个服务。

参考资料

1.《UEFI原理与编程》戴正华 著
2. UEFI Spec 2_6
3. EDK2 GitHub 

 

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值