UEFI基础服务——记录
学习戴正华大佬的《UEFI原理与编程》第5章记录如下。UEFI 的核心任务是为 OS 准备好软硬件运行环境。它向上暴露的接口分两种启动服务Boot Services和运行时服务Runtime Services统称为 UEFI 服务。任何 UEFI 应用或驱动都是通过系统表拿到这两个服务表的指针。5.1 系统表5.1.1 概述系统表是整个 UEFI 编程的入口。它本质上是一个全局结构体在 DXE 阶段被初始化也就是说 SEC、PEI 阶段用不了只有到了 DXE 及以后才能访问。每个 .efi 文件Image被加载到内存后UEFI 固件会调用它的入口函数并把系统表指针传进去。入口函数的签名typedef EFI_STATUS(EFIAPI *EFI_IMAGE_ENTRY_POINT) ( IN EFI_HANDLE ImageHandle, IN EFI_SYSTEM_TABLE *SystemTable );Image 和 ImageHandle 的区别Image 是加载到内存后的可执行程序实体一个数据结构ImageHandle 是这个 Image 的句柄——可以理解为 Image 的身份证用来唯一标识这个 Image。驱动/应用之间的交互比如 Protocol 的安装和查找都是通过 Handle 来关联的。具体调用链.efi 文件加载后形成 Image 对象_ModuleEntryPoint的地址被填到Image-EntryPoint然后固件执行Image-EntryPoint(ImageHandle, SystemTable)最终进入 .inf 里指定的模块入口函数。5.1.2 系统表的结构typedef struct { EFI_TABLE_HEADER Hdr; 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; EFI_CONFIGURATION_TABLE *ConfigurationTable; // 配置表数组 } EFI_SYSTEM_TABLE;几点理解1表头 EFI_TABLE_HEADERUEFI 里所有表都用EFI_TABLE_HEADER开头启动服务表、运行时服务表都是。这个 Header 里有 64 位 Signature跟 GUID 类似的思路用于标识表的类型、版本号、整表大小、CRC32 校验值。CRC32 是这样算的先把 CRC32 字段清零算整表的 CRC再填回去。如果表被篡改或者损坏校验就对不上。2控制台系统表提供了 3 套控制台ConIn键盘、ConOut屏幕、StdErr屏幕。每套包含一个设备句柄和一个对应的 Protocol。有了这些UEFI 应用不用关心底层硬件细节直接调 Protocol 的函数就行。3配置表ConfigurationTable是一个数组每一项是一对GUID 数据指针。ACPI 表、SMBIOS 表、设备树等等都挂在这上面。OS Loader 通过它获取硬件信息交给内核这个设计很巧妙——用 GUID 做索引随意扩展不用改系统表结构。5.2 启动服务启动服务表在 DXE 阶段初始化通过gST-BootServices或gBS访问。它提供的服务覆盖了 UEFI 应用在启动阶段ExitBootServices 之前几乎所有需求共 8 类。注虽然也叫表但它不像系统表那样存数据——启动服务表里基本都是函数指针。5.2.1 八类服务概览类别说明典型使用场景事件服务事件、定时器、任务优先级TPL创建回调事件、设置定时器内存管理分配/释放内存页或池驱动分配缓冲区Protocol 管理安装/卸载 Protocol、注册通知驱动提供 ProtocolProtocol 使用查找/打开/关闭 Protocol应用调用驱动服务驱动管理Connect/Disconnect 控制器加载驱动到设备Image 服务加载/卸载/启动 .efi 文件OS Loader 加载内核ExitBootServices结束启动服务移交控制权OS Loader 接管系统其他CopyMem、SetMem、CRC32、Stall 等通用工具操作启动服务表的结构体非常大100 行但理清分类后其实很好记。额外提几点TPL任务优先级RaiseTPL/RestoreTPL 用来在关键区域提升优先级防止其他事件打断。跟中断屏蔽的思路类似但用的是软件优先级。事件UEFI 的事件模型支持多种类型通知事件、定时器事件等。CreateEvent 的时候可以指定回调函数和触发条件核心是异步通知机制。Protocol 函数的历史包袱InstallProtocolInterface和ReinstallProtocolInterface是早期版本遗留下来的新代码应该用InstallMultipleProtocolInterfaces它一次可以安装多个 Protocol而且会在失败时自动回滚。5.2.2 内存管理服务详解内存管理服务分三组1AllocatePool / FreePool —— 按字节分配最常用的内存分配方式类似 C 的 malloc/free。必须成对调用。CHAR16 *Buffer NULL; gBS-AllocatePool(EfiBootServicesCode, 1024, (VOID**)Buffer); gBS-FreePool(Buffer);内存类型是关键参数。AllocatePool第一个参数指定内存类型这决定了内存在 ExitBootServices 之后的命运EfiBootServicesCode/EfiBootServicesDataExitBootServices 后固件回收。用于 UEFI 驱动和应用自身。EfiLoaderCode/EfiLoaderData由 OS Loader 和 OS 决定是否回收。用于存放交给内核的数据比如内存映射表。EfiACPIReclaimMemoryACPI 表初始化完成后回收。EfiRuntimeServicesCode/EfiRuntimeServicesData等ExitBootServices 后保留给运行时服务继续用。理解这个很重要——如果你在 Boot Service 里分配了一块EfiBootServicesData内存想在 RT 阶段继续用那就会挂掉因为这块内存已经被回收了。2AllocatePages / FreePages —— 按物理页分配当你需要整页内存或者不想分配跨页时用这个。驱动开发中很常见因为有些硬件 DMA 要求物理连续而且 DMA 缓冲区通常以页为单位。EFI_PHYSICAL_ADDRESS pages 0; // 0 表示让固件自己选地址 gBS-AllocatePages(AllocateAnyPages, EfiBootServicesCode, 3, pages); // 拿到的是物理地址操作内存前要转成指针 CHAR16 *Str (CHAR16*)pages; gBS-FreePages(pages, 3);重要AllocatePages返回的是物理地址EFI_PHYSICAL_ADDRESS不是指针。在 32 位 UEFI 上如果物理内存超过 4GBAllocateAnyPages可能分配到 4G 以上的物理地址32 位指针够不着——这时候要用AllocateMaxAddress限制允许分配的最大物理地址。Type参数的三种分配方式AllocateAnyPages任意地址AllocateMaxAddress分配的页面物理地址不能超过传入值AllocateAddress强制分配到指定物理地址3GetMemoryMap —— 获取系统内存布局这是 ExitBootServices 之前必须调用的服务。它返回整个系统的内存映射表每个条目EFI_MEMORY_DESCRIPTOR描述了一块内存的类型、物理地址、虚拟地址、页数和属性。调用有个经典的两步走模式先传个 0 大小的 Buffer 让固件告诉你需要多大分配好再调一次。MapKey是当前内存映射的版本标识ExitBootServices 时要传这个 Key防止内存映射在你获取之后被修改如果 Key 变了说明驱动对内存做了操作就得重新获取。5.2.3 启动服务的生命周期总结gBS从 DXE 初始化开始可用到gBS-ExitBootServices()成功后结束。ExitBootServices 执行后被回收的启动服务表中除EFI_TABLE_HEADER外的所有函数指针都失效ConIn、ConOut、StdErr相关的控制台设备/Protocol 失效EfiBootServicesCode和EfiBootServicesData类型的内存被回收保留的gRT—— 运行时服务继续可用ConfigurationTable—— 配置表保留ACPI、SMBIOS 等 HW 信息FirmwareVendor和FirmwareRevision字符串调试技巧在 ExitBootServices 前后分别看gST的地址和其成员地址可以确认哪些区域被释放了。书中提到可以用asm int 3打调试断点来观察。MapKey的坑如果第一次 GetMemoryMap 拿到 MapKey 后在调用 ExitBootServices 之前又有代码调了分配/释放内存MapKey 就对不上了ExitBootServices 会失败。正确的做法是检测到失败后重新 GetMemoryMap 再调。5.3 运行时服务运行时服务同样在 DXE 阶段初始化但它的生命周期比启动服务长得多——一直到操作系统运行期间都存活给 OS 提供底层服务。gRT提供了 4 类服务类别说明时间服务读写硬件 RTC、唤醒定时器虚拟内存SetVirtualAddressMap、ConvertPointer变量服务读写 UEFI 变量Get/Set Variable其他ResetSystem重启、Capsule 更新、查询变量信息5.3.1 时间服务GetTime / SetTime直接操作 RTC 硬件。时间结构体包含年月日时分秒纳秒和时区。注意几个细节EFI_TIME有两个 Padding 字节Pad1、Pad2这是为了字节对齐TimeZone-1440 到 1440单位分钟UTC 时区是 0。还有一个特殊值 2047 表示时区未知Daylight夏令时标志Nanosecond字段精度看起来很高但实际受限于 RTC 硬件精度EFI_TIME_CAPABILITIES描述时钟硬件能力Resolution时钟每秒脉冲数Accuracy精度单位 ppm百万分之一SetsToZero是否在设置时间时清零亚秒计数器GetWakeupTime / SetWakeupTime操作系统用这个实现定时开机——比如笔记本在 AC 状态下设置唤醒时间关机后 RTC 到时触发上电。5.3.2 变量服务UEFI 变量是运行在 Flash 上的键值存储用 (GUID Name) 作为唯一标识。OS 装好后的启动项、Secure Boot 密钥、固件设置都存成变量。核心三个接口GetVariable读取变量SetVariable写入变量可以附加属性如 Non-Volatile、BootService、Runtime、Authenticated Write 等GetNextVariableName遍历所有变量5.3.3 虚拟内存服务这两个接口只能在 ExitBootServices 之后调用。由于 OS 启用分页后物理地址就不能直接用了所以 RT 阶段要先把运行时服务使用的物理地址映射到虚拟地址。调用流程SetVirtualAddressMap传入内存映射表物理地址→虚拟地址的映射固件内部遍历并更新。然后ConvertPointer用于将某个指针的指向从物理地址改为虚拟地址——注意它转换的是指针的指针。小结UEFI 的这套服务设计其实很清晰系统表 导航中心从这里拿到一切启动服务 DXE 到 OS Loader 期间的工具箱内存管理 Protocol 操作 Image 加载功能最强运行时服务 OS 运行后还能用的精简接口时间 变量 复位理解生命周期这个概念特别重要什么内存在什么时候分配、什么时候回收什么服务在什么时候可用。

相关新闻