作者:Maxwell Li
日期:2017/12/07
未经作者允许,禁止转载本文任何内容。如需转载请留言。
[TOC]
5.1 系统表
系统表是重要的数据接口之一,是用户空间通往内核空间的通道。
(1)在应用程序和驱动中访问系统表
系统表是 UEFI 内核的一个全局结构体,其指针作为程序映像入口函数的参数传递到用户空间。程序映像(包括 UEFI 应用程序、DXE 驱动程序、UEFI 驱动程序)的入口函数有统一的格式,函数原型如下:
typedef EFI_STATUS(EFIAPI *EFI_IMAGE_ENTRY_POINT) (
IN EFI_HANDLE ImageHandle, // 程序映像的句柄
IN EFI_SYSTEM_TABLE *SystemTable // 系统表指针
);
(2)系统表指针从内核传递到用户空间的过程
程序映像的入口函数通常是 _ModuleEntryPoint。当应用程序或驱动加载到内存形成 Image 后,_ModuleEntryPoint 函数地址被赋值给 Image 对象的 EntryPoint,然后 Image->EntryPoint 会被执行,最终会从 Image 的入口函数 _ModuleEntryPoint 执行到模块的入口函数。
5.1.1 系统表的构成
系统表可分为以下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;
(1)表头 EFI_TABLE_HEADER
UEFI 中的表通常都以 EFI_TABLE_HEADER 开头,数据结构如下:
typedef struct {
UINT64 Signature;
UINT32 Revision;
UINT32 HeaderSize;
UINT32 CRC32;
UINT32 Reserved;
} EFI_TABLE_HEADER;
- Signature 为64位无符号整数,EDK2 提供了宏 SIGNATURE_64(A,B,C,D,E,F,G,H),它用于将 ASCII 码串转化为64位的无符号整数。
- HeaderSize 是整个表的长度。
- CRC32 是表的校验码。
(2)标准输入控制台、标准输出控制台、标准错误控制台
- ConIn 用于从输入控制台 ConsoleInHandle 读取字符,通常输入控制台为键盘。
- ConOut 用于向输出控制台 ConsoleOutHandle 输出字符串,通常输出控制台为屏幕。
- StdErr 用于向标准错误控制台 StandardErrorHandle 输出字符串。
这三个控制台设备以及 ConIn、ConOut、StdErr 三个 Protocol 在驱动 ConSplitterDxe 中被初始化。
(3)系统配置表
ConfigurationTable 是系统配置表,指向 EFI_CONFIGUTATION_TABLE 数组,数组中每一项是一个表,这个表的数据结构如下:
typedef struct{
EFI_GUID VendorGuid; // 配置表标识符
VOID *VendorTable; // 指向配置表的数据
} EFI_CONFIGURATION_TABLE;
5.1.2 使用系统表
在 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; // 取得按键的 Unicode 码
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。
(2)在用户空间使用 gST 访问系统表
上面示例中的模块入口函数 UefiMain 中使用传入的参数 SystemTable 访问系统表。EDK2 为了方便开发者,提供了 UefiBootServicesTableLib,在 UefiLib 定义了全局变量 gST
、gBS、gImageHandle。这三个全局变量在函数 UefiBootServicesTableLibConstructor 中被初始化,该函数是库 UefiBootServicesTableLib 的构造函数,在 AutoGen.c 中的 ProcessLibraryConstructorList 被调用,而 ProcessLibraryConstructorList 是在 UefiMain 之前被调用的。
构造函数 UefiBootServicesTableLibConstructor 源码:
EFI_HANDLEgImageHandle = NULL;
EFI_SYSTEM_TABLE*gST = NULL;
EFI_BOOT_SERVICES*gBS = NULL;
EFI_STATUS EFIAPI UefiBootServicesTableLibConstructor (
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable)
{
gImageHandle = ImageHandle;
gST = SystemTable;
gBS = SystemTable->BootServices;
return EFI_SUCCESS;
}
gST 变量是定义在用户空间的变量,而它指向的系统表定义在 UEFI 内核中。在应用程序或驱动工程文件的 [LibraryClasses] 里引用 UefiBootServicesTableLib 后,就可以使用 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。
5.2 启动服务
启动服务是 UEFI 的核心数据结构,可以分为以下几类:UEFI 事件服务、内存管理服务、Protocol 管理服务、Protocol 使用类服务、驱动管理服务、Image 管理服务、ExitBootServices、其他服务。
5.2.1 启动服务的构成
启动服务由 UEFI 表头和表项组成,表中每一项是一个函数指针,该函数用于提供一项服务。
UEFI 事件服务
事件是异步操作的基础,使得在 UEFI 系统内可以执行并发操作。UEFI 事件服务包含事件(Event)、定时器(Timer)、任务优先级(TPL)三类服务。
- 事件服务用于产生、关闭、触发、等待事件和检查事件状态。
- 定时器服务用于设置定时器属性。
- 任务优先级服务用于提升、降低当前程序的优先级。
详见第六章。
内存管理服务
内存管理服务主要提供内存的分配与释放服务、管理系统内存映射。主要包括:AllocatePages、FreePages、AllocatePool、FreePool、GetMemoryMap。
AllocatePool/FreePool 用法
AllocatePool 和 FreePool 服务函数原型:
/**gBS->AllocatePool 分配内存
@retval EFI_SUCCESS 成功分配所请求的内存
@retval EFI_OUT_OF_RESOURECES 资源耗尽
@retval EFI_INVALID_PARAMETER 参数非法
**/
typedef EFI_STATUS(EFIAPI *EFI_ALLOCATE_POOL)(
IN EFI_MEMORY_TYPE PoolType, // 内存类型
IN UINTN Size, // 需分配的内存字节数
OUT VOID **Buffer // 返回分配的内存地址
);
/**gBS->FreePool 释放内存
@retval EFI_SUCCESS 内存成功被系统回收
@retval EFI_INVALID_PARAMETER 参数非法
**/
typedef EFI_STATUS(EFIAPI *EFI_FREE_POOL)(
IN VOID *Buffer // 待释放内存
);
枚举类型 EFI_MEMORY_TYPE:
typedef enum {
EfiReservedMemoryType=0, // 保留类型,未使用
EfiLoaderCode, // 分配给 OS Loader 的代码
EfiLoaderDate, // 分配给 OS Loader 的数据,应用程序分配内存时的默认类型
EfiBootServicesCode, // 启动服务驱动 / 应用程序的代码区
EfiBootServicesDate, // 启动服务驱动 / 应用程序的数据区,BS 驱动分配内存的默认类型
EfiRuntimeServicesCode, // 运行时服务驱动的代码区
EfiRuntimeServicesData, // 运行时服务驱动的数据区,RS 驱动分配内存的默认类型
EfiConventionalMemory, // 可分配内存
EfiUnusableMemory, // 该块内存区域出现错误,不能使用
EfiACPIReclaimMemory, // 用于存放 ACPI 表
EfiACPIMemoryNVS, // 保留给固件使用
EfiMemoryMappedIO, // 内存映射 I/O,可被运行时服务使用
EfiMemoryMappedIOPortSpace, // 内存映射 I/O,被 CPU 用于转换内存周期的 IO 周期
EfiPalCode, // 保留给固件
EfiMaxMemoryType
} EFI_MEMORY_TPYE;
调用 gBS->ExitBootServices 之后, EfiBootServicesCode 和 EfiBootServicesData 类型内存被回收;EfiLoaderCode 和 EfiLoaderData 由 OS Loader 和操作系统决定是否回收;EfiACPIReclaimMemory 类型内存在 APCI 启用后被回收;其他类型内存保留。
AllocatePages/FreePages 用法
内存和驱动开发中经常会要求分配到的内存不得跨页,或需要分配完整的内存页,为此提供了分配页的服务 AllocatePages。
AllocatePages 和 FreePages 服务函数原型:
/**gBS->AllocatePages 分配内存页
@retval EFI_SUCCESS 成功分配内存页
@retval EFI_OUT_OF_RESOURECES 资源耗尽
@retval EFI_INVALID_PARAMETER 参数非法
@retval EFI_NOT_FOUND 请求的物理页不存在
**/
typedef EFI_STATUS(EFIAPI *EFI_ALLOCATE_PAGES)(
IN EFI_ALLOCATE_TPYE Type, // 分配方式
IN EFI_MEMORY_TYPE MemoryType, // 内存类型
IN UINTN Pages, // 分配的页面数
// 输入:物理地址或 NULL;输出:分配到的页面虚拟地址
IN OUT EFI_PHYSICAL_ADDRESS *Memory
);
typedef EFI_STATUS(EFIAPI *EFI_FREE_PAGES)(
IN EFI_PHYSICAL_ADDRESS Memory // 待释放的页面的起始物理地址
IN UINTN Pages // 待释放的页面数
);
枚举类型 EFI_AlLOCATE_TYPE
typedef enum {
AllocateAnyPages, // 分配任何可用的页面
AllocateMaxAddress, // 分配到的页面的末地址不超过指定地址
AllocateAddress, // 分配指定物理地址上的页面
MaxAllocateType,
} EFI_ALLOCATE_TPYE;
GetMemoryMap 用法
GetMemoryMap 用于取得系统中所有的内存映射。
GetMemoryMap 服务函数原型:
/**返回系统内存映射
@retval EFI_SUCCESS 成功返回当前内存映射
@retval EFI_BUFFER_TOO_SMALL 缓冲区太小
@retval EFI_INVALID_PARAMETER 参数非法
**/
typedef EFI_STATUS(EFIAPI *EFI_GET_MEMORY_MAP)(
IN OUT UINTN *MemoryMapSize,
IN OUT EFI_MEMORY_DESCRIPTOR *MemoryMap, // 输出缓冲区
OUT UINTN *MapKey, // 当前内存个映射的 Key
OUT UINTN *DescriptorSize, // EFI_MEMORY_DESCRIPTOR 大小
OUT UINT32 *DescriptorVersion // EFI_MEMORY_DESCRIPTOR 版本
);
- MemoryMapSize:
- 作为输入参数:表示缓冲区 MemoryMap 字节数
- 作为输出参数:
- 缓冲区不够大,返回需要的缓冲区大小
- 缓冲区足够大,返回写入 MemoryMap 的字节数
- MemoryMap:返回 EFI_MEMORY_DESCRIPTOR 数组
EFI_MEMORY_DESCRIPTOR 数据结构
typedef struct {
UINT32 Type; // EFI_MEMORY_TPYE,内存类型
EFI_PHYSICAL_ADDRESS PhusicalStart; // 首字节物理地址,必须按 4KB 对齐
EFI_VITRUAL_ADDRESS VirtualStart; // 首字节虚拟地址,必须按 4KB 对齐
UINT64 NumberOfPages; // 此区域的页面数
UINT64 Attribute; // 内存属性
} EFI_MEMORY_DESCRIPTOR;
Protocol 管理服务
Protocol 管理服务提供安装与卸载 Protocol 的服务,以及注册 Protocol 通知函书(安装时调用)的服务。
详见第八章、第九章。
Protocol 使用类服务
Protocol 使用类服务包括 Protocol 的打开与关闭,查找支持 Protocol 的控制器,主要提供 Protocol 使用者使用。
详见第四章。
驱动管理服务
驱动管理服务包括将驱动安装到控制器的 connect 服务和将驱动从控制器上卸载的 disconnect 服务。
详见第九章。
Image 管理服务
Image 管理服务包括加载、卸载、启动、退出 UEFI 应用程序或驱动。
启动服务中的 Image 管理服务
Image 管理服务 | 作用 |
---|---|
LoadImage | 加载 .efi 文件至内存并生成 Image |
StartImage | 启动 Image,调用 Image 的入口函数 |
Exit | 退出 Image |
UnloadImage | 卸载 Image |
ExitBootService
ExitBootService 用于结束启动服务,该服务成功返回后,系统进入 RT 期。操作系统加载器从启动服务接过对计算机系统的控制权后必须调用该服务。
其他服务
启动服务中的其他服务
服务名 | 作用 |
---|---|
InstallConfigurationTable | 增加、更新、删除系统配置表项 |
GetNextMonotonicCount | 获得系统单调计数器的下一个值 |
Stall | 暂停 CPU 指定的微秒数 |
SetWatchdogTimer | 设置看门狗定时器(指定的时间内若无系统反应,则重启) |
CalculateCrc32 | 计算 CRC32 校验码 |
CopyMem | 复制内存 |
SetMem | 设置指定内存区域的值 |
5.3 运行时服务
从进入 DXE 阶段运行时服务被初始化,直到操作系统结束,运行时服务都一直存在并向上层提供服务。运行时服务主要包括:时间服务、读写系统变量、虚拟内存服务、其他服务。
时间服务
时间服务包括:读取 / 设置硬件事件、读取 / 设置唤醒定时器。
GetTime/SetTime
计算机硬件时钟由单独的电池供电,操作系统启动时通过读取硬件时钟获得事件。
GetTime/SetTime 服务函数原型:
typedef EFI_STATUS(EFIAPI *EFI_GET_TIME)(
OUT EFI_TIME *Time, // 当前时间
OUT EFI_TIME_CAPABILITIES *Capabilities OPTIONAL // 时钟硬件的性能
);
/**gRT->SetTime
@retval EFI_SUCCESS 成功设置时钟
@retval EFI_INVALID_PARAMETER Time 超出范围
@retval EFI_DEVICE_ERROR RTC 硬件返回错误
**/
typedef EFI_STATUS(EFIAPI *EFI_SET_TIME)(
IN EFI_TIME *Time
);
时钟性能用 EFI_TIME_CAPABILITIES 表示:
- Resolution(时钟分辨率):实时时钟每秒报告的次数。
- Accuracy:时钟守时精度。
- SetsToZero:布尔类型,True 表示设置时钟的操作会清除低于时钟分辨率的计数状态。
GetWakeupTime/SetWakeupTime
GetWakeupTime 用于读取唤醒定时器的状态,SetWakeupTime 用于启用或禁用唤醒定时器。
GetWakeupTime/SetWakeupTime 服务函数原型:
typedef EFI_STATUS(EFIAPI *EFI_GET_WAKEUP_TIME)(
OUT BOOLEAN *Enabled, // 返回定时器启用或禁用状态
OUT BOOLEAN *Pending, // 定时器信号是否处于 Pending 状态
OUT EFI_TIME *Time // 返回定时器设置
);
typedef EFI_STATUS(EFIAPI *EFI_SET_WAKEUP_TIME)(
IN BOOLEAN Enable, // 启用或禁用唤醒定时器
IN EFI_TIME *Time OPTIONAL // 启用时,定时器的设置
);
系统变量服务
UEFI 系统变量服务包括:GetVariable、SetVariable、GetNextVariableName。
GetVariable 用法
GetVariable 用于根据变量名获取变量值和属性。
GetVariable 服务函数原型:
/**gRT->GetVariable
根据变量名获得系统变量的值和属性
@retval EFI_SUCCESS 成功返回
@retval EFI_NOT_FOUND 变量不存在
@retval EFI_BUFFER_TOO_SMALL 缓冲区 Data 太小
@retval EFI_INVALID_PARAMETER VariableName/VendorGuid/DataSize/Data 为空
@retval EFI_DEVICE_ERROR 设备返回错误
@retval EFI_SECURITY_VIOLATION 身份验证未通过
**/
typedef EFI_STATUS
(EFIAPI *EFI_GET_VARIABLE)(
IN CHAR16 *VariableName, // 变量名字
IN EFI_GUID *VendorGuid, // 变量所有者 GUID
OUT UINT32 *Attributes, OPTIONAL // 变量属性
// 输入值:缓冲区 Data 大小;输出值:返回到 Data 的字节数
IN OUT UINTN *DataSize,
OUT VOID *Data // 返回变量的值
);
- 属性值 Attributes 是32位无符号整数,每一位表示一种属性。
- VendorGuid 作为命名空间使用。最常用的是 gEfiGlobalVariableGuid。
SetVariable 用法
SetVariable 有三项功能:新建、更新、删除变量。
SetVariable 服务函数原型:
typedef EFI_STATUS(EFIAPI *EFI_SET_VARIABLE)(
IN CHAR16 *VariableName, // 变量名字
IN EFI_GUID *VendorGuid, // 变量所有者的 GUID
IN UINT32 Attributes, // 变量属性
IN UINTN DataSize, // 缓冲区 Data 大小
IN VOID *Data // 变量值
);
GetNextVariableName 用法
GetNextVariableName 用于获取下一个系统变量,通过这个服务可以遍历系统中的变量。
GetNextVariableName 服务函数原型:
typedef EFI_STATUS(EFIAPI *EFI_GET_NEXT_VARIABLE_NAME)(
IN OUT UINTN *VariableNameSize,
IN OUT CHAR16 *VariableName,
IN OUT EFI_GUID *VendorGuid
);
- 参数 *VariableNameSize:
- 输入时:作为缓冲区 VariableName 的字节数。
- 输出时:返回变量名字符串占用的字节数(包括字符串结尾的 NULL 字符)。
- 参数 *VariableName:
- 输入时:上一个变量的名字。
- 输出时:返回当前变量名。
- 参数 *VendorGuid:
- 输入时:上一个变量的 VendorGuid。
- 输出时:当前变量的 VendorGuid。
要启动搜索(获得第一个变量),需要 VariableName 指向空字符串。
虚拟内存服务
虚拟内存服务包括:SetVirtualAddressMap 和 ConvertPointer。这两个服务只有在运行时期间被操作系统加载器调用。
- SetVirtualAddressMap:设置系统的内存映射。
- ConvertPointer:查询指定物理地址的虚拟地址。
调用流程:
- 调用 gBS->GetMemoryMap 获得系统的内存映射。
- 调用 gBS->ExitBootServices 从 BS 转换到 RT 阶段。
- 内存映射中属性为 EfiBootServicesCode 和 EfiBootServicesData 的内存被释放,其他类型内存在 RT 阶段使用。
- 准备虚拟地址环境,生成新的内存映射表,并未内存映射表设置虚拟地址和物理地址映射。
- 调用 SetVirtualAddressMap。
- SetVirtualAddressMap 返回前,所有 EVT_SIGNAL_VIRTUAL_ADDRESS_CHANGE 类型的事件都将触发,触发函数中需要调用 ConvertPointer 将物理地址转换成虚拟地址(这些地址转换后才能在 RT 阶段使用)。
- SetVirtualAddressMap 返回。系统进入虚拟地址模式。
SetVirtualAddressMap/ConvertPointer 服务函数原型:
/**gRT->SetVirtualAddressMap
从物理地址模式转换为虚拟地址模式
@retval EFI_SUCCESS 成功返回
@retval EFI_UNSUPPORTED 系统未进入 RT 阶段或系统已进入虚拟地址模式
@retval EFI_NO_MAPPING 内存映射表中需要提供虚拟地址的项未提供虚拟地址
@retval EFI_NOT_FOUND 内存映射表中存在无效物理地址
**/
typedef EFI_STATUS(EFIAPI *EFI_SET_VIRTUAL_ADDRESS_MAP)(
IN UINTN MemoryMapSize, // VirtualMap 数组的字节数
IN UINTN DescriptorSize, // VirtualMap 数组内每个数据项的字节数
In UINT32 DescriptorVersion, // 内存映射描述符版本号
IN EFI_MEMORY_DESCRIPTOR *VirtualMap // 内存映射描述符数组
);
/**ConvertPointer
查询给定物理地址的虚拟地址
@retval EFI_SUCCESS 成功查询到虚拟地址
@retval EFI_INVALID_PARAMETER Address 为空,或 *Address 为 NULL 且 DebugDispositon 没有设置 EFI_OPTION_PTR 位
@retval EFI_NOT_FOUND 指定的物理地址不存在
**/
typedef EFI_STATUS(EFIAPI *EFI_CONVERT_POINTER)(
IN UINTN DebugDispositon,
IN OUT VOID **Address
);
*Address 作为输入参数时是物理地址,作为输出参数时返回对应的虚拟地址。ConvertPointer 通过查询系统的内存映射表计算出给定物理地址的虚拟地址。当 DebugDispositon 设置了 EFI_OPTION_PTR 标志时,输入参数 *Address 允许为空。
网友评论