EFI stub引导下ARM64设备树传递路径与调试方法 - 第4节

4.1 EFI Stub 概念与定位

EFI Stub 是嵌入 Linux 内核镜像(vmlinuz)头部的一段小型 UEFI 应用程序。它在 UEFI 固件与内核之间充当“胶水层”,使得内核能够直接被 UEFI 固件加载并启动,而无需依赖 GRUB、systemd-boot 等独立的 Boot Manager。

核心定义: EFI Stub 是一个符合 UEFI 规范的 PE32+ 可执行文件,它被链接到内核映像的起始位置,负责从 UEFI 运行时环境过渡到 Linux 内核的虚拟内存空间。

4.1.1 Stub 与普通 UEFI 应用的区别

虽然 EFI Stub 在二进制格式上与普通 UEFI 应用(如 Shell.efi、GRUB.efi)相同,但它们在生命周期、依赖关系和最终目标上存在本质差异:

对比维度 普通 UEFI 应用 EFI Stub
生命周期 运行结束后返回 UEFI Shell 或 Boot Manager 运行结束后不再返回,直接接管系统控制权
依赖关系 依赖 UEFI 协议栈(文件系统、网络、控制台) 仅依赖少数启动协议(Loaded Image、Device Path)
最终目标 提供用户交互或加载其他应用 解压内核、设置页表、跳转到内核入口
退出方式 BS->Exit() 返回调用者 BS->ExitBootServices() 终止 UEFI 服务
内存占用 运行期间常驻,退出后释放 执行 ExitBootServices 后 Stub 自身内存被内核回收
调试提示: 普通 UEFI 应用可以通过 gdb + OVMF 调试到应用退出为止;而 EFI Stub 一旦调用 ExitBootServices(),UEFI 调试通道立即关闭,后续调试必须切换到内核调试器(如 kgdb 或 JTAG)。

4.2 Stub 的入口点与执行流程

4.2.1 入口点定义

EFI Stub 的入口点由 UEFI 规范定义,采用标准 PE32+ 入口签名。在 Linux 内核源码中,入口函数位于 drivers/firmware/efi/libstub/ 目录下:

// 文件: drivers/firmware/efi/libstub/arm64.c
// 入口函数: efi_enter_kernel()

efi_status_t efi_enter_kernel(efi_loaded_image_t *image,
                              unsigned long image_addr,
                              char *cmdline_ptr,
                              unsigned long fdt_addr)
{
    // 1. 解析 UEFI 配置表获取设备树或 ACPI
    // 2. 设置内核引导协议参数(x0 = FDT 地址,x1 = 0)
    // 3. 调用 ExitBootServices()
    // 4. 跳转到内核入口点
}

ARM64 架构下,Stub 入口点与内核入口点的寄存器约定如下:

寄存器 Stub 调用内核时的值 说明
x0 设备树 Blob(FDT)的物理地址 若为 0,表示使用 ACPI
x1 0x0 保留,必须为 0
x2 内核入口点物理地址 由 Stub 计算得出
DAIF 所有中断屏蔽 Stub 退出前设置

4.2.2 执行流程 SVG 图

EFI Stub 入口点与执行流程 UEFI 固件加载 Stub 解析 Loaded Image Protocol 获取设备树 / ACPI 表 配置引导参数 调用 ExitBootServices() 终止 UEFI 服务,关闭中断 跳转到内核入口点 x0 = FDT 地址,x1 = 0 关键转折点:ExitBootServices() 之后,UEFI 调试通道关闭,所有调试必须切换到内核侧 ARM64 设备树地址通过 x0 寄存器传递,这是 Stub 与内核之间的唯一硬件约定

4.3 Stub 在 ARM64 设备树传递中的角色

在 ARM64 平台上,EFI Stub 承担了设备树从 UEFI 固件到内核的“搬运工”角色。整个传递路径包含三个关键步骤:

  1. 固件侧: UEFI 固件在启动时从 SPI Flash 或 eMMC 加载原始设备树(.dtb),并将其地址记录在 UEFI 配置表(Configuration Table)中,GUID 为 EFI_DTB_TABLE_GUID。
  2. Stub 侧: EFI Stub 通过 efi_get_fdt() 函数从配置表中读取设备树地址,将其复制到内核预留的内存区域,并进行必要的修正(如内存节点更新、chosen 节点填充)。
  3. 内核侧: 内核启动时从 x0 寄存器获取设备树地址,解析并展开设备树,完成外设驱动绑定。
常见陷阱: 如果 UEFI 固件没有在配置表中注册设备树(例如仅支持 ACPI 的服务器固件),EFI Stub 将无法获取 FDT 地址,x0 寄存器会被设置为 0。此时内核必须支持 ACPI 启动,否则系统会 panic。调试时可通过 efi=debug 内核参数观察 Stub 的 FDT 获取日志。

4.3.1 设备树传递路径 SVG 图

ARM64 设备树传递路径(EFI Stub 视角) UEFI 固件 加载原始 DTB 注册到配置表 efi_get_fdt() EFI Stub 复制 FDT 到内核区域 修正 chosen / memory x0 = FDT 地址 Linux 内核 解析 FDT 展开设备树 设备树修正内容(Stub 负责) 1. /chosen/bootargs ← 从 UEFI 命令行获取 2. /chosen/linux,initrd-start/end ← 从 Loaded Image 获取 3. /memory/reg ← 从 UEFI 内存映射获取 调试命令:efi=debug initcall_debug 可打印 Stub 阶段的 FDT 操作日志 若 FDT 地址为 0,检查固件配置表是否包含 EFI_DTB_TABLE_GUID

4.4 Stub 与普通 UEFI 应用的代码结构对比

从代码实现角度看,EFI Stub 与普通 UEFI 应用在入口函数签名和协议使用上存在显著差异:

// 普通 UEFI 应用入口
EFI_STATUS EFIAPI efi_main(
    EFI_HANDLE          ImageHandle,
    EFI_SYSTEM_TABLE   *SystemTable
) {
    // 可以使用所有 UEFI 协议
    // 返回后控制权交还 UEFI
    return EFI_SUCCESS;
}

// EFI Stub 入口(Linux 内核源码)
efi_status_t efi_stub_entry(
    efi_loaded_image_t *image,
    unsigned long       image_addr,
    char               *cmdline_ptr,
    unsigned long       fdt_addr
) {
    // 仅使用 LoadedImage 和配置表
    // 调用 ExitBootServices() 后不再返回
    // 直接跳转到内核
}
关键差异: 普通 UEFI 应用通过 SystemTable 访问所有 UEFI 服务;EFI Stub 则直接接收 efi_loaded_image_t 指针,且不依赖 SystemTable 中的控制台或文件系统协议。这种设计使得 Stub 的代码体积极小(约 20-30 KB),适合嵌入内核头部。

4.5 本章小结

  • EFI Stub 是嵌入内核的微型 UEFI 应用,负责从 UEFI 环境过渡到内核环境。
  • 与普通 UEFI 应用相比,Stub 不返回 UEFI 环境,且仅依赖 Loaded Image 和 Configuration Table 协议。
  • ARM64 设备树通过 x0 寄存器 从 Stub 传递给内核,Stub 负责从 UEFI 配置表获取 FDT 并进行必要修正。
  • 调试 Stub 阶段时,efi=debug 参数是观察设备树传递过程的首选工具。