3、UEFI设备树协议:EFI_DEVICE_TREE_PROTOCOL介绍、协议接口函数、如何获取设备树指针

3.1 这个协议到底是干嘛的?

说实话,我刚接触UEFI设备树协议时,也琢磨了好一阵子。

EFI_DEVICE_TREE_PROTOCOL,说白了就是UEFI固件和操作系统之间传递设备树的那座桥。你想想看,UEFI启动时已经把硬件摸得一清二楚了,这些信息怎么告诉内核?靠的就是这个协议。

我个人习惯把它理解成「一个装着设备树指针的信封」。UEFI把设备树在内存中的位置、大小等信息打包好,操作系统启动时直接拆开信封就能拿到。

核心要点:这个协议不负责解析设备树,它只负责「递送」设备树。解析工作是操作系统内核的事。

3.2 协议长什么样?

我们直接看代码。这是UEFI规范里定义的结构体:

typedef struct {
  UINT64                Revision;
  EFI_DEVICE_TREE_GET   GetDeviceTree;
  EFI_DEVICE_TREE_SET   SetDeviceTree;
  EFI_DEVICE_TREE_REMOVE RemoveDeviceTree;
} EFI_DEVICE_TREE_PROTOCOL;

嗯,结构很简单,就四个成员。Revision是版本号,后面三个是函数指针。

我在项目中遇到过一个问题:有些固件实现只支持Revision 1,但我的驱动按Revision 2的规范去调用,结果直接挂掉了。所以拿到协议后,第一件事就是检查版本号。

小技巧:检查版本号时,用 Protocol->Revision >= 1 来判断,不要用 ==。这样向前兼容性更好。

3.3 三个接口函数详解

3.4.1 GetDeviceTree — 获取设备树

这个函数最常用。原型如下:

typedef
EFI_STATUS
(EFIAPI *EFI_DEVICE_TREE_GET)(
  IN  EFI_DEVICE_TREE_PROTOCOL  *This,
  OUT VOID                      **DeviceTreeBase,
  OUT UINTN                     *DeviceTreeSize
);

调用后,DeviceTreeBase 指向设备树在内存中的起始地址,DeviceTreeSize 给出总大小。

我曾经踩过一个坑:拿到的指针指向的是UEFI的Runtime区域,操作系统进入长模式后这块内存可能被映射成别的用途。正确的做法是在ExitBootServices之前就把设备树拷贝到安全的地方。

3.4.2 SetDeviceTree — 设置设备树

这个函数允许你替换或更新设备树。原型:

typedef
EFI_STATUS
(EFIAPI *EFI_DEVICE_TREE_SET)(
  IN EFI_DEVICE_TREE_PROTOCOL  *This,
  IN VOID                      *DeviceTreeBase,
  IN UINTN                     DeviceTreeSize
);

说实话,这个函数我平时用得不多。但在做设备树覆盖(overlay)时,它就很关键了。比如你要在UEFI阶段根据用户选择的外设动态修改设备树,就得用它。

注意:调用SetDeviceTree后,原来的设备树内存不会被自动释放。如果你之前用AllocatePool分配过,记得自己FreePool,否则会内存泄漏。我曾经因为这个被同事吐槽过...

3.4.3 RemoveDeviceTree — 移除设备树

这个函数更简单:

typedef
EFI_STATUS
(EFIAPI *EFI_DEVICE_TREE_REMOVE)(
  IN EFI_DEVICE_TREE_PROTOCOL  *This
);

调用后,协议内部会把设备树指针清空。什么场景下会用?我记得有一次调试时,发现设备树和ACPI有冲突,我临时把设备树移除掉,只让系统用ACPI启动,问题一下子就定位到了。

3.4 如何获取设备树指针?

获取设备树指针,核心就两步:

  1. 找到协议 — 用 LocateProtocol 或 HandleProtocol
  2. 调用GetDeviceTree — 拿到指针和大小

代码示例:

EFI_DEVICE_TREE_PROTOCOL  *DtProtocol = NULL;
VOID                      *DtBase     = NULL;
UINTN                     DtSize      = 0;
EFI_STATUS                Status;

// 第一步:找到协议
Status = gBS->LocateProtocol(
           &gEfiDeviceTreeProtocolGuid,
           NULL,
           (VOID**)&DtProtocol
         );
if (EFI_ERROR(Status)) {
  DEBUG((EFI_D_ERROR, "找不到设备树协议!错误码:%r\n", Status));
  return Status;
}

// 第二步:获取设备树指针
Status = DtProtocol->GetDeviceTree(DtProtocol, &DtBase, &DtSize);
if (EFI_ERROR(Status)) {
  DEBUG((EFI_D_ERROR, "获取设备树失败!错误码:%r\n", Status));
  return Status;
}

DEBUG((EFI_D_INFO, "设备树地址:0x%p,大小:0x%x 字节\n", DtBase, DtSize));

你看,代码其实不复杂。但有几个细节要注意:

  • 时机问题:最好在DXE阶段早期就获取,越晚越容易出问题
  • 内存属性:拿到的指针指向的内存,属性可能是EfiRuntimeServicesData,别随便改
  • 多实例:有些平台会注册多个设备树协议实例,用LocateHandleBuffer遍历一下更稳妥

我的习惯:拿到设备树指针后,我会立即用 gBS->CopyMem 把它拷贝到 EfiBootServicesData 类型的内存中。这样后续操作更安全,也不怕UEFI退出后内存被回收。

3.5 知识体系一览

下面这张图,是我梳理的本章核心逻辑:

EFI_DEVICE_TREE_PROTOCOL 知识体系 EFI_DEVICE_TREE_PROTOCOL GetDeviceTree SetDeviceTree RemoveDeviceTree 获取设备树 返回基地址和大小 设置/更新设备树 支持overlay动态修改 移除设备树 清空指针,调试用 获取流程:LocateProtocol → GetDeviceTree → 拷贝到安全内存 ⚠ 注意:检查版本号 | 在ExitBootServices前拷贝 | 注意内存属性

3.6 避坑指南

最后,分享几个我实际踩过的坑:

  • 协议找不到? 检查一下你的 .inf 文件有没有加 gEfiDeviceTreeProtocolGuid 的依赖。我有一回忘了加,LocateProtocol死活返回EFI_NOT_FOUND,查了半天才发现。
  • 拿到的指针是NULL? 可能是平台固件还没初始化设备树。试试在 gEfiPlatformToDriverConfigurationProtocolGuid 回调之后再去获取。
  • 设备树大小不对? 有些固件实现里,DeviceTreeSize 返回的是整个FDT块的大小,包括填充字节。别拿这个值去 AllocatePool 然后直接memcpy,多出来的字节可能是垃圾数据。

嗯,关于EFI_DEVICE_TREE_PROTOCOL的核心内容,差不多就这些了。记住一句话:这个协议就是个快递员,它不生产设备树,它只是设备树的搬运工。

可可精品资料库