UEFI 中的杂项知识总结-Protocol Handle 机制的详细介绍

⭐️UEFI 中的 Protocol Handle 机制

一、ResetVector

Reset Vector(复位向量) 是 CPU(或其他处理器)在上电复位(Power-on Reset)或手动复位(Reset信号触发)后,无条件跳转去执行的第一条指令的地址。

x86 实模式的典型值是 0xFFFFFFF0

CPU 收到 Reset 信号后的大致动作:

  1. 所有寄存器复位到默认值(包括 PC)。

  2. PC 被强制加载 Reset Vector 地址处的内容(这个内容通常是一条跳转指令)。

  3. 从该地址开始取指、译码、执行 → 进入Boot ROM / BIOS / Bootloader。

通常这个地址里放的不是真正的第一条有用代码,而是一条跳转指令,比如:

; x86 实模式例子(物理地址 0xFFFFFFF0)
jmp far 0xF000:0xXXX   ; 跳到BIOS入口

真正的启动代码(复位处理程序 reset handler)一般放在 Flash、ROM 或固化的 BootROM 里。

以 x86 为例,主板一上电,CPU 的 PC 被设为 0xFFFFFFF0(物理地址,相当于 FFFF:FFF0 段:偏移)。这个地址属于主板上的 SPI Flash 里的 UEFI 固件镜像(通过芯片组如 Intel PCH 映射到 4GB 地址空间顶部,通常 4-16MB 大小)。0xFFFFFFF0 处仍是一条 jmp 指令(通常是 jmp far 或短跳转),跳到 UEFI 固件的真正入口(Reset Vector Code,位于固件镜像末尾附近)。

南北桥到芯片组的演化:

序号 芯片组负责的事 以前是谁管(2000年以前)
1 直接连接内存条(DDR5) 以前是北桥
2 提供 PCIe 通道给显卡、NVMe SSD、网卡等 以前是北桥
3 提供 USB 口(包括 USB4/雷电) 以前是南桥
4 提供 SATA 硬盘接口、声卡、网卡 以前是南桥
5 管理电源、上电时序、风扇控制、RGB灯 以前是超级I/O + 南桥
6 运行 Intel ME / AMD PSP(那块“后门”固件) 以前没有

二、OS loader 与 TSL

在之前的介绍中我们知道 TSL 阶段的作用,即作为 BDS 和 RT 之间的过渡阶段。此时系统的主要任务是从平台初始化转向加载操作系统内核。那么用于加载操作系统的 OS Loader 与 TSL 之间有什么关系呢?OS Loader 是 TSL 阶段的实际执行主体,负责加载并启动操作系统。

在 TSL 阶段 Boot Services 仍可用(直到 ExitBootServices() 被调用)。OS Loader 可以调用 UEFI 协议和服务来读取文件系统、加载内核镜像等。一旦 OS Loader 完成内核加载并准备跳转到操作系统入口点,会调用 ExitBootServices(),标志着 TSL 阶段结束,进入 RT 阶段。

OS Loader 是负责加载操作系统内核并将其控制权转移给内核的程序。在 UEFI 环境下,它具有以下关键功能:

  1. 定位操作系统镜像
    • 通过 UEFI 文件系统协议(如 Simple File System Protocol)访问 ESP(EFI System Partition)。
    • 读取配置文件(如 bootmgfw.efi for Windows, grubx64.efi for Linux)。
  2. 加载内核与初始化数据
    • 将操作系统内核(如 vmlinuzntoskrnl.exe)和可能的 initramfs/initrd 加载到内存。
    • 解析启动参数(如来自 UEFI NVRAM 中的 Boot#### 变量)。
  3. 准备执行环境
    • 设置必要的内存布局、页表(某些情况下)、传递启动信息(如 ACPI 表、内存映射等)。
    • 调用 ExitBootServices() 终止 UEFI Boot Services,释放对硬件的控制。
  4. 跳转到操作系统入口点
    • 将 CPU 控制权移交给操作系统内核,完成启动过程。

示例:

  1. UEFI 固件在 BDS 阶段找到 ESP 分区中的 \EFI\SYSTEMD\SYSTEMD-BOOTX64.EFI
  2. 进入 TSL 阶段,执行该 EFI 应用(即 OS Loader)。
  3. systemd-boot 显示启动菜单,用户选择内核。
  4. 加载 vmlinuzinitrd 到内存,设置 cmdline。
  5. 调用 ExitBootServices(),跳转到内核入口。
  6. UEFI 进入 RT 阶段,操作系统全面接管。

不通平台和系统使用的 OS Loader 示例:

平台类型 架构 操作系统 典型启动流程
嵌入式 ARM Linux BootROM → SPL → U-Boot → Kernel
PC x86 Linux BIOS/UEFI → OS Loader (GRUB/systemd-boot) → Kernel
PC x86 Windows UEFI → Windows Boot Manager → winload.efi → NTOSKRNL
嵌入式/PC ARM Windows(少见) ARM64 UEFI → bootmgfw.efi → Windows Kernel

2.1 通用概念

  1. Bootloader
    • Bootloader 是一段在操作系统内核运行前执行的代码,负责初始化硬件、加载内核、传递启动参数,在不同平台上名称和层级不同。
  2. BIOS vs UEFI
    • BIOS:传统 x86 PC 固件,16 位实模式,功能有限。
    • 现代标准固件,支持 32/64 位,模块化、可扩展,有驱动模型和文件系统支持。
    • ARM 平台一般不用 BIOS,直接使用 BootROM + 自定义 Bootloader 或 ARM Trusted Firmware (ATF) + UEFI。

由于 BIOS 的概念深入人心,如今我们一般称传统的固件叫做 Legacy BIOS,现代固件叫做 UEFI BIOS。

2.2 不通平台的启动流程对比

  1. 嵌入式 Linux(ARM 架构,如 Raspberry Pi, i.MX6)

    BootROM(芯片内置) 
    → SPL(可选,如 U-Boot SPL) 
    → U-Boot(主 Bootloader) 
    → Linux Kernel(zImage/Image + dtb + initramfs) 
    → 用户空间(init)
    

    嵌入式设备启动的特点:

    • 无标准固件:不像 PC 有 BIOS/UEFI,依赖 SoC 厂商提供的 BootROM。
    • BootROM:固化在芯片中,上电后自动从预设介质(SD/eMMC/NAND/SPI Flash)加载第一段代码(通常是 SPL 或直接 U-Boot)。
    • U-Boot:最常用的嵌入式 Bootloader,支持命令行、脚本、设备树(DTB)传递。
    • 设备树(Device Tree):ARM Linux 必须通过 Bootloader 传递 .dtb 文件描述硬件。
    • 无 ExitBootServices():因为没有 UEFI,直接跳转到内核。
  2. PC 上的 Linux(x86/x86_64 架构,UEFI 模式)

    UEFI Firmware 
    → OS Loader(如 GRUB2 / systemd-boot / shim.efi) 
    → Linux Kernel(vmlinuz + initrd) 
    → 用户空间
    

    桌面端 Linux 启动特点

    • 标准化固件:UEFI 提供统一接口(如 EFI System Partition, ESP)。
    • ESP 分区:FAT32 格式,存放 .efi 可执行文件(如 grubx64.efi)。
    • OS Loader:负责加载内核和 initrd,解析 /etc/default/grub 等配置。
    • 无需设备树:x86 硬件信息通过 ACPI 表传递。
    • 调用 ExitBootServices():OS Loader 在跳转内核前关闭 UEFI Boot Services。

    BIOS 模式(Legacy)已逐渐淘汰,流程为:BIOS → MBR → GRUB Stage1/Stage2 → Kernel

  3. PC 上的 Windows(x86/x86_64,UEFI 模式)

    UEFI Firmware 
    → \EFI\Microsoft\Boot\bootmgfw.efi(Windows Boot Manager) 
    → winload.efi 
    → ntoskrnl.exe(Windows 内核) 
    → Session Manager (smss.exe) → 用户登录
    
    • 完全依赖 UEFI(现代 Windows 不再支持纯 Legacy BIOS 安装)。
    • Secure Boot:验证 bootmgfw.efi 和 winload.efi 的数字签名。
    • BCD(Boot Configuration Data):替代旧的 boot.ini,存储在 ESP 或系统分区。
    • 同样调用 ExitBootServices():由 winload.efi 完成。

    注意:Windows 的 Boot Manager 本身就是一个 UEFI 应用(.efi 文件)。

  4. ARM 架构的 Windows

    目前 ARM 架构与 Windows 的组合在市场上还不常见,但有,可通过转译实现,部分应用能够原生运行。目前较有前景的芯片例子:骁龙X Elite。

    ARM64 UEFI Firmware(由 SoC 提供,如 Qualcomm Snapdragon) 
    → bootmgfw.efi 
    → winload.efi 
    → ntoskrnl.exe
    
    • 与 x86 Windows 启动流程几乎一致,只是架构为 ARM64。
    • 需要 ARM64 版本的 UEFI 固件(通常由 OEM 集成在 SoC 中)。
    • 微软要求 Secure Boot 和 UEFI 支持。
  5. ARM64 服务器/开发板

    启动流程类似嵌入式 ARM Linux,但部分高端 ARM 服务器支持 UEFI + ACPI(而非设备树)。
    例如:EDK II UEFI → GRUB for ARM64 → vmlinuz(使用 ACPI 表)

    • 低端 ARM(嵌入式):U-Boot + Device Tree
    • 高端 ARM(服务器/PC):UEFI + ACPI(更接近 x86 PC 模式)

总结:

维度 嵌入式 Linux (ARM) PC Linux (x86 UEFI) PC Windows (x86 UEFI) ARM PC (Windows/Linux)
固件 BootROM(厂商定制) 标准 UEFI 标准 UEFI ARM64 UEFI(OEM 提供)
Bootloader U-Boot / Barebox GRUB / systemd-boot bootmgfw.efi U-Boot(嵌入式)或 UEFI + GRUB/bootmgfw
配置存储 环境变量(U-Boot) grub.cfg / kernel cmdline BCD BCD(Win)或 U-Boot env(Linux)
硬件描述 Device Tree (.dtb) ACPI ACPI DTB(嵌入式)或 ACPI(高端 ARM)
是否调用 ExitBootServices ❌(无 UEFI) ✅(若使用 UEFI)
Secure Boot 通常无(除非自实现) 可选(shim + signed kernel) 强制(Win 11 要求) 支持(Win on ARM 强制)
典型介质 eMMC / SPI Flash / SD NVMe / SATA SSD(ESP 分区) NVMe SSD(ESP) eMMC / UFS / NVMe

三、GRUB

GRUB 是 Linux 和类 Unix 系统中最著名、最常用的 Bootloader 之一。全称 GRand Unified Bootloader,目前主流使用的是 GRUB 2(一般就读 GRUB),早期版本 GRUB Legacy 已基本淘汰。

第二节中介绍了 TSL 阶段执行的 OS Loader,而 GRUB 就是 Linux 系统启动最常用的 Loader 之一。

GRUB 的工作流程:

  1. 显示启动菜单
  2. 从硬盘/SSD/U盘等设备读取操作系统内核文件(如 vmlinuz
  3. 加载初始内存盘(initrd/initramfs)
  4. 传递启动参数给内核(如 root=/dev/sda2 quiet splash)
  5. 将控制权交给操作系统内核,完成启动交接

💡 在 UEFI 系统中,GRUB 本身是一个 .efi 可执行文件(如 grubx64.efi),由 UEFI 固件直接加载运行。

UEFI Firmware
    ↓
加载 ESP 分区中的 \EFI\ubuntu\grubx64.efi(即 GRUB)
    ↓
GRUB 读取 /boot/grub/grub.cfg(配置文件)
    ↓
显示启动菜单(Ubuntu / Advanced options / Windows Boot Manager 等)
    ↓
用户选择一项 → GRUB 加载对应的 vmlinuz + initrd
    ↓
GRUB 设置内核命令行参数(如 root=UUID=...)
    ↓
跳转到 Linux 内核入口,移交控制权

GRUB 的应用场景包括:

场景 说明
多系统启动 同一台电脑装了 Windows + Linux,GRUB 菜单让你选择进哪个系统
多内核选择 Linux 更新后保留旧内核,GRUB 允许你选择用哪个版本启动
救援模式 可通过 GRUB 编辑启动参数进入单用户模式或修复系统
网络启动(PXE) 高级用法,GRUB 支持从网络加载内核

在传统 BIOS 模式下,GRUB 会分阶段加载(Stage 1 → Stage 1.5 → Stage 2),但在 UEFI 模式下,这个过程被简化了,因为 UEFI 本身提供了文件系统访问能力。

GRUB 关键组件

组件 作用
grub-install 安装 GRUB 到磁盘或 ESP 分区的工具(如 grub-install /dev/sda
grub-mkconfig 自动生成 /boot/grub/grub.cfg 的命令(通常通过 update-grub 调用)
grub.cfg 主配置文件,定义菜单项、内核路径、启动参数等(不要手动编辑!
/etc/default/grub 用户可编辑的 GRUB 配置模板,运行 update-grub 后生效
grub-efi UEFI 版本的 GRUB 包(如 Debian/Ubuntu 中的 grub-efi-amd64

各种 Bootloader 总结

Bootloader 平台 特点
GRUB 2 x86/x86_64(PC/Linux) 功能强大,支持脚本、主题、多系统
systemd-boot UEFI-only PC 轻量、简单,仅支持 EFI 分区内的内核
U-Boot 嵌入式 ARM/MIPS 用于开发板,支持设备树、网络启动
rEFInd UEFI 多系统 图形化菜单,自动检测所有 OS
Windows Boot Manager Windows 仅用于 Windows,通过 BCD 管理启动项

四、Boot Services - GOP

GOP 即 Graphics Output Protocol,图形输出协议,是 UEFI 标准中用于提供基本图形显示能力的核心协议之一。GOP 属于 Boot Services 阶段可用的接口。它允许 UEFI 应用程序(如 OS Loader、UEFI Shell、图形化启动菜单)以像素级方式在屏幕上绘制图像、文字、背景等。在调用 ExitBootServices() 后,GOP 协议失效,但屏幕内容通常保留。操作系统内核需自行接管显卡驱动。

功能 说明
获取屏幕分辨率 如 1920×1080、1280×720 等
获取像素格式 PixelBlueGreenRedReserved8BitPerColor(BGR)
访问帧缓冲区(Frame Buffer) 直接写入显存(物理地址)进行绘图
设置显示模式 切换分辨率(如果硬件支持)
绘制简单图形 配合其他库(如 uefi-graphics-lib)可画线、矩形、位图

💡 GOP 本身不提供“画线”“写字”等高级 API,它只暴露 Frame Buffer。高级图形操作需上层软件实现(如 GRUB 的 gfxterm、systemd-boot 的图形菜单)。

在 EDK II 或 UEFI 开发中,GOP 主要通过以下结构体使用:

typedef struct _EFI_GRAPHICS_OUTPUT_PROTOCOL {
    EFI_GRAPHICS_OUTPUT_PROTOCOL_QUERY_MODE  QueryMode;
    EFI_GRAPHICS_OUTPUT_PROTOCOL_SET_MODE    SetMode;
    EFI_GRAPHICS_OUTPUT_PROTOCOL_BLT         Blt;     // Block Transfer(位块传输)
    EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE        *Mode;
} EFI_GRAPHICS_OUTPUT_PROTOCOL;
  • Blt 函数:用于将像素数据复制到屏幕(或从屏幕读取),支持填充颜色、拷贝图像等。
  • Mode->Info:包含当前分辨率、像素格式、帧缓冲区物理地址(FrameBufferBase)。

各种 Loader 的使用

  1. GRUB 图形启动菜单

    • GRUB 使用 GOP 进入图形模式(gfxmode=1920x1080

    • 显示背景图、高亮菜单项

    • 依赖 GOP 的 Blt 操作刷新屏幕

  2. systemd-boot

    • 支持简单的图形启动界面(需启用)

    • 使用 GOP 清屏并绘制文字菜单

  3. Windows Boot Manager

    • 在 UEFI 模式下使用 GOP 显示 Windows 徽标和加载动画
  4. UEFI Shell 图形扩展

    • 可运行 .efi 图形程序(如诊断工具、Logo 显示器)

需要注意的是,GOP 是 Boot Services ,OS 没法调用。Frame Buffer 信息是由 UEFI 通过 ACPI 或 Device Tree 传递给 OS 的。虽然 OS 不能调用 GOP 协议,但 UEFI 会在 ExitBootServices() 之前,把关键图形信息“固化”到标准位置,供操作系统读取。

五、ESP 目录

ESP(EFI System Partition,EFI 系统分区)是操作系统与 UEFI 固件之间进行启动交互的“桥梁”。它本质上是一个格式化为 FAT32(有时也支持 FAT16/FAT12)的磁盘分区。用于存放 UEFI 可执行文件(.efi)、驱动、配置文件等,供 UEFI 固件直接读取和运行。OS 的 Loader 放在 ESP 目录,这样 Loader 直接读取当前路径下的 xxx.efi 文件并解析运行即可。

特性 说明
文件系统 FAT32
分区类型 GUID C12A7328-F81F-11D2-BA4B-00A0C93EC93B(GPT 分区表) 或类型 ID 0xEF(MBR 分区表,较少见)
大小建议 Windows 要求 ≥100 MB,Linux 建议 ≥512 MB(便于多内核共存)
是否可分配盘符 不应分配盘符(Windows 默认隐藏,Linux 通常挂载到 /boot/efi
内容可读写 是,但需谨慎操作(误删会导致无法启动!)

ESP 的典型目录结构:

[ESP 分区根目录]
├── EFI/
│   ├── BOOT/
│   │   └── BOOTX64.EFI        ← 默认 fallback 启动文件(x86_64 架构)
│   ├── Microsoft/
│   │   └── Boot/
│   │       ├── bootmgfw.efi   ← Windows Boot Manager
│   │       └── BCD            ← Windows 启动配置数据库
│   └── ubuntu/
│       ├── grubx64.efi        ← Ubuntu 的 GRUB 启动器
│       ├── shimx64.efi        ← 支持 Secure Boot 的签名代理
│       └── grub.cfg           ← GRUB 的简单配置(指向 /boot/grub/grub.cfg)
└── [其他可能文件]
    ├── drivers/               ← UEFI 驱动(少见)
    └── tools/                 ← UEFI 工具(如 fwupdate)
  • BOOTX64.EFI:UEFI 的“备用启动项”。如果 NVRAM 中的启动项损坏,固件会自动尝试从此路径加载。
  • *.efi 文件:都是 PE32+ 格式的可执行程序(类似 Windows 的 .exe,但专为 UEFI 环境编译)。
  • BCD:Windows 的启动配置,二进制格式,用 bcdedit 管理。
  • grub.cfg(在 ESP 中):通常只包含一行 configfile 指向真正的配置(位于 Linux 的 /boot/grub/grub.cfg)。

启动流程示例(UEFI 模式):

  • 电脑上电 → UEFI 固件初始化
  • 固件读取 NVRAM 中的启动项(如 Boot0001: ubuntu)
  • 根据启动项路径(如 \EFI\ubuntu\grubx64.efi),从 ESP 分区加载该 .efi 文件
  • 执行 GRUB → GRUB 再从普通 ext4 分区加载 Linux 内核
  • 完成启动

在第一章讲 EDKII 环境搭建的时候,命令

qemu-system-x86_64 \
	-bios /home/ayuan/run-ovmf/bios.bin \
	-drive format=raw,file=fat:rw:/home/ayuan/run-ovmf/hda-contents \ 
	-m 1024M

中的 -drive 就给出了 ESP 的路径。

在 Linux 系统中通常挂载在 /boot/efi(/boot/efi/EFI)。

六、用到的一些 Services

完成启动的一些功能需要依赖的启动或者运行时服务,非常明确的有以下几个

6.1 Boot Services - Memory Allocation Services

Memory Allocation Services 为内存分配服务,用于在操作系统加载前动态申请和释放内存,这些服务通过 EFI_BOOT_SERVICES 结构体中的函数指针提供。

Memory Allocation Services 在 EDKII 中的组织形式:

MdeModulePkg/
└── Core/
    └── Dxe/
        ├── Mem/
        │   ├── Pool.c          ← Pool 内存分配(AllocatePool/FreePool)
        │   ├── Page.c          ← 页面内存分配(AllocatePages/FreePages)
        │   └── MemoryMap.c     ← GetMemoryMap 实现
        ├── Core/
        │   └── DxeMain.c       ← 初始化内存服务,设置 gBS 表
        └── Include/
            └── CoreData.h      ← 内存管理全局变量声明

UEFI 内存分配服务主要包括以下 4 个核心函数

函数名 作用
AllocatePages 页(Page) 为单位分配物理连续内存
FreePages 释放通过 AllocatePages 分配的内存
AllocatePool 内存池(Pool) 中分配小块内存(类似 malloc
FreePool 释放通过 AllocatePool 分配的内存

详细接口说明

  1. AllocatePages

    // 为内核镜像分配大块连续内存
    // 自动 4KB 对齐
    typedef
    EFI_STATUS
    (EFIAPI *EFI_ALLOCATE_PAGES)(
       IN     EFI_ALLOCATE_TYPE     Type,        // 分配策略(任意地址,指定地址*Memory等)
       IN     EFI_MEMORY_TYPE       MemoryType,  // 内存用途类型
       IN     UINTN                 Pages,       // 要分配的页数
       IN OUT EFI_PHYSICAL_ADDRESS  *Memory      // 输入/输出参数,返回分配的物理地址
    );
    
  2. FreePages

    // 释放之前通过 AllocatePages 分配的内存
    typedef
    EFI_STATUS
    (EFIAPI *EFI_FREE_PAGES)(
       IN EFI_PHYSICAL_ADDRESS  Memory,
       IN UINTN                 Pages
    );
    
  3. AllocatePool

    // 从内存池中分配指定字节数的小块内存(内部由固件管理堆)
    // 内存不一定物理连续,但逻辑上连续
    // 适合分配字符串、结构体、临时缓冲区等
    // 通常 8 字节对齐(具体由固件实现决定)
    typedef
    EFI_STATUS
    (EFIAPI *EFI_ALLOCATE_POOL)(
    IN  EFI_MEMORY_TYPE   PoolType,  // 内存类型(通常用 EfiLoaderData)
    IN  UINTN             Size,      // 字节数(不要求对齐到页)
    OUT VOID              **Buffer   // 返回分配的虚拟地址(在 UEFI 应用地址空间中)
    );
    
  4. FreePool

    // 释放 AllocatePool 分配的内存
    typedef
    EFI_STATUS
    (EFIAPI *EFI_FREE_POOL)(
       IN VOID  *Buffer
    );
    
  5. GetMemoryMap

    // 在退出启动服务(ExitBootServices)之前,获取系统当前的内存布局信息。
    // 核心作用是讲物理内存的哪些地址范围被什么类型的数据占用了(比如被UEFI固件、加载的EFI程序、或者系统保留内存等),以及这些内存区域的属性(比如是否可写、是否可执行)。
    typedef
    EFI_STATUS
    (EFIAPI *EFI_GET_MEMORY_MAP) (
       IN OUT UINTN                  *MemoryMapSize,   // 输入: 为 MemoryMap 缓冲区分配的大小; 
                                                       // 输出: 实际需要的内存地图大小
       OUT VOID                      *MemoryMap,       // 指向缓冲区的指针,用于存放内存描述符数组。每个描述符代表一个连续的内存区域。
       OUT UINTN                     *MapKey,          // 代表了当前内存地图的“版本”。每次内存布局发生变化(比如你又分配了一块内存),这个 MapKey 就会改变。
       OUT UINTN                     *DescriptorSize,  // 返回单个内存描述符结构体(EFI_MEMORY_DESCRIPTOR)的大小。
       OUT UINT32                    *DescriptorVersion// 内存描述符结构体的版本号
    );
    
    // 内存描述符
    typedef struct {
       UINT32                Type;            // 内存类型
       EFI_PHYSICAL_ADDRESS  PhysicalStart;   // 物理起始地址
       EFI_VIRTUAL_ADDRESS   VirtualStart;    // 虚拟起始地址(Boot Services阶段通常为0)
       UINT64                NumberOfPages;   // 该区域有多少个4KB页面
       UINT64                Attribute;       // 内存属性
    } EFI_MEMORY_DESCRIPTOR;
    
    • 在调用 ExitBootServices() 时,必须传入参数 MapKey。UEFI会检查你传入的Key是否与当前最新的Key一致。如果不一致,说明在你获取地图和退出服务之间内存布局变了,ExitBootServices() 会失败,你需要重新获取地图并再次尝试。

    使用流程:
    第一次调用(获取所需大小):

    • 将 MemoryMap 指针设为 NULL。
    • 将 MemoryMapSize 指向一个变量(比如设为0)。
    • 调用 GetMemoryMap()。函数会失败(返回 EFI_BUFFER_TOO_SMALL),但会在 MemoryMapSize 指向的变量中填入实际需要的缓冲区大小。
    • 分配缓冲区:使用上一步获取的大小,通过 AllocatePool() 或 AllocatePages() 分配足够大的内存缓冲区。

    第二次调用(获取真实数据):

    • 将新分配的缓冲区地址和大小传入 GetMemoryMap()。
    • 这次调用应该会成功返回 EFI_SUCCESS,并填充 MemoryMap、MapKey 等所有输出参数。
    • 立即调用 ExitBootServices():
      拿到 MapKey 后,尽快调用 ExitBootServices(ImageHandle, MapKey),确保内存地图没有失效。

    对于内存描述符,Type 字段尤为重要,它定义了内存的用途,常见的有:

    • EfiConventionalMemory: 普通可用内存,这是操作系统可以自由分配和使用的。
    • EfiBootServicesCode/Data: Boot Services的代码和数据,在调用ExitBootServices后会变为EfiConventionalMemory。
    • EfiRuntimeServicesCode/Data: 运行时服务,退出Boot Services后仍可使用。
    • EfiMemoryMappedIO: 内存映射的I/O空间。
    • EfiReservedMemoryType: 保留内存,不能使用。

接口的调用方式

#include <Uefi.h>
#include <Library/UefiLib.h>
#include <Library/BaseMemoryLib.h>

EFI_STATUS
EFIAPI
UefiMain (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
)
{
    EFI_BOOT_SERVICES *gBS = SystemTable->BootServices;
    EFI_STATUS Status;
    VOID *Buffer;
    EFI_PHYSICAL_ADDRESS PhysAddr;

    // 示例1:分配 2 页(8KB)物理内存
    PhysAddr = 0;
    Status = gBS->AllocatePages(
        AllocateAnyPages,
        EfiLoaderData,
        2,               // 2 pages = 8192 bytes
        &PhysAddr
    );
    if (!EFI_ERROR(Status)) {
        Print(L"Allocated pages at 0x%llx\n", PhysAddr);
        // 使用内存...
        gBS->FreePages(PhysAddr, 2);
    }

    // 示例2:分配 256 字节池内存
    Status = gBS->AllocatePool(
        EfiLoaderData,
        256,
        &Buffer
    );
    if (!EFI_ERROR(Status)) {
        ZeroMem(Buffer, 256);
        Print(L"Pool allocated at %p\n", Buffer);
        gBS->FreePool(Buffer);
    }

    return EFI_SUCCESS;
}

上面程序中涉及 EFI_SYSTEM_TABLE,这里简单介绍一下。通俗来说它就像是一个“目录”或“索引”,它本身不包含所有功能,但它告诉你去哪里找这些功能,包括重要的BootServicesBootServices指针,以及配置表。

当一个 UEFI 应用程序(如 Windows 的 bootmgfw.efi 或Linux的 grubx64.efi)被加载执行时,UEFI 固件会通过一个标准化的入口点(通常是EFI_IMAGE_ENTRY_POINT)传递两个最重要的参数:

  • ImageHandle: 一个代表当前应用程序本身的句柄。
  • SystemTable: 一个指向 EFI_SYSTEM_TABLE 结构体的指针。

配置表包括:

  • ACPI (RSDP): 指向ACPI根系统描述符表的指针,这是OS进行电源管理的基石。
  • SMBIOS: 指向SMBIOS表的指针,包含硬件资产信息(型号、序列号、内存布局等)。
  • MPS: 旧的多处理器规范表(较少用)。
  • 显卡GOP信息等。

6.2 Boot Services - Protocol Handler Services

Protocol Handler Services 即协议处理器服务,专门用于安装、卸载、打开、关闭、查询和管理 Protocol(协议)与 Handle(句柄)之间的关系。

其在 EDKII 中的组织:

MdeModulePkg/Core/Dxe/
├── Core/                 ← DXE Core 主体
│   ├── Hand/             ← Handle & Protocol 管理(Protocol Handler Services)
│   ├── Sched/            ← 调度器(驱动加载等)
│   └── Event/            ← 事件与定时器服务
└── Library/              ← Boot Services 接口封装(如 gBS 初始化)

与上一节中的内存分配服务子类中的接口调用方式一样,gBS 指向一个静态分配的 EFI_BOOT_SERVICES 结构体,其函数指针指向这些实现。

既然 Protocol Handler Services 提供协议和句柄管理,所以在介绍它之前,先了解一下 Protocol 和 Handle 是做什么的。

6.2.1 Protocol 和 Handle 概述

Protocol 本质上是结构体加函数指针结合成的一套函数接口,Handle 则为一个资源对象的标识。Handle 代表一个资源实例,那么这个实例提供哪些服务呢,由安装在 Handle 上的 Protocol 决定。简单来说,我们找到一个资源对象,要调用这个对象提供的服务,就要调用 Handle -> Protocol -> 具体函数

如果你有 Linux 驱动开发经验,一定对字符设备的注册流程很熟悉,一个字符设备 device 会对应着一系列的文件操作函数集合 fileoperations,Handle 就类似于 device,Protocol 就类似于 Fileoperations。

UEFI 涉及这个 Protocol Handle 机制的动机在哪里呢。这有类似于 Linux 的分层涉及思想了,在 UEFI 中,驱动、应用、Boot Manager 之间 不直接调用彼此的函数,不同层级之间程序的调用在 Linux 通过系统调用实现,在 UEFI 中即使用 Protocol Handle 机制。

下面举个 UEFI 驱动的例子,看代码更容易理解,示例代码由 AI 生成。

1. 驱动程序

跟 Linux 类似,UEFI 也由驱动和应用程序,设计思想与 Linux 很类似。简单来说 Driver 是服务提供者,装到 Handle 上的 Protocol, App 是服务使用者,通过 Protocol 来做事。

对比项 UEFI Driver UEFI Application
作用 向系统“提供服务” 使用服务、执行一次性任务
提供内容 Protocol(函数接口) 使用协议、执行逻辑
加载时机 Boot Services(加载得早) 通常由 Shell/Boot Manager 运行
生命周期 可持续存在直到 ExitBootServices 运行完就退出
被谁调用? 由系统调用(Driver Binding) 由用户或 Boot Manager 调用
能否处理硬件? 是(PCI/USB/Block etc.) 一般不处理硬件
是否参与设备枚举? 是(ConnectController)
是否能被自动绑定? 是(基于 GUID 和设备路径)
是否必须遵循 Driver Model? 不需要

举个例子:文件系统服务

驱动(Driver)实现文件系统协议 EFI_SIMPLE_FILE_SYSTEM_PROTOCOL,安装在某个 Handle 上。

BlockDeviceHandle
    ├── BlockIoProtocol
    └── SimpleFileSystemProtocol

它提供读目录、读文件的服务。假如你的 UEFI 程序需要读文件:

LocateProtocol(&gEfiSimpleFileSystemProtocolGuid, ...);

总的来说驱动负责控制、初始化、封装硬件,应用负责用这些已封装好的服务。

#include <Uefi.h>
#include <Library/UefiDriverEntryPoint.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/DebugLib.h>
#include <Protocol/DriverBinding.h>

// 1. 自定义一个协议
typedef struct {
	EFI_STATUS (EFIAPI *Hello)(VOID);
} MY_PROTOCOL;

// MyHello 是个函数,把这个函数放到Protocol实例里面
EFI_STATUS
EFIAPI
MyHello(VOID)
{
	DEBUG((DEBUG_INFO, "MyProtocol: Hello called!\n"));
	return EFI_SUCCESS;
}

MY_PROTOCOL mMyProtocol = {
	MyHello
};

EFI_GUID gMyProtocolGuid = { 0xaabbccdd, 0x1234, 0x4321, {0xaa,0xbb,0xcc,0xdd,0xee,0xff,0x11,0x22} };

// 2. Driver Binding:Supported()
//   判断该驱动是否支持某个 Controller Handle
EFI_STATUS
EFIAPI
MyDriverSupported (
	IN EFI_DRIVER_BINDING_PROTOCOL  *This,
	IN EFI_HANDLE                   ControllerHandle,
	IN EFI_DEVICE_PATH_PROTOCOL     *RemainingDevicePath
) {
	// 示例程序直接返回支持
  return EFI_SUCCESS;
}

// 3. Driver Binding:Start()
//   当 ConnectController() 被调用时执行
EFI_STATUS
EFIAPI
MyDriverStart (
	IN EFI_DRIVER_BINDING_PROTOCOL  *This,
	IN EFI_HANDLE                   ControllerHandle,
	IN EFI_DEVICE_PATH_PROTOCOL     *RemainingDevicePath
) {
    EFI_STATUS Status;
    // 在 ControllerHandle 上安装协议(绑定)
    Status = gBS->InstallProtocolInterface(
        &ControllerHandle,
        &gMyProtocolGuid,
        EFI_NATIVE_INTERFACE,
        &mMyProtocol
    );

    DEBUG((DEBUG_INFO, "MyDriver: Installed MyProtocol. Status = %r\n", Status));
    return Status;
}

// 4. Driver Binding:Stop()
//   用于卸载协议
EFI_STATUS
EFIAPI
MyDriverStop (
    IN EFI_DRIVER_BINDING_PROTOCOL  *This,
    IN EFI_HANDLE                   ControllerHandle,
    IN UINTN                        NumberOfChildren,
    IN EFI_HANDLE                   *ChildHandleBuffer
) {
    EFI_STATUS Status;

    Status = gBS->UninstallProtocolInterface(
        ControllerHandle,
        &gMyProtocolGuid,
        &mMyProtocol
    );

    DEBUG((DEBUG_INFO, "MyDriver: Uninstalled MyProtocol. Status = %r\n", Status));
    return Status;
}

// 5. DriverBinding Protocol 实例
EFI_DRIVER_BINDING_PROTOCOL gMyDriverBinding = {
    MyDriverSupported,		// 1. 驱动能不能处理某个设备?
    MyDriverStart,			// 2. 真的开始驱动(绑定协议)
    MyDriverStop,			// 3. 停止驱动(解绑协议)
    0x10,    				// 4. 驱动版本号(随便填)
    NULL,    				// 5. 驱动自己的 Image Handle(UEFI 会填)
    NULL     				// 6. DriverBindingHandle(UEFI 会填)
};

// 6. 驱动入口
EFI_STATUS
EFIAPI
MyDriverEntryPoint (
    IN EFI_HANDLE        ImageHandle,	// 这里我们上文在讲EFI_SYSTEM_TABLE时提到过
    IN EFI_SYSTEM_TABLE  *SystemTable
) {
  // 安装 Driver Binding Protocol
  return EfiLibInstallDriverBindingComponentName2(
           ImageHandle,
           SystemTable,
           &gMyDriverBinding,
           ImageHandle,
           NULL,
           NULL
         );
}

关于 UEFI 驱动框架的关键部分解释:

UEFI 驱动采用动态绑定的方式,一个驱动可以支持多个设备,一个设备也可能被多个驱动尝试支持(但最终只有一个成功绑定)。为了实现这种灵活的匹配和管理,UEFI 引入了:

  • Controller Handle(控制器句柄):代表一个“设备”或“服务”的抽象(比如一个 USB 控制器、一块硬盘、一个网络接口)。

  • Driver Binding Protocol:每个驱动都要提供这个协议,告诉系统:“我能支持哪些设备?怎么启动/停止?”

    每个 UEFI 驱动必须实现一个 EFI_DRIVER_BINDING_PROTOCOL 结构体,包含三个重要的函数指针。

    • DriverSupported

      EFI_STATUS MyDriverSupported (
        IN EFI_DRIVER_BINDING_PROTOCOL  *This,
        IN EFI_HANDLE                   ControllerHandle,
        IN EFI_DEVICE_PATH_PROTOCOL     *RemainingDevicePath OPTIONAL
      );
      
      // 举例:驱动只支持有 UsbIo 协议的设备
      Status = gBS->OpenProtocol(
          ControllerHandle,			// 要查询协议的对象句柄
          &gEfiUsbIoProtocolGuid,		// 协议GUID
          (VOID**)&UsbIo,				// 返回协议接口指针
          This->DriverBindingHandle, 	// 调用者(驱动)的句柄
          ControllerHandle,          	// 控制器句柄
          EFI_OPEN_PROTOCOL_TEST_PROTOCOL	// 打开方式属性,"测试模式"
      );
      if (!EFI_ERROR(Status)) {
          return EFI_SUCCESS; // 支持!
      }
      return EFI_UNSUPPORTED;
      

      该函数用于判断该驱动程序是否支持传入的 ControllerHandle 代表的设备,如果检查通过,系统会调用驱动的 Start()函数,在 Start()中再使用 BY_DRIVER正式打开协议。这种方式确保了驱动只会绑定到真正支持其协议的设备上。系统(通常是 DispatcherConnectController)会遍历所有已加载的驱动,对每一个 Controller Handle 调用此函数,问是否能驱动这个设备。

      1. Supported()调用触发场景

        graph TD A[驱动被加载] --> B[调用DriverEntry] B --> C[安装DriverBinding协议] C --> D[UEFI扫描现有设备] D --> E[对每个设备调用Supported] E --> F{Supported返回成功?} F -->|是| G[将驱动标记为支持该设备] F -->|否| H[继续扫描下一个设备] G --> I[等待ConnectController调用] I --> J[调用驱动的Start函数]

        举个在 shell 中加载驱动的例子:

        # 加载一个驱动
        load MyUsbDriver.efi
        # 系统会立即:
        # 1. 调用 MyUsbDriver 的 Supported() 函数
        # 2. 传入系统中每个设备 Handle
        # 3. 对支持的设备,可能立即或稍后调用Start()
        
        # 插入一个USB设备
        # 系统会:
        # 1. 创建新Handle
        # 2. 调用所有驱动的Supported()函数
        # 3. 包括刚加载的MyUsbDriver
        
      2. OpenProtocol函数的作用

        OpenProtocol 用于查看传入的 Handle 上是否安装了我们关心的协议(比如 EFI_USB_IO_PROTOCOL),如果符合你的驱动能力,就返回 EFI_SUCCESS;否则返回 EFI_UNSUPPORTED

      3. ControllerHandle 参数介绍

      函数传入的参数 ControllerHandle 是要判断是否支持的那个设备的句柄。比如:一个 SATA 控制器、一个 USB 键盘、一块 NVMe 硬盘——每个在 UEFI 中都是一个 Handle。

      当平台固件新发现一个设备,会为这个设备创建一个 Handle,并且安装对应的协议:

      // 平台固件(或UEFI内核)发现一个PCI设备
      // 创建一个新的 Handle 来代表这个设备
      EFI_HANDLE PciDeviceHandle = AllocateHandle();
      
      // 安装 PCI I/O 协议到这个 Handle
      gBS->InstallProtocolInterface(
          &PciDeviceHandle,           // Handle
          &gEfiPciIoProtocolGuid,     // 协议GUID
          EFI_NATIVE_INTERFACE,
          PciIoProtocol               // 协议实例
      );
      
      // 此时,PciDeviceHandle就是一个 ControllerHandle。
      

      当我们的驱动被调用时,会判断驱动是否支持调用的设备,传入的 ControllerHandle 即为调用驱动的设备句柄。

      更具体的例子:场景,USB 键盘驱动。这个代码的逻辑是:在固件启动过程中会枚举主板上连接的各种硬件设备,然后为每个硬件设备匹配对应的驱动,对应 USB 键盘设备,会将其 Handle 传入多个驱动程序调用 DriverSupported,如果找到支持 USB 键盘设备的驱动,就会返回 EFI_SUCCESS,代码如下:

      // 当系统枚举USB端口时...
      EFI_STATUS UsbKeyboardDriverSupported(
        IN EFI_DRIVER_BINDING_PROTOCOL *This,
        IN EFI_HANDLE                  ControllerHandle  // 这个可能是USB键盘设备
      ) {
        // 检查:这个Handle是USB设备吗?
        EFI_USB_IO_PROTOCOL *UsbIo;
        Status = gBS->OpenProtocol(
          ControllerHandle,
          &gEfiUsbIoProtocolGuid,
          (VOID**)&UsbIo,
          This->DriverBindingHandle,
          ControllerHandle,
          EFI_OPEN_PROTOCOL_TEST_PROTOCOL
        );
        if (EFI_ERROR(Status)) {
          return EFI_UNSUPPORTED;  // 不是USB设备
        }
        
        // 检查2:是键盘设备吗?
        // 通过USB I/O协议获取设备描述符
        EFI_USB_DEVICE_DESCRIPTOR DevDesc;
        UsbIo->UsbGetDeviceDescriptor(UsbIo, &DevDesc);
        
        if (DevDesc.DeviceClass == 0 &&  // 由接口定义类别
            DevDesc.DeviceSubClass == 0 &&
            DevDesc.DeviceProtocol == 0) {
          // 获取接口描述符
          EFI_USB_INTERFACE_DESCRIPTOR IfDesc;
          UsbIo->UsbGetInterfaceDescriptor(UsbIo, &IfDesc);
          
          if (IfDesc.InterfaceClass == 3 &&  // HID类
              IfDesc.InterfaceSubClass == 1 &&  // 启动接口
              IfDesc.InterfaceProtocol == 1) {  // 键盘协议
            return EFI_SUCCESS;  // 支持USB键盘!
          }
        }
        
        return EFI_UNSUPPORTED;  // 不是键盘
      }
      
    • DriverStart

      EFI_STATUS MyDriverStart (
        IN EFI_DRIVER_BINDING_PROTOCOL  *This,
        IN EFI_HANDLE                   ControllerHandle,
        IN EFI_DEVICE_PATH_PROTOCOL     *RemainingDevicePath OPTIONAL
      );
      
    • DriverStop

      EFI_STATUS MyDriverStop (
        IN EFI_DRIVER_BINDING_PROTOCOL  *This,
        IN EFI_HANDLE                   ControllerHandle,
        IN UINTN                        NumberOfChildren,
        IN EFI_HANDLE                   *ChildHandleBuffer OPTIONAL
      );
      

对应的 INF 文件:

[Defines]
  INF_VERSION                    = 0x00010005
  BASE_NAME                      = MyDriver
  FILE_GUID                      = aabbccdd-0000-1111-2222-123456789abc
  MODULE_TYPE                    = UEFI_DRIVER
  VERSION_STRING                 = 1.0
  ENTRY_POINT                    = MyDriverEntryPoint

[Sources]
  MyDriver.c

[Packages]
  MdePkg/MdePkg.dec

[LibraryClasses]
  UefiDriverEntryPoint
  UefiBootServicesTableLib
  DebugLib

[Protocols]
  gEfiDriverBindingProtocolGuid
模块 功能
MyDriverEntryPoint 驱动被加载时执行,安装 Driver Binding
DriverBinding.Supported 判断能否驱动某个设备(简化)
DriverBinding.Start ConnectController 时被调用,并安装你的协议
DriverBinding.Stop DisconnectController 时卸载协议
MyProtocol 自定义协议示例(你可以替换成自己的接口)

驱动的运行过程(UEFI Driver Model 流程)

  1. 驱动被加载,入口安装 Driver Binding
  2. Boot Manager 调用 ConnectController()
  3. MyDriverSupported → 判断是否支持设备
  4. MyDriverStart → 绑定驱动 + 安装协议
  5. 其他驱动或应用可通过 LocateProtocol 打开你的协议

Handle 和 Protocol 的对应关系是通过 InstallProtocolInterface() 在 DriverBinding Start() 中建立的。设备的 Handle 是系统创建的,驱动只负责在 Start() 中把协议装上去。

2. 应用程序

本示例说明了通过 LocateProtocol() 查找自定义的 MyProtocol;调用协议里的函数(Hello());完整可编译,可直接与前面的驱动示例配套使用。

#include <Uefi.h>
#include <Library/UefiLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/PrintLib.h>
#include <Library/DebugLib.h>

// 与驱动中完全一致的协议声明(只需复制)
typedef struct {
    EFI_STATUS (EFIAPI *Hello)(VOID);
} MY_PROTOCOL;

EFI_GUID gMyProtocolGuid = { 
    0xaabbccdd, 0x1234, 0x4321, 
    {0xaa,0xbb,0xcc,0xdd,0xee,0xff,0x11,0x22}
};

// main 函数
EFI_STATUS
EFIAPI
UefiMain (
    IN EFI_HANDLE        ImageHandle,
    IN EFI_SYSTEM_TABLE  *SystemTable
) {
    EFI_STATUS Status;
    MY_PROTOCOL *MyProto = NULL;

    Print(L"MyApp: Locating MyProtocol...\n");

    // 1. 查找 MyProtocol
    Status = gBS->LocateProtocol(
        &gMyProtocolGuid,
        NULL,
        (VOID**)&MyProto
    );

    if (EFI_ERROR(Status)) {
        Print(L"MyApp: Failed to locate MyProtocol: %r\n", Status);
        return Status;
    }

    Print(L"MyApp: MyProtocol Found. Calling Hello()...\n");

    // 2. 调用协议函数
    Status = MyProto->Hello();
    Print(L"MyApp: Hello() returned: %r\n", Status);

    return Status;
}

看到上面的代码,可能会有疑问,前面我们讲过 Protocol 一半是安装到某个 Handle 上的,但是在上面的应用程序中并没有体现出来。首先我们要明确,UEFI 中 Protocol 必须依附于 Handle。代码中没有显式使用 Handle,但 gEfiSimpleTextIn-ProtocolGuid 所对应的 Protocol 实例仍然是挂载在某个 Handle 上的。LocateProtocol() 内部会遍历系统中所有 Handle,查找哪个 Handle 安装了该 GUID 的 Protocol,然后返回其接口指针(即函数表)。你只是不需要自己操作 Handle,不代表 Handle 不存在或不重要。

注意:LocateProtocol() 只能用于全局唯一的 Protocol(比如控制台输入、实时时钟等)。如果一个 Protocol 有多个实例(比如多个 USB 键盘都提供 SimpleTextIn),你就不能用 LocateProtocol(),而要用 LocateHandleBuffer() + HandleProtocol() 来枚举所有 Handle 并选择合适的那个。

反过来讲 Handle 的存在就是为了:

  • 支持多实例:多个设备可以提供相同类型的 Protocol(如多个网卡都有 SimpleNetworkProtocol),每个绑定到不同的 Handle。
  • 资源管理:卸载驱动或设备时,可以通过 Handle 一次性移除其所有 Protocol。
  • 协议组合:一个 Handle 可以挂多个 Protocol(如一个 USB 设备 Handle 同时有 DevicePath、UsbIo、BlockIo 等),形成完整的设备描述。

对应 INF 文件:

[Defines]
  INF_VERSION                    = 0x00010005
  BASE_NAME                      = MyApp
  FILE_GUID                      = aabbccdd-1111-2222-3333-123456789abc
  MODULE_TYPE                    = UEFI_APPLICATION
  VERSION_STRING                 = 1.0
  ENTRY_POINT                    = UefiMain

[Sources]
  MyApp.c

[Packages]
  MdePkg/MdePkg.dec

[LibraryClasses]
  UefiLib
  UefiBootServicesTableLib
  PrintLib
5. 测试
  • 先加载驱动(MyDriver.efi)

    fs0:\> load MyDriver.efi
    
  • 然后运行测试 APP

    fs0:\> MyApp.efi
    
  • 输出示例:

    MyApp: Locating MyProtocol...
    MyApp: MyProtocol Found. Calling Hello()...
    MyProtocol: Hello called!
    MyApp: Hello() returned: Success
    

6.2.2 Protocol Handler Services

Protocol Handler Services 是 UEFI Boot Services 提供的一组“协议管理功能”,用于注册、查找、打开、关闭和连接各种 UEFI Protocol(接口)。它是 UEFI 驱动之间通信的核心基础设施。上面我们对 Protocol 和 Handle 两个重要的概念进行了说明,给出的代码中其实已经用到了 Protocol Handler Services 中的各种管理功能。

协议管理服务中一些重要的 API:

  1. 注册协议,即驱动将自己的服务以 Protocol 的方式暴露出来。

    InstallProtocolInterface()
    ReinstallProtocolInterface()
    UninstallProtocolInterface()
    
  2. 查找协议。

    LocateProtocol()
    LocateHandle()
    LocateHandleBuffer()
    RegisterProtocolNotify()
    
  3. 打开或关闭协议(权限控制),UEFI 不让你随便直接访问别人的协议,它有权限模型。

    OpenProtocol()
    CloseProtocol()
        
    // Open 时必须指定访问类型
    EFI_OPEN_PROTOCOL_GET_PROTOCOL
    EFI_OPEN_PROTOCOL_BY_DRIVER
    EFI_OPEN_PROTOCOL_BY_CHILD_CONTROLLER
    EFI_OPEN_PROTOCOL_TEST_PROTOCOL
    

    OpenProtocol() 函数:

    EFI_STATUS EFIAPI OpenProtocol(
      IN  EFI_HANDLE        Handle,             // 要查询协议的对象句柄
      IN  EFI_GUID          *Protocol,          // 协议GUID
      OUT VOID              **Interface, OPTIONAL // 返回协议接口指针
      IN  EFI_HANDLE        AgentHandle,        // 调用者(驱动)的句柄
      IN  EFI_HANDLE        ControllerHandle,   // 控制器句柄
      IN  UINT32            Attributes          // 打开方式属性
    );
    
    属性 用途 是否需要Close 引用计数
    TEST_PROTOCOL 仅测试是否存在 不变
    BY_DRIVER 驱动使用协议 增加
    GET_PROTOCOL 获取接口指针 增加
    BY_CHILD_CONTROLLER 子控制器使用 增加

关键 API 总结:

功能 关键 API 作用
注册协议 InstallProtocolInterface 将协议挂到 Handle 上
查找协议 LocateProtocol / LocateHandle 找协议实例或对应的设备句柄
打开协议 OpenProtocol 获取协议接口(带权限控制)
关闭协议 CloseProtocol 释放协议使用
监听协议事件 RegisterProtocolNotify 当协议安装后自动回调事件
驱动连接 ConnectController 绑定驱动与设备
驱动断开 DisconnectController 解绑

七、DXE 阶段驱动加载简化伪代码

所有驱动加载、设备发现、协议匹配,都发生在DXE阶段。这个阶段就像是"装修房子",把硬件驱动一个一个装好,然后才能进入BDS阶段引导操作系统。

// DXE阶段的核心代码流程(简化版)

// 1. DXE核心先启动
DXE_Core_Entry() {
    // 加载基础服务
    LoadMemoryServices();
    LoadEventServices();
    LoadProtocolServices();  // 包括InstallProtocolInterface
}

// 2. 按顺序执行DXE驱动
foreach (driver in DXE_FV) {
    // 执行驱动的入口函数
    driver->EntryPoint();
    // 这个入口函数会安装DriverBindingProtocol
}

// 3. 每个驱动加载时
MyDriverEntryPoint() {
    // 安装驱动绑定协议
    gBS->InstallProtocolInterface(&ImageHandle, 
                                  DriverBindingProtocol, ...);
    
    // 注意:此时系统会立即
    // 1. 找出系统中所有已有设备
    // 2. 对每个设备调用这个驱动的Supported()
}

// 4. 如果有新设备被创建
SomeBusDriver() {
    // 发现新硬件
    DiscoverNewHardware();
    
    // 创建设备Handle
    gBS->InstallProtocolInterface(&NewDevice, DeviceProtocol, ...);
    
    // 注意:此时系统会立即
    // 1. 找出系统中所有已加载驱动
    // 2. 对每个驱动调用Supported(NewDevice)
}

步骤:

开机 → [SEC → PEI] → [DXE阶段开始]
                        ↓
                1. 加载基础服务
                2. 加载总线驱动
                3. 总线发现硬件 → 创建设备Handle
                4. 加载你的驱动
                5. 系统自动匹配:你的驱动 vs 设备Handle
                6. 匹配成功 → 调用你的Start()
                        ↓
                   [BDS阶段]
                        ↓
                 引导操作系统

八、Console Suport 和 Media Access

在 UEFI 规范中,Console Support 和 Media Access 是两个重要的协议部分,主要用于预启动环境中的输入输出和存储介质访问。

Console Support 对应UEFI规范中的 Console I/O Protocols(包括 EFI_SIMPLE_TEXT_INPUT_PROTOCOL、EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL 和 EFI_GRAPHICS_OUTPUT_PROTOCOL)。主要作用:提供基本的文本和图形控制台支持,用于预启动阶段的输入输出。它取代传统VGA文本模式,支持Unicode字符、简单输入控制,确保管理员能在无OS环境下交互(如进入UEFI设置或诊断)。

  • 支持文本模式控制台(类似终端),允许固件、UEFI应用和OS加载器显示信息(如启动菜单、日志、配置界面)和接收用户输入(如键盘扫描码)。

  • 支持图形输出(Graphics Output Protocol, GOP),用于显示logo、多语言界面、设置屏幕等现代固件功能。

  • 允许OS启动前直接访问帧缓冲区(frame buffer),实现更丰富的预启动体验。

Media Access 对应UEFI规范中的 Media Access Protocols(包括 EFI_BLOCK_IO_PROTOCOL、EFI_SIMPLE_FILE_SYSTEM_PROTOCOL 和 EFI_DISK_IO_PROTOCOL 等)。主要作用是提供对存储介质(如硬盘、U盘、光盘)的访问支持,特别是文件系统级访问。它可以实现从各种块设备(block device)引导,支持大容量磁盘(GPT分区)、可移动介质引导,确保固件能访问文件系统进行启动或恢复操作。

  • 支持EFI文件系统(基于FAT12/FAT16/FAT32),允许固件直接读取分区上的文件,而无需执行介质上的引导代码。

  • 处理介质变化(如更换U盘时返回 EFI_MEDIA_CHANGED 错误,需要重新打开卷)。

  • 支持从EFI系统分区(ESP)加载UEFI应用、驱动和OS加载器。

九、Protocols 的组织形式

UEFI 的接口不像传统 BIOS 那样是固定的一组函数,而是高度模块化和可扩展的,主要通过 Protocols 来实现。

BootServices 和 RuntimeServices 的作用回顾

  • UEFI 系统表(EFI_SYSTEM_TABLE)中包含两个主要的服务表:
    • Boot Services:预启动阶段(pre-boot)可用,包括内存分配、事件管理、协议安装/查找等。在 OS 调用 ExitBootServices() 后失效。
    • Runtime Services:运行时可用,即使 OS 启动后也能调用,如获取时间、变量读写、重启等。
  • 并非所有 UEFI 接口都直接在这些服务表中。BootServices 和 RuntimeServices 主要提供基础全局服务(如内存管理、事件、变量)和协议管理服务(如安装、打开、定位协议),但具体的设备访问或功能接口大多通过 Protocols 实现。

Protocols 的分类和组织:

  • UEFI 的接口主要以 Protocol 的形式存在,每个 Protocol 是一个 C 风格的结构体,包含函数指针(接口函数)和数据。

  • 这些 Protocols 按照功能分类,UEFI 规范中将它们分成多个类别,例如:

    • Console I/O Protocols(控制台输入输出,如 Simple Text Input/Output、Graphics Output Protocol)。
    • Media Access Protocols(介质访问,如 Block I/O、Simple File System、Disk I/O)。
    • ACPI Protocols(ACPI 表安装和管理)。
    • USB Protocols(USB I/O、USB Host Controller 等)。
    • Network Protocols(网络栈,如 PXE、TCP/IP)。
    • PCI Protocols(PCI 枚举和 I/O)。
    • 其他如 Driver Binding Protocol(驱动绑定)、Loaded Image Protocol 等。
  • 这与 EDK2 的结构非常相似:

    EDK2 将代码组织成 Packages (Pkg),如 MdePkg(基础模块)、MdeModulePkg(通用驱动)、PcAtChipsetPkg 等。每个 Pkg 包含相关 Protocols 的定义、实现和库。例如,ACPI 相关的 Protocols 在规范的独立章节中,在 EDK2 中也分布在相应 Pkg 中。总之,Protocols 是 UEFI 可扩展性的核心,不是全部塞进 BootServices/RuntimeServices,而是按功能“分组合集”组织,便于模块化开发和跨平台兼容。比如ACPI Protocol等,它们使用前要先打开,打开Protocol获得的指针,进一步调用具体的功能函数。

十、UEFI BIOS 与 Legacy BIOS

1. UEFI BIOS 与 Legacy BIOS 的差异与演进

经典 BIOS(Legacy BIOS)工作在16 位实模式下,CPU 的可寻址内存空间被严格限制在1 MB 以内。在这种运行环境中,BIOS 可以完成一些基础硬件的初始化,例如键盘、显示设备和简单的存储控制器等。但对于功能较为复杂的设备,仅靠主 BIOS 已无法满足需求,因此引入了 Option ROM 机制。

Option ROM 本质上是存放在设备上的一段固件程序,可以视为该硬件的早期驱动。BIOS 在启动过程中会扫描特定的地址空间,一旦发现 Option ROM,便在合适的时机调用它们以完成设备初始化和服务扩展。由于 Option ROM 同样运行在 16 位实模式下,其代码必须包含大量16 位汇编指令,并且只能使用有限的低端内存空间。

这种设计带来了多个根本性问题。首先,处理器运行模式的限制严重影响了系统的可扩展性。16 位实模式无法直接运行在保护模式或长模式下,一旦系统切换到 32 位或 64 位模式,Option ROM 中的 16 位代码便无法继续执行。如果某个 Option ROM 在初始化过程中切换了 CPU 模式,为了执行其他 ROM,又必须切回实模式。这种频繁的模式切换不仅使固件代码结构极其复杂,也显著增加了开发难度和系统不稳定性。

其次,可用内存资源极为有限。在传统内存布局中,0x00000–0x9FFFF 为常规内存,0xA0000–0xBFFFF 被 VGA 显存占用,0xF0000–0xFFFFF 保留给系统 BIOS,真正可供 Option ROM 使用的空间非常狭小。而复杂设备往往需要较大的代码和数据空间,这在实模式环境下几乎无法满足,进一步制约了固件功能的扩展。

再次,硬件与驱动之间的绑定方式效率低下。在 Legacy BIOS 体系中,Option ROM 并不具备统一的设备发现和管理机制,它们通常需要自行扫描系统总线,判断自己应当服务于哪个硬件设备。结果是:每个 Option ROM 都可能重复扫描一次总线,既浪费启动时间,又增加了代码复杂度。当系统中存在多个设备时,这种方式在性能和可靠性上都难以接受。

  • UEFI 的解决思路

UEFI 从体系结构层面彻底解决了上述问题。其核心思想是:尽早摆脱 16 位实模式的束缚,进入现代处理器运行环境

在 EDK II 中,这一过程由 UefiCpuPkg 下的 ResetVector 代码完成。该代码是 UEFI 固件中最早执行的程序之一,其主要职责是在系统上电后,将 CPU 从 16 位实模式迅速切换到 32 位保护模式64 位长模式(若处理器支持)。

在随后的 PEI(Pre-EFI Initialization)阶段,UEFI 会完成部分永久内存的初始化,并建立页表,为 DXE(Driver Execution Environment)阶段提供稳定、统一的执行环境。此后运行的 UEFI 驱动程序均基于平坦内存模型,无需再包含任何 16 位汇编代码,同时也不再受 1 MB 内存限制的约束。

此外,UEFI 通过 标准化的驱动模型和设备发现机制(如 Handle–Protocol 模型),实现了驱动与硬件之间的自动绑定。驱动只需声明自己支持的协议,固件即可完成设备匹配与加载,避免了 Legacy BIOS 中 Option ROM 反复扫描总线的问题,大幅提升了启动效率和系统可维护性。

2. UEFI 硬件-驱动模型

1. 硬件抽象:ControllerHandle

在 UEFI 体系中,不同类型的硬件设备被统一抽象为 控制器(Controller)。控制器的本质职责,是完成 CPU 与外部设备之间的 I/O 交互。从根本上讲,CPU 与设备之间的交互无非是指令和数据的发送与接收,因此在 UEFI 中,硬件设备不再以具体型号或寄存器形式暴露,而是以一个统一的抽象对象呈现。这个抽象对象就是 ControllerHandle。 从实现角度看,ControllerHandle 本质上是一个指针,用于标识某一个具体的硬件实例。这一点在概念上与 Linux 中“一切皆文件”的思想类似:设备本身并不直接暴露给上层,而是通过一个统一的句柄进行访问。

UEFI 将控制器划分为两类:总线控制器(Bus Controller)终端控制器(Device Controller / Leaf Controller)

系统以 CPU 为起点,向下延展出多级总线结构,例如 PCI 总线、USB 总线、SCSI 总线等。 这些总线本身由 总线控制器 管理,并直接与 CPU 进行通信。在总线之下,可以继续连接子设备或子总线。比如 PCI 总线下连接 GPU、SSD、USB 控制器等。而 GPU,SSD,U 盘等 不再向下分叉的设备,即为终端控制器(叶子节点)。这样,CPU 与所有外设共同构成了一棵 设备树(Device Tree),UEFI 的核心任务之一,就是对这棵设备树进行统一管理。

2. 总线驱动:设备发现与 ControllerHandle 的创建

为了高效管理复杂的硬件拓扑,UEFI 采用了一种与 Legacy BIOS 完全不同的策略:将总线驱动程序下沉到主板固件中。在 UEFI 中,每一种总线(如 PCI、USB、SCSI)都由固件内置的总线驱动程序负责管理。这些总线驱动的职责包括:

  1. 枚举并发现挂接在总线上的子设备;
  2. 为每一个发现的设备创建对应的 ControllerHandle
  3. 根据设备特征,为后续驱动绑定提供基础信息。

总线驱动只负责“发现设备并抽象设备”,并不关心设备的具体功能实现。

3. 驱动程序与 ImageHandle

UEFI 中的设备驱动程序是以 PE/COFF 可执行文件 的形式存在的。当驱动被加载到内存后,该驱动实例被称为一个 Image,并由一个 ImageHandle 标识。ImageHandle:标识一个已加载到内存中的驱动程序实例;ControllerHandle:标识一个具体的硬件设备实例,二者是 UEFI 系统中的两个核心对象,但它们本身并不直接建立联系。

驱动程序在被加载时是“被动的”,它并不知道自己最终要控制哪一个硬件设备。驱动与设备之间的绑定,依赖于 UEFI 定义的一套统一机制,而非驱动自行扫描总线。

4. 功能抽象:Protocol(协议)

为了避免不同厂商、不同设备驱动接口碎片化,UEFI 对“设备能力”进行了进一步抽象,形成了 Protocol(协议)机制。从本质上看:Protocol 是一组 C 语言风格的接口函数集合,用于描述某一类能力或服务。以块设备为例,虽然外部设备种类繁多,但从 I/O 方式上看,许多设备都可以归类为 块设备。 只要具备“按块读写”的能力,其底层实现方式并不重要。UEFI 为此定义了标准的 Block I/O Protocol,要求所有块设备驱动都实现这一协议。这样上层无需关心设备是 SATA 硬盘、NVMe SSD 还是 U 盘,只需通过统一的 Protocol 接口即可完成访问。

5. Protocol 与 ControllerHandle 的关联

Protocol 本身并不直接对应某一个具体硬件,而是作为功能接口存在于驱动程序中。真正将“功能”与“设备”联系起来的,是 Protocol 在 ControllerHandle 上的安装(Install)过程

其逻辑如下:

  1. 总线驱动发现设备,并创建对应的 ControllerHandle
  2. 设备驱动被加载到内存,形成一个 Image
  3. 驱动判断自己是否支持某个 ControllerHandle
  4. 若支持,则将自己实现的某个 Protocol 安装到该 ControllerHandle 上
  5. 从此以后,通过该 ControllerHandle 即可访问对应的 Protocol 接口。

需要注意的是:同一种 Protocol 在内存中只需要一份代码实现,但可以被安装到多个 ControllerHandle 上,从而区分不同的设备实例(例如多个硬盘)。

6. 驱动绑定机制:Driver Binding Protocol

UEFI 使用 EFI_DRIVER_BINDING_PROTOCOL 来完成驱动与设备的绑定管理。该 Protocol 由驱动程序实现,包含三个关键接口:

  • Supported(): 用于判断当前驱动是否支持指定的 ControllerHandle
  • Start(): 在支持的情况下,启动驱动并控制该设备;
  • Stop() 停止驱动对该设备的控制。

UEFI 固件通过该 Protocol 统一调度驱动的绑定、启动与卸载过程,而不是由驱动自行管理设备生命周期。一个驱动程序可以同时实现 多个 Protocol,从而支持多种设备类型或功能。

7. Boot Services 与 Protocol 的使用

UEFI 的 Boot Services 提供了一组标准接口,用于查找、打开和管理 Protocol。其中最常用的是:

  • OpenProtocol()
    通过 Protocol GUIDControllerHandle 打开指定协议;
  • HandleProtocol()
    用于简化场景,直接打开系统中找到的第一个符合条件的设备。

每一个 Protocol 都由一个 唯一的 GUID 标识,这些 GUID 在 UEFI 规范中都有明确说明。


Steady Progress!

posted @ 2025-12-10 01:47  阿源-  阅读(206)  评论(0)    收藏  举报