转自: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_SUCCESS | The driver was initialized |
EFI_OUT_OF_RESOURCES | The 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