UEFI固件基础:架构、驱动模型与协议概念
好,咱们正式开始啃UEFI固件这块硬骨头。说实话,我刚接触UEFI那会儿,也被它那一堆抽象概念搞得头大。什么Handle、Protocol、Driver Binding,听着像黑话。但等你真正搞懂了它的设计哲学,你会发现——这玩意儿其实挺优雅的。
UEFI固件架构:分层看更清楚
UEFI固件不是一坨代码堆出来的。它分了好几个阶段,每个阶段干不同的事。我习惯把它分成三层来看:
- 硬件抽象层:直接跟CPU、芯片组打交道。说白了就是初始化硬件,让系统能跑起来。
- 核心服务层:提供Boot Services、Runtime Services这些基础API。你写UEFI应用时调的函数,大部分都在这一层。
- 协议层:各种Protocol的集合。UEFI的精髓就在这儿——一切皆协议。
嗯,这里要注意:UEFI固件启动过程是分阶段的。从SEC到PEI,再到DXE,最后到BDS。每个阶段都有明确的任务。我在调试一个国产平台时遇到过,PEI阶段内存初始化失败,结果卡在PEI Core里出不来。后来发现是MRC参数配错了。
核心要点:UEFI固件启动流程 = SEC → PEI → DXE → BDS → TSL → RT。前四个阶段是开发者最常打交道的。
UEFI驱动模型:别跟传统BIOS驱动搞混
传统BIOS的驱动是静态的,编译时就得定好。UEFI不一样,它搞了一套动态驱动模型。你想想看,UEFI驱动本质上就是一个遵循特定接口规范的模块。它可以在系统运行时被加载、绑定到设备上、然后卸载。
UEFI驱动模型的核心是Driver Binding Protocol。每个UEFI驱动都必须实现这个协议。它有三个关键函数:
| 函数名 | 作用 | 我踩过的坑 |
|---|---|---|
Supported() |
检查驱动是否支持某个设备 | 返回值判断要严谨,否则驱动乱绑定 |
Start() |
绑定驱动到设备,开始干活 | 资源分配失败要回滚,不然内存泄漏 |
Stop() |
解绑驱动,清理资源 | 我曾经忘了释放DMA缓冲区,结果系统重启后还报错 |
我的习惯:写UEFI驱动时,先把Supported写严谨。它就像门卫,放错了设备进来,后面Start和Stop再怎么写都白搭。
驱动加载的流程是这样的:UEFI固件扫描总线上的设备,然后遍历所有已注册的驱动,调用每个驱动的Supported去匹配。匹配上了就调用Start。说白了就是「相亲」——驱动和设备互相看对眼了,就在一起。
UEFI协议概念:一切皆协议
UEFI协议,你可以把它理解成C++里的接口类。它是一组函数指针的集合,封装在结构体里。每个协议都有一个GUID作为唯一标识。你想用某个功能?先找到对应的协议,然后调用它的函数。
举个例子,你想读写磁盘上的文件:
// 1. 先找Simple File System Protocol
EFI_SIMPLE_FILE_SYSTEM_PROTOCOL *FileSystem;
status = gBS->LocateProtocol(
&gEfiSimpleFileSystemProtocolGuid,
NULL,
(VOID**)&FileSystem
);
// 2. 打开根目录
EFI_FILE_PROTOCOL *Root;
status = FileSystem->OpenVolume(FileSystem, &Root);
// 3. 打开具体文件
EFI_FILE_PROTOCOL *File;
status = Root->Open(Root, &File, L"\\EFI\\BOOT\\BOOTX64.EFI",
EFI_FILE_MODE_READ, 0);
看到了吗?每一步都在跟协议打交道。LocateProtocol、OpenVolume、Open,全是协议里的函数。
警告:协议使用完要CloseHandle。我曾经在项目里忘了关文件协议,导致固件升级时文件句柄泄漏,最后系统跑着跑着就崩了。排查了两天才找到原因。
协议的生命周期
协议不是凭空出现的。它有明确的创建、安装、使用、卸载流程:
- 创建:驱动或应用在堆上分配协议实例的内存。
- 安装:调用
InstallProtocolInterface()或InstallMultipleProtocolInterfaces()把协议注册到系统。 - 使用:其他模块通过
LocateProtocol()或OpenProtocol()获取协议指针。 - 卸载:调用
UninstallProtocolInterface()移除协议。
我个人习惯在驱动入口点(Entry Point)里安装协议,在卸载函数(Unload)里卸载。这样生命周期清晰,不容易出问题。
常见的UEFI协议一览
UEFI规范定义了上百个协议。但实际开发中,常用的就那么十几个。我列几个你一定会遇到的:
| 协议名称 | GUID | 用途 |
|---|---|---|
| Device Path Protocol | ...09576e91-6d3f-11d2-8e39-00a0c969723b |
描述设备路径,定位硬件 |
| Loaded Image Protocol | ...5b1b31a1-9562-11d2-8e3f-00a0c969723b |
获取当前镜像信息,入口参数 |
| Simple Text Output Protocol | ...387477c2-69c7-11d2-8e39-00a0c969723b |
屏幕输出,调试必备 |
| Block I/O Protocol | ...964e5b21-6459-11d2-8e39-00a0c969723b |
块设备读写,磁盘操作基础 |
避坑指南:我曾经在项目里直接用LocateProtocol找Block I/O协议,结果返回了NULL。后来才发现,有些磁盘控制器驱动还没加载。正确的做法是先确保设备驱动已绑定,或者用OpenProtocol从设备Handle上打开。
协议与Handle的关系
Handle是UEFI里的另一个核心概念。你可以把Handle想象成一个「钉子」,协议就是挂在钉子上的「牌子」。一个Handle上可以挂多个协议。比如一个磁盘控制器Handle上,可能同时挂着Device Path Protocol、Block I/O Protocol、Disk I/O Protocol。
查找设备的标准流程是:先枚举所有Handle,然后对每个Handle查询它是否安装了你要的协议。这就像在一排钉子上找挂着你想要牌子的那个钉子。
嗯,说到这儿,我想起一个调试技巧。如果你不确定某个设备上挂了哪些协议,可以用UEFI Shell里的dh命令查看。它会列出所有Handle及其关联的协议GUID。这招在我排查驱动加载问题时帮了大忙。
小结
UEFI固件的基础就这三块:架构分阶段、驱动靠模型、功能用协议。你把这三点吃透了,后面写UEFI应用、做系统部署,心里就有底了。说白了,UEFI就是一套面向对象的框架——协议是接口,Handle是对象实例,驱动是实现了接口的类。搞清楚了这层关系,剩下的就是查规范、调API的事了。