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 图
4.3 Stub 在 ARM64 设备树传递中的角色
在 ARM64 平台上,EFI Stub 承担了设备树从 UEFI 固件到内核的“搬运工”角色。整个传递路径包含三个关键步骤:
- 固件侧: UEFI 固件在启动时从 SPI Flash 或 eMMC 加载原始设备树(.dtb),并将其地址记录在 UEFI 配置表(Configuration Table)中,GUID 为
EFI_DTB_TABLE_GUID。 - Stub 侧: EFI Stub 通过
efi_get_fdt()函数从配置表中读取设备树地址,将其复制到内核预留的内存区域,并进行必要的修正(如内存节点更新、chosen节点填充)。 - 内核侧: 内核启动时从 x0 寄存器获取设备树地址,解析并展开设备树,完成外设驱动绑定。
常见陷阱: 如果 UEFI 固件没有在配置表中注册设备树(例如仅支持 ACPI 的服务器固件),EFI Stub 将无法获取 FDT 地址,x0 寄存器会被设置为 0。此时内核必须支持 ACPI 启动,否则系统会 panic。调试时可通过
efi=debug 内核参数观察 Stub 的 FDT 获取日志。
4.3.1 设备树传递路径 SVG 图
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参数是观察设备树传递过程的首选工具。