自制工业控制系统:UEFI固件层实战——手写FFI绑定与ExitBootServices权力交接
Contents
起因
《自制工业控制系统:从UEFI到内核》定下了路线:零外部 Cargo 依赖,与固件打交道的这一层参考 r-efi 手写 FFI 绑定,代码全部 vendor 进自己的仓库。这篇把这个系列真正的第一步展开:从 UEFI 应用入口写起,拿齐所有"过了这村没这店"的资源,最后调用 ExitBootServices() 完成固件到系统的权力交接。这一篇的目标是:照着做就能跑。权力交接为什么长这个样子,有段来历:BIOS 是 1981 年 IBM PC 的遗产,16 位实模式加 1MB 地址空间的假设,靠 INT 10h/13h 这类软中断服务往后缝缝补补撑了近二十年;九十年代末 Intel 给 64 位安腾平台做启动方案,发现这套底子实在撑不住,1998 年立项 EFI 推倒重来,2005 年捐给 UEFI 论坛变成开放标准——今天的主板固件几乎清一色是它。我们手写 FFI 对接的不是某家厂商的私有接口,是这份二十多年没换过骨架的行业标准。
一、入口与调用约定:先避开 ABI 地雷
用官方 x86_64-unknown-uefi 目标编译(rustup target add x86_64-unknown-uefi),产物就是合法的 .efi 文件,放到 ESP 分区 EFI/BOOT/BOOTX64.EFI,QEMU 里 qemu-system-x86_64 -bios ovmf.fd -serial stdio 就能调。入口签名:
|
|
要点:
extern "efiapi"对应 Microsoft x64 调用约定(前四个参数走 RCX/RDX/R8/R9,调用方清栈),UEFI 规范钦定,跟 Linux 侧 SysV(RDI/RSI/RDX/RCX)不同。用官方 target 编译器替你处理对了;自建工具链混用两种 ABI 是裸机项目最常见的暴毙点EFI_SYSTEM_TABLE里这一层用到的关键字段:BootServices: *mut EFI_BOOT_SERVICES(主角,函数指针表)、ConfigurationTable(按 GUID 找 ACPI RSDP 的地方)、ConOut(调试期文字输出)
EFI_BOOT_SERVICES 是一张几十个函数指针的表,我们实际用到的只有这几个,绑定时按需声明:
|
|
结构体字段顺序和偏移必须跟规范一致,多一个少一个都会错位——所以逐条对着 r-efi 的定义搬(MIT/Apache-2.0 宽松许可,注明出处后 vendor 进仓库,从此与上游脱钩)。
二、拿 GOP:帧缓冲五元组
|
|
locate_protocol 的第一个参数是协议 GUID——协议就是"带 GUID 的接口",这是 UEFI 一切服务的寻址方式。GOP 结构里真正要用的字段:
|
|
拿到这五个值——基址、宽、高、每行像素数、像素格式——显示的全部家当就到手了。显卡篇会回到这里接着写像素。
三、拿内存映射:物理内存管理的唯一依据
|
|
坑都在细节里:
- 返回
EFI_BUFFER_TOO_SMALL是正常流程不是错误;描述符数组大小随设备状态变化,必须留余量(惯例是多留两个描述符) - 描述符是变长的(
desc_size通常 40 字节,别硬编码size_of::<T>()),遍历方式是ptr + i * desc_size跳着走 - 描述符里
type == 7(EFI_CONVENTIONAL_MEMORY)的区间是可用内存,其余类型要么已占用要么有特殊用途(如 MMIO);这个数组就是后面物理页分配器的输入
顺手在 ExitBootServices 之前用 GetTime 拿一次 RTC 时间做日志起点,再调 SetWatchdogTimer(0, 0, 0, null) 关看门狗——UEFI 默认开着 5 分钟看门狗,不关的话内核初始化到第 4 分 59 秒机器直接重启,不留任何错误信息。
四、ExitBootServices():一个重试循环
规范设了"对暗号"机制:GetMemoryMap 返回 MapKey,ExitBootServices 校验它;两次调用之间只要有事件触发或内存分配,key 就失效返回 EFI_INVALID_PARAMETER(5)。标准写法:
|
|
把"最后一次 GetMemoryMap“和”ExitBootServices“之间的代码压到最少,别在中间碰任何可能分配内存的接口。返回 0 的那一刻起:Boot Services 作废、协议作废、ConOut 作废;CPU 仍在 64 位长模式、仍用固件建好的 identity 分页——不需要任何"跳转内核"的仪式,efi_main 接下来的代码就是内核本身。Runtime Services 理论上还活着,但用不到 UEFI 变量服务就当它不存在。
五、Handoff:出生证明
移交产物收敛成一个结构体,后面的内核拿它当出生证明:
|
|
它就放在 efi_main 栈上或紧跟内核镜像的 .bss 里——反正 efi_main 接下来的代码就是内核本身,不存在"跨模块传不过去"的问题。ACPI RSDP 的取法:遍历 system_table.configuration_table,匹配 ACPI 2.0 的 GUID(8868e871-e4f1-11d3-bc22-0080c73c8881),取其 vendor_table 字段——后续 MADT/MCFG/HPET 全从这张表往下找。
总结
这一篇的实战清单:官方 uefi target 起项目、薄 FFI 绑定层搬 r-efi、locate_protocol 拿 GOP 五元组、两次 GetMemoryMap 问大小再取映射、关看门狗、重试循环闯 ExitBootServices、RSDP 从 Configuration Table 按 GUID 取出。跑通的标志:QEMU 里执行到 ExitBootServices 返回 0 后,串口每隔几秒打一行心跳横幅(直写 16550,此时 ConOut 已经死了),挂过 5 分钟不复位——顺手把"看门狗确实关掉了"这件事也验了。下一篇接管 GDT/IDT。
参考链接
- UEFI Specification——Boot Services/GOP/ExitBootServices 权威定义
- OSDev Wiki: UEFI / Exit Boot Services / GOP
- r-efi——FFI 绑定参考(MIT/Apache-2.0)
- 本系列第一篇:自制工业控制系统:从UEFI到内核,为线切割机全自研专用系统的立项与技术选型
Author 软件开发大郭
LastMod 2026-09-11