简介这份PPT资料围绕UEFI启动流程中的DXEDriver Execution Environment阶段展开面向固件开发工程师、BIOS初学者及嵌入式系统学习者帮助理解DXE阶段在系统初始化中的核心作用与运行机理。资源共1个pptx文件压缩包约44.17MB内容以图文幻灯片形式呈现便于按章节浏览与讲解。资料系统梳理了UEFI Boot Flow中SEC、PEI与DXE三阶段的衔接关系重点讲解DXE Foundation、DXE Dispatcher及DXE Drivers三部分职责并深入分析HOB List传递、可移植性设计、固件卷发现、依赖表达式评估、状态机流转以及emergency patch加载机制同时涉及EFI系统表、Boot Services、Runtime Services与DXE Services等关键数据结构。已有1153人学习下载适合需要建立DXE阶段整体认知、梳理驱动调度顺序与协议抽象思路的读者参考。1. UEFI中DXE阶段到底在跑什么从加电到操作系统之间的关键一跳很多人第一次看 UEFI 启动流程注意力都停在 SEC 和 PEI觉得那才是“最底层、最硬核”的部分到了 DXE 就默认它只是走个过场。实际恰恰相反DXE 阶段才是固件真正开始“像操作系统一样干活”的地方。你按电源键之后CPU 初始化、内存控制器配置这些事在 PEI 阶段已经做完了但那时候能用的资源极其有限连完整的堆分配都没有。到了 DXE固件手里终于有了可用的内存空间、有了协议数据库、有了事件机制才开始批量加载驱动、枚举总线、初始化外设最后把控制权交给 BDS 去选启动项。换句话说DXE 是固件从“能跑”到“能用”的分水岭。如果你在做主板 bring-up、写 UEFI 驱动、调启动卡死问题或者单纯想搞清楚为什么换个内存条就开不了机DXE 阶段是你绕不过去的一站。这一章先把 DXE 在整条启动链里的位置和它解决的核心问题讲清楚后面几章再落到具体怎么写驱动、怎么调参数、怎么排查翻车现场。2. DXE 的骨架Dispatcher、协议数据库和事件机制怎么配合2.1 DXE Core 启动时先干了哪三件事DXE 阶段的第一段代码是 DXE Core它本身也是一个被 PEI 阶段“交接”过来的模块。DXE Core 入口拿到 PEI 传过来的 HOB 列表之后不会立刻去加载驱动而是先做三件基础建设。第一件是初始化内存服务把 HOB 里描述的系统内存、MMIO 区域登记成 DXE 自己能管理的资源后面所有 AllocatePool、AllocatePages 都依赖这一步。第二件是建立协议数据库也就是 Protocol Database它本质上是一个全局的链表或哈希表记录“哪个 GUID 的协议被安装到了哪个 Handle 上”。第三件是初始化事件机制包括定时器事件、通知事件和回调事件让驱动之间可以异步解耦。这三件事做完DXE Core 才会去遍历固件卷找那些类型为 DXE_DRIVER 的模块交给 Dispatcher 去调度。这里有个容易混淆的点DXE Core 和 DXE Dispatcher 不是一回事。Core 提供的是运行时服务的基础设施Dispatcher 负责的是“按依赖顺序把驱动一个个跑起来”。Dispatcher 会读取每个驱动模块的依赖表达式也就是 DEPEX决定谁先谁后。如果 A 驱动依赖协议 P而 P 还没被安装A 就会被挂起直到某个驱动安装了 PDispatcher 再回头把 A 捞起来继续跑。这个机制是 DXE 能支持“驱动之间不写死加载顺序”的关键。2.2 协议数据库为什么是 DXE 的“黑匣子”协议数据库是 DXE 阶段最核心的抽象。你可以把它理解成一个服务注册中心任何驱动都可以把自己提供的功能封装成一个协议用 GUID 标识安装到某个 Handle 上其他驱动只要拿到这个 GUID就能通过 LocateProtocol 或 OpenProtocol 找到对应的函数表直接调用。这样做的好处是驱动之间不需要互相 include 头文件也不需要知道对方在哪个固件卷里只依赖一个 GUID 和一套接口定义。实际写代码的时候安装协议通常长这样// 假设我们定义了一个 MY_PROTOCOL包含一个 GetVersion 函数 EFI_STATUS Status; MY_PROTOCOL *MyProtocolInstance; // 分配协议实例内存 Status gBS-AllocatePool( EfiBootServicesData, sizeof(MY_PROTOCOL), (VOID **)MyProtocolInstance ); if (EFI_ERROR(Status)) { return Status; } // 填充函数表 MyProtocolInstance-GetVersion MyGetVersion; MyProtocolInstance-Revision MY_PROTOCOL_REVISION; // 把协议安装到当前驱动的 ImageHandle 上 Status gBS-InstallProtocolInterface( ImageHandle, gMyProtocolGuid, EFI_NATIVE_INTERFACE, MyProtocolInstance );这段代码里AllocatePool 的第二个参数是内存类型EfiBootServicesData 表示这块内存在 ExitBootServices 之后会被释放适合放协议实例这种只在启动阶段用的数据。InstallProtocolInterface 的第一个参数是 Handle通常传当前驱动的 ImageHandle表示“这个协议是由我这个驱动提供的”。第三个参数 EFI_NATIVE_INTERFACE 表示接口是原生 C 函数表不是兼容模式。安装成功之后其他驱动就可以用 gBS-LocateProtocol(gMyProtocolGuid, NULL, (VOID **)Instance) 来拿到这个实例。注意协议安装之后不要手动释放协议实例的内存除非你先调用 UninstallProtocolInterface。否则其他驱动可能还持有指针你一释放就是野指针后面随机崩溃查起来非常痛苦。2.3 事件和定时器驱动之间怎么做到不互相阻塞DXE 的事件机制分三类定时器事件、通知事件和回调事件。定时器事件最常用比如某个驱动需要等外设上电稳定 100 毫秒再继续初始化就可以创建一个定时器事件把后续操作放在回调里而不是死循环等。通知事件用于监听某个协议或某个 GUID 的安装比如 BDS 会注册一个通知事件等所有启动项相关协议都就绪之后再开始枚举启动设备。回调事件则是在 TPL 变化时触发一般驱动开发很少直接用。创建定时器事件的典型写法EFI_EVENT TimerEvent; EFI_STATUS Status; // 创建一个相对定时器100000 微秒 100 毫秒后触发 Status gBS-CreateEvent( EVT_TIMER | EVT_NOTIFY_SIGNAL, TPL_CALLBACK, MyTimerCallback, NULL, TimerEvent ); if (EFI_ERROR(Status)) { return Status; } // 设置定时器相对时间 100 毫秒 Status gBS-SetTimer( TimerEvent, TimerRelative, 100000 );CreateEvent 的第二个参数是 TPL也就是任务优先级。TPL_CALLBACK 是普通回调级别比 TPL_APPLICATION 高比 TPL_NOTIFY 低。定时器回调里不要做太耗时的操作否则会阻塞其他同级别或低级别的任务。如果确实需要做重活可以在回调里发一个事件让另一个 TPL_APPLICATION 级别的任务去处理。SetTimer 的第三个参数单位是 100 纳秒所以 100 毫秒要写 100000。这个单位换算我见过不少人第一次写错写成 100 结果 10 微秒就触发了外设还没上电稳定初始化直接失败。3. 写一个最小 DXE 驱动从 INF 到加载验证的完整路径3.1 驱动入口和 ImageHandle 到底怎么用DXE 驱动的入口函数签名是固定的EFI_STATUS EFIAPI MyDriverEntryPoint( IN EFI_HANDLE ImageHandle, IN EFI_SYSTEM_TABLE *SystemTable ) { EFI_STATUS Status; // 在这里做初始化安装协议创建事件 Status MyDriverInitialize(ImageHandle, SystemTable); if (EFI_ERROR(Status)) { return Status; } return EFI_SUCCESS; }ImageHandle 是当前驱动模块的句柄SystemTable 是系统表指针里面包含 BootServices、RuntimeServices 和 ConfigurationTable。入口函数返回 EFI_SUCCESS 表示驱动加载成功返回错误码的话 Dispatcher 会认为这个驱动加载失败可能会影响依赖它的其他驱动。注意入口函数里不要做太耗时的操作因为 Dispatcher 是顺序调度的你卡在这里后面的驱动都起不来。如果确实需要等硬件用定时器事件异步处理。3.2 INF 文件里哪些字段决定驱动能不能被 Dispatcher 认出来INF 文件是 DXE 驱动的“身份证”Dispatcher 靠它来判断这个模块是不是 DXE_DRIVER、依赖哪些协议、在哪个阶段加载。一个最小的 INF 大概长这样[Defines] INF_VERSION 0x00010005 BASE_NAME MyDxeDriver FILE_GUID 12345678-1234-1234-1234-1234567890AB MODULE_TYPE DXE_DRIVER VERSION_STRING 1.0 ENTRY_POINT MyDriverEntryPoint [Sources] MyDriver.c [Packages] MdePkg/MdePkg.dec MdeModulePkg/MdeModulePkg.dec [LibraryClasses] UefiDriverEntryPoint UefiLib DebugLib [Protocols] gMyProtocolGuid ## PRODUCES [Depex] gEfiPciIoProtocolGuidMODULE_TYPE 必须是 DXE_DRIVER否则 Dispatcher 不会把它当 DXE 驱动处理。ENTRY_POINT 要和 C 文件里的函数名一致。FILE_GUID 是这个模块的唯一标识不能和别的模块重复否则构建系统会报错。PRODUCES 表示这个驱动会安装 gMyProtocolGuidPRODUCES 和 CONSUMES 不是强制的但写上之后构建系统会帮你做依赖检查。Depex 段是最关键的它告诉 Dispatcher 这个驱动依赖 gEfiPciIoProtocolGuid如果这个协议还没被安装驱动就会被挂起。Depex 写得太严格会导致驱动永远不加载写得太松又可能在依赖没就绪时跑起来直接崩这个度要自己把握。3.3 用 QEMU 加 OVMF 验证驱动加载的最小命令写完驱动之后最直接的验证方式是在 QEMU 里跑 OVMF。假设你已经编译出了 OVMF.fd 和你的驱动.efi可以把驱动放到一个 FAT 格式的磁盘镜像里然后启动 QEMU# 创建一个 64MB 的磁盘镜像 dd if/dev/zero ofdisk.img bs1M count64 # 格式化为 FAT32 mkfs.vfat -F 32 disk.img # 挂载镜像并把驱动复制进去 mkdir -p /mnt/efi sudo mount -o loop disk.img /mnt/efi sudo mkdir -p /mnt/efi/EFI/BOOT sudo cp MyDxeDriver.efi /mnt/efi/EFI/BOOT/ sudo umount /mnt/efi # 启动 QEMU使用 OVMF 固件 qemu-system-x86_64 \ -machine q35 \ -drive ifpflash,formatraw,readonlyon,fileOVMF.fd \ -drive filedisk.img,formatraw \ -serial stdio \ -nographic启动之后进入 UEFI Shell用 load MyDxeDriver.efi 手动加载驱动或者把驱动放到 Driver#### 变量里让 BDS 自动加载。如果驱动入口打了 DEBUG 输出你会在串口里看到对应的日志。如果加载失败先看返回码EFI_INVALID_PARAMETER 通常是 INF 或入口函数签名有问题EFI_NOT_FOUND 多半是 Depex 依赖的协议没就绪EFI_OUT_OF_RESOURCES 则是内存分配失败检查 AllocatePool 的大小和内存类型。提示QEMU 的 OVMF 默认没有打开串口输出需要在编译 OVMF 时设置 -D DEBUG_SERIAL_ENABLE或者用 -debugcon file:debug.log 把调试输出重定向到文件。4. DXE 阶段最常见的翻车现场协议没装、内存踩踏和 Depex 死锁4.1 驱动加载了但 LocateProtocol 返回 NOT_FOUND现象是驱动入口明明执行了DEBUG 日志也打出来了但调用 LocateProtocol 找某个协议时返回 EFI_NOT_FOUND。原因通常有三种第一种是提供该协议的驱动还没加载你的 Depex 没写对导致你比对方先跑第二种是对方驱动加载失败了协议根本没安装成功第三种是 GUID 写错了比如头文件里定义的是 gEfiPciIoProtocolGuid你在 C 文件里手写了一个长得差不多的 GUID编译能过但运行时找不到。解决办法先确认 Depex 里有没有写对方提供的协议 GUID如果没有就加上。然后看对方驱动的 DEBUG 输出确认它有没有加载成功。最后用 UEFI Shell 的 dh 命令列出所有 Handle再用 dmpstore 或 guid 命令查协议数据库确认目标 GUID 到底有没有被安装。GUID 写错这种问题最隐蔽建议始终用头文件里的符号不要手写。4.2 AllocatePool 之后没检查返回值后面随机崩现象是驱动跑着跑着突然跳到异常处理或者直接死机日志停在某个看似无关的地方。原因往往是 AllocatePool 返回了 EFI_OUT_OF_RESOURCES但代码没检查继续往 NULL 指针里写数据踩了低地址内存。DXE 阶段虽然内存比 PEI 充裕但如果你一次性分配几十 MB或者内存碎片严重照样会失败。解决办法所有 AllocatePool 和 AllocatePages 之后必须检查返回值失败就返回错误码不要继续往下跑。如果确实需要大块内存用 AllocatePages 按页分配比 AllocatePool 更可靠。另外注意内存类型EfiBootServicesData 在 ExitBootServices 之后会失效如果你在 Runtime 阶段还要用这块内存必须用 EfiRuntimeServicesData否则操作系统接管之后你一访问就是页错误。4.3 Depex 写成死锁驱动永远不加载现象是驱动在固件卷里明明存在但入口函数从来没被执行过DEBUG 日志里也看不到任何输出。原因是 Depex 里依赖的协议永远没有被安装Dispatcher 一直在等等到 BDS 阶段结束都没等到。常见的情况是 A 驱动依赖 B 协议B 驱动依赖 A 协议互相等形成死锁。或者你依赖了一个只在 PEI 阶段存在的协议DXE 阶段根本不会安装它。解决办法先用构建工具生成 Depex 依赖图看看有没有循环依赖。如果是循环依赖把其中一个驱动的 Depex 去掉改成在入口函数里用 LocateProtocol 主动查找找不到就返回 EFI_NOT_READY让 Dispatcher 稍后重试。另外确认依赖的协议确实是 DXE 阶段安装的PEI 阶段的 PPI 和 DXE 阶段的 Protocol 是两套东西不能混用。4.4 在入口函数里死等硬件导致后续驱动全部超时现象是系统启动特别慢或者卡在某个进度条不动但没有任何报错。原因是你驱动的入口函数里写了一个 while 循环等硬件寄存器就绪硬件没就绪就一直等Dispatcher 是顺序调度的你卡在这里后面的驱动全起不来。更糟糕的是有些硬件在 QEMU 里根本不存在你的循环永远等不到。解决办法入口函数里不要死等用定时器事件异步轮询或者设置一个超时计数器超过一定次数就返回 EFI_TIMEOUT。如果硬件初始化确实需要时间把初始化拆成两步入口函数只做资源分配和事件注册真正的硬件操作放在定时器回调里这样不会阻塞 Dispatcher。4.5 忘记卸载协议导致热插拔或重启后行为异常现象是第一次启动正常热插拔设备或者软重启之后驱动行为异常比如重复安装协议返回 EFI_ALREADY_STARTED或者旧协议实例还在被引用。原因是你驱动卸载时没有调用 UninstallProtocolInterface协议数据库里还留着旧实例新驱动加载时要么安装失败要么拿到的是已经失效的旧指针。解决办法在驱动的 Unload 函数里按照安装的反序卸载协议、关闭事件、释放内存。Unload 函数通过 gBS-UnloadImage 或者驱动自身的卸载回调触发。如果你不打算支持卸载至少要在入口函数里判断协议是否已经安装避免重复安装。热插拔场景下设备移除时也要主动卸载相关协议否则操作系统或后续固件访问到野指针蓝屏或死机是迟早的事。5. 把 DXE 驱动调试效率提上去串口日志、断点和协议数据库 dump5.1 用 DEBUG 宏和串口输出把执行路径打出来DXE 阶段没有 printf但 EDK2 提供了 DebugLib可以用 DEBUG 宏输出不同级别的日志。常用级别有 DEBUG_ERROR、DEBUG_WARN、DEBUG_INFO、DEBUG_VERBOSE编译时通过 PcdDebugPrintErrorLevel 控制哪些级别会被输出。串口配置一般在 DSC 文件里指定比如[PcdsFixedAtBuild] gEfiMdePkgTokenSpaceGuid.PcdDebugPrintErrorLevel|0x8000004F gEfiMdeModulePkgTokenSpaceGuid.PcdSerialUseMmio|FALSE gEfiMdeModulePkgTokenSpaceGuid.PcdSerialRegisterBase|0x3F8 gEfiMdeModulePkgTokenSpaceGuid.PcdSerialBaudRate|1152000x8000004F 这个值表示打开 ERROR、WARN、INFO 和部分 VERBOSE 级别。PcdSerialRegisterBase 是串口寄存器的 IO 地址0x3F8 是 COM1 的标准地址。PcdSerialBaudRate 设成 115200和你的串口终端一致。如果日志乱码先检查波特率再检查数据位和停止位通常是 8N1。DEBUG 宏的用法DEBUG((DEBUG_INFO, MyDriver: EntryPoint called, ImageHandle%p\n, ImageHandle)); DEBUG((DEBUG_ERROR, MyDriver: AllocatePool failed, Status%r\n, Status));%r 是 EDK2 特有的格式符用来打印 EFI_STATUS 的字符串描述比 %x 直观得多。日志里带上函数名和关键变量出问题时能快速定位。5.2 在 QEMU 里挂 GDB 单步调试 DXE CoreQEMU 支持 GDB stub可以在启动时加 -s -S 参数让 QEMU 等待 GDB 连接。然后另开一个终端gdb OVMF.fd (gdb) target remote :1234 (gdb) break MyDriverEntryPoint (gdb) continue这样就能在驱动入口下断点单步执行查看寄存器和内存。DXE Core 的符号一般在 OVMF.fd 的调试版本里有编译时加 -D DEBUG_ON_SERIAL_PORT 和 -D BUILD_DEBUG。如果断点打不上检查 OVMF.fd 是不是 stripped 版本或者地址有没有被 ASLR 偏移。QEMU 的 -s 是 -gdb tcp::1234 的简写-S 表示启动时暂停 CPU等 GDB 发 continue 再跑。5.3 用 UEFI Shell 的 dh 和 dmpstore 快速确认协议状态进入 UEFI Shell 之后dh 命令列出所有 Handle 和它们安装的协议 GUID。如果输出太长可以用 dh -p gEfiPciIoProtocolGuid 过滤。dmpstore 用来 dump UEFI 变量比如 dmpstore -all 列出所有变量dmpstore BootOrder 只看启动顺序。协议数据库没有直接的 dump 命令但你可以写一个小的 DXE 驱动遍历 gBS-LocateHandleBuffer对每个 Handle 调用 ProtocolsPerHandle把 GUID 和协议实例地址打出来。这个工具驱动在 bring-up 阶段非常有用能一眼看出哪个协议装了、哪个没装、装到了哪个 Handle 上。提示dh 命令在 DXE 阶段就能用不需要进 BDS。如果你在 BDS 之前就卡住了可以在 DXE Core 的入口附近加一个断点或者用串口日志确认 Dispatcher 跑到哪一步了。5.4 一个真实踩坑DEBUG 日志把定时器回调拖慢导致超时某次调一个 USB 控制器驱动入口函数里注册了一个 10 毫秒的定时器轮询控制器状态。为了调试在回调里加了一堆 DEBUG_INFO 日志结果串口输出本身要花几毫秒10 毫秒的定时器实际执行间隔变成了 15 毫秒以上控制器状态机超时枚举失败。现象是日志看起来一切正常但设备就是认不到。后来把回调里的日志级别降到 DEBUG_VERBOSE只在出错时打 DEBUG_ERROR问题消失。这个坑的血泪经验是定时器回调里的操作必须考虑执行时间串口输出、内存拷贝这些看似轻量的操作在 10 毫秒级别的定时器里可能就是压垮骆驼的最后一根稻草。如果必须打日志用内存缓冲区先存起来等初始化完成后再统一输出。另外 TPL 级别也要注意TPL_NOTIFY 级别的回调里不能调用可能引起阻塞的 BootServices否则会触发 ASSERT。6. 从 DXE 到 BDS 的交接怎么确认你的驱动真的“交棒”成功了DXE 阶段结束的标志是 BDS 架构协议被安装然后 BDS 开始枚举启动项。但“DXE 结束”不等于“所有 DXE 驱动都加载完了”有些驱动可能因为 Depex 一直没满足而挂起有些可能加载失败但被忽略了。要确认你的驱动真的在 BDS 之前完成了初始化最可靠的方法是在驱动入口的最后打一条 DEBUG_INFO然后在 BDS 入口打另一条看两条日志的先后顺序。如果 BDS 的日志先出来说明你的驱动被 Dispatcher 跳过了回去检查 Depex 和加载状态。另一个验证手段是看 gEfiDxeServicesTableGuid 对应的 DXE 服务表里面记录了 Dispatcher 的统计信息包括已加载驱动数、挂起驱动数、失败驱动数。这个表在 UEFI Shell 里可以用 dmem 或者自定义工具读出来。如果挂起驱动数不为零说明有驱动的 Depex 没满足需要逐个排查。我自己的习惯是每次改完 Depex 或者驱动加载顺序先在 QEMU 里跑一遍用串口日志确认所有关键驱动的入口都执行了再上真实硬件。真实硬件的 bring-up 时间宝贵QEMU 里能复现的问题不要带到板子上。另外DXE 阶段的驱动尽量保持“无状态”初始化完成之后就把该装的协议装好该注册的事件注册好不要在 BDS 阶段还依赖 DXE 驱动的内部状态因为 BDS 可能会卸载或替换某些驱动。把 DXE 当成一个“服务启动阶段”你的驱动是服务提供者BDS 是服务消费者边界清晰了调试效率会高很多。希望帮到你。本文还有配套的精品资源点击获取