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 如何获取设备树指针?
获取设备树指针,核心就两步:
- 找到协议 — 用
LocateProtocol或HandleProtocol - 调用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 知识体系一览
下面这张图,是我梳理的本章核心逻辑:
3.6 避坑指南
最后,分享几个我实际踩过的坑:
- 协议找不到? 检查一下你的
.inf文件有没有加gEfiDeviceTreeProtocolGuid的依赖。我有一回忘了加,LocateProtocol死活返回EFI_NOT_FOUND,查了半天才发现。 - 拿到的指针是NULL? 可能是平台固件还没初始化设备树。试试在
gEfiPlatformToDriverConfigurationProtocolGuid回调之后再去获取。 - 设备树大小不对? 有些固件实现里,
DeviceTreeSize返回的是整个FDT块的大小,包括填充字节。别拿这个值去AllocatePool然后直接memcpy,多出来的字节可能是垃圾数据。
嗯,关于EFI_DEVICE_TREE_PROTOCOL的核心内容,差不多就这些了。记住一句话:这个协议就是个快递员,它不生产设备树,它只是设备树的搬运工。