3、UEFI基础概念:GUID、Protocol、Handle、Image、事件(Event)与任务优先级(TPL)

好,咱们今天来啃UEFI里最核心的几个概念。说实话,刚接触UEFI的时候,我被这些名词绕得晕头转向。GUID、Protocol、Handle……每个词都认识,凑在一起就不知道它们在干嘛。后来写了几年代码,踩了不少坑,才慢慢理清楚它们之间的关系。

我个人的理解是:UEFI本质上是一个面向对象的框架。Handle就是对象实例,Protocol就是接口,GUID就是接口的唯一ID。Image是程序本身,Event和TPL则是系统用来处理异步任务和同步的机制。咱们一个一个来拆。

一句话总结:Handle是“谁”,Protocol是“能干什么”,GUID是“叫什么”,Image是“代码本体”,Event是“什么时候干”,TPL是“谁先干”。

3.1 GUID——全球唯一标识符

GUID,全称Globally Unique Identifier。说白了就是一个128位的数字,用来唯一标识某个东西。在UEFI里,Protocol、Image、事件类型、甚至某些系统表,都有自己的GUID。

你想想看,UEFI里可能有几百个Protocol,怎么区分它们?靠名字?不行,名字可能重复。靠编号?也不行,容易冲突。GUID就是解决方案——它保证全球唯一。

// GUID 结构定义
typedef struct {
    UINT32  Data1;    // 4字节
    UINT16  Data2;    // 2字节
    UINT16  Data3;    // 2字节
    UINT8   Data4[8]; // 8字节
} EFI_GUID;

举个例子,我们常用的磁盘驱动Protocol的GUID是这样的:

// 磁盘I/O Protocol 的 GUID
#define EFI_DISK_IO_PROTOCOL_GUID \
  {0xce345171, 0xba0b, 0x11d2, {0x8e, 0x4f, 0x00, 0xa0, 0xc9, 0x69, 0x72, 0x3b}}

我在项目中遇到过一个问题:两个模块用了同一个GUID但实现不同,结果系统加载时直接崩溃。嗯,GUID重复是灾难性的。所以UEFI规范里规定,GUID必须通过官方工具生成,不能自己随便编一个。

小技巧:调试时可以用 gBS->LocateProtocol() 配合GUID来查找某个Protocol是否已经安装。如果返回EFI_NOT_FOUND,说明这个Protocol还没注册。

3.2 Handle——对象的“身份证”

Handle是什么?它是一个不透明的指针,本质上就是一个 VOID *。每个Handle代表系统里的一个“对象”——比如一个设备、一个驱动、一个文件系统。

我习惯把Handle理解成“身份证号”。你拿着身份证号,可以去查这个人的信息。同样,你拿着Handle,可以去查它上面安装了哪些Protocol。

// Handle 类型定义
typedef VOID *EFI_HANDLE;

Handle本身不存数据,它只是一个索引。真正的数据存在Protocol里。一个Handle上可以挂多个Protocol,就像一个人可以同时拥有身份证、驾驶证、护照一样。

举个例子:一个USB键盘设备,它的Handle上可能挂了这些Protocol:

  • Device Path Protocol——描述设备路径
  • Simple Text Input Protocol——键盘输入功能
  • I/O Protocol——底层读写功能

我记得有一次调试,怎么都找不到某个设备。后来发现是Handle被意外关闭了,但代码还在用这个Handle去查Protocol。结果当然是系统挂掉。所以记住:Handle用完后要检查有效性。

3.3 Protocol——接口就是契约

Protocol是UEFI里最重要的概念之一。它定义了一组函数指针和数据结构,说白了就是一个接口规范。

你想想看,UEFI要支持各种硬件——不同厂商的网卡、硬盘、显卡。怎么让驱动和应用程序能互相配合?答案就是Protocol。每个硬件驱动都实现标准的Protocol,应用程序通过Protocol来调用硬件功能,不用关心底层实现。

// 一个简单的 Protocol 示例
typedef struct {
    UINT64    Revision;    // 版本号
    EFI_STATUS (*Read)(IN EFI_FILE_PROTOCOL *This, 
                       IN OUT UINTN *BufferSize, 
                       OUT VOID *Buffer);
    EFI_STATUS (*Write)(IN EFI_FILE_PROTOCOL *This, 
                        IN OUT UINTN *BufferSize, 
                        IN VOID *Buffer);
} EFI_FILE_PROTOCOL;

Protocol的安装和查找,靠的是前面说的GUID和Handle。流程是这样的:

  1. 驱动创建一个Protocol实例,包含函数指针和数据
  2. 调用 gBS->InstallProtocolInterface() 把Protocol挂到某个Handle上
  3. 应用程序调用 gBS->LocateProtocol() 通过GUID找到Protocol
  4. 然后就可以调用Protocol里的函数了

注意:Protocol的版本号很重要。我曾经遇到一个坑:新版本的Protocol改了结构体大小,但旧代码还在用旧版本去强制转换,结果内存越界。所以检查Revision字段是必要的。

3.4 Image——可执行程序的化身

Image就是UEFI里的可执行程序。它可以是EFI应用程序(比如Shell.efi),也可以是EFI驱动(比如某个网卡驱动)。

每个Image在加载后,系统会为它创建一个Handle,并挂上 EFI_LOADED_IMAGE_PROTOCOL。这个Protocol里包含了Image的加载地址、入口点、命令行参数等信息。

// 获取当前Image的信息
EFI_STATUS Status;
EFI_LOADED_IMAGE_PROTOCOL *LoadedImage;

Status = gBS->HandleProtocol(
    ImageHandle,                    // 当前Image的Handle
    &gEfiLoadedImageProtocolGuid,   // 固定的GUID
    (VOID **)&LoadedImage
);

if (!EFI_ERROR(Status)) {
    // LoadedImage->FilePath 是设备路径
    // LoadedImage->LoadOptions 是命令行参数
    // LoadedImage->ImageBase 是加载基地址
}

我个人习惯在调试时打印Image的加载地址,这样可以确认代码是否被正确加载到内存。如果地址不对,多半是加载器出了问题。

3.5 事件(Event)与任务优先级(TPL)

这两个概念经常一起出现,咱们放在一起讲。

3.5.1 事件——异步通知机制

Event是UEFI里的异步通知机制。你可以创建一个事件,设置一个回调函数,然后告诉系统:“当某个条件满足时,调用我这个函数。”

事件有几种类型:

事件类型 说明 典型用途
Timer事件 定时触发 周期性检查设备状态
Signal事件 手动触发 通知其他模块某个操作完成
Wait事件 等待某个条件 等待按键输入
Exit事件 Image退出时触发 清理资源
// 创建一个定时事件,每100ms触发一次
EFI_EVENT TimerEvent;
EFI_STATUS Status;

Status = gBS->CreateEvent(
    EVT_TIMER | EVT_NOTIFY_SIGNAL,  // 事件类型
    TPL_CALLBACK,                    // 优先级
    TimerCallback,                   // 回调函数
    NULL,                            // 回调参数
    &TimerEvent
);

// 设置定时器为周期性触发,100ms
gBS->SetTimer(TimerEvent, TimerPeriodic, 1000000); // 单位是100ns

嗯,这里要注意:回调函数里不能做耗时操作。我曾经在回调里调用了 gBS->Stall() 做延时,结果整个系统卡死。因为事件回调是在中断上下文执行的,不能阻塞。

3.5.2 任务优先级(TPL)——谁先谁后

TPL,全称Task Priority Level。它是UEFI里用来控制任务执行顺序的机制。说白了就是:高优先级的任务可以打断低优先级的任务。

UEFI定义了四个优先级级别:

级别 名称 说明
0 TPL_APPLICATION 应用程序级别,最低
1-15 TPL_CALLBACK 回调函数级别
16-30 TPL_NOTIFY 通知级别,用于事件回调
31 TPL_HIGH_LEVEL 最高级别,用于中断处理

为什么要搞这么复杂?因为UEFI是单线程的,没有操作系统的进程调度。如果不控制优先级,一个耗时操作就会把整个系统卡住。

举个例子:你在应用程序里(TPL_APPLICATION)正在打印日志,突然来了一个定时事件(TPL_CALLBACK),系统会暂停打印,先去执行回调。回调执行完,再回来继续打印。

核心规则:

  • 只能在当前TPL或更低的TPL下执行操作
  • 高TPL的代码不能调用可能阻塞的函数(比如等待按键)
  • TPL_HIGH_LEVEL下几乎不能做任何操作,只能处理中断

我曾经犯过一个错误:在TPL_NOTIFY级别的回调里调用了 gBS->AllocatePool()。结果系统直接ASSERT。因为内存分配需要在TPL_CALLBACK或更低级别才能做。从那以后,我养成了一个习惯:在回调函数里只做最轻量级的操作,复杂逻辑通过Signal事件抛到低优先级去处理。

3.6 知识体系总览

说了这么多,咱们用一张图来总结一下这些概念之间的关系:

UEFI核心概念关系图 Handle(对象身份证) GUID(唯一标识) Protocol(接口契约) Image(可执行程序) Event(异步通知) TPL(任务优先级) 核心关系说明 • Handle 是中心,上面可以挂多个 Protocol • 每个 Protocol 通过 GUID 来唯一标识和查找 • Image 加载后也会生成一个 Handle,并挂载 LoadedImage Protocol 标识 安装/查找 加载后生成 事件回调受TPL控制 事件可关联Handle

这张图把咱们今天讲的内容串起来了。Handle是核心,Protocol和Image都围绕它展开。GUID是Protocol的“身份证号”。Event和TPL则负责协调这些组件之间的异步交互和优先级控制。

说实话,刚开始理解这些概念确实需要一点时间。但一旦你写几个实际的UEFI驱动或应用程序,亲手创建过Handle、安装过Protocol、处理过Event回调,这些概念就会变得非常自然。嗯,别急,后面几章咱们会动手写代码,到时候你就明白了。