3、UEFI基础框架:EDK2工程结构、模块类型与固件体积管理
好,咱们今天聊点实在的。EDK2 这个框架,说白了就是 UEFI 固件的“骨架”。你如果要做 UEFI 移植,第一件事就是搞懂它怎么搭起来的。我当年刚接触 EDK2 的时候,看着那一堆 .dsc、.fdf、.inf 文件,说实话有点懵。后来踩了几个坑,才慢慢摸清楚门道。
3.1 EDK2 工程结构:三个核心文件
EDK2 的工程结构,其实不复杂。你只要盯住三个文件就行:
- .dsc(平台描述文件)—— 定义平台要编译哪些模块、用什么库
- .fdf(固件描述文件)—— 定义固件镜像的布局,哪个模块放哪个位置
- .inf(模块信息文件)—— 每个模块自己的“身份证”,告诉编译器它依赖什么
我习惯把 .dsc 比作“菜单”,.fdf 比作“拼图板”,.inf 比作“食材清单”。你想想看,菜单决定了吃什么,拼图板决定了怎么摆,食材清单决定了每道菜需要什么料。三者缺一不可。
核心要点:移植一个新平台,你通常只需要修改 .dsc 和 .fdf,.inf 一般不动。除非你要加新模块。
3.2 模块类型:DXE / SMM / PEI
EDK2 把固件执行分成了几个阶段。我重点讲三个最常见的:
3.2.1 PEI(Pre-EFI Initialization)
这是最早运行的阶段。内存还没初始化,代码只能在 Cache-as-RAM(CAR)里跑。说白了,就是“裸奔”阶段。PEI 模块体积要非常小,我见过有人把 PEI 模块写大了,结果 CAR 空间不够,直接死机。嗯,这里要注意:PEI 里别放复杂逻辑,能少就少。
3.2.2 DXE(Driver Execution Environment)
内存初始化完成后,就进入 DXE 阶段了。这时候你可以尽情发挥,加载各种驱动:PCI 枚举、USB 初始化、文件系统支持等等。DXE 模块是 UEFI 固件的主体,体积最大。我个人习惯把 DXE 模块按功能拆成多个小驱动,方便调试和复用。
3.2.3 SMM(System Management Mode)
SMM 是个特殊的存在。它运行在系统管理模式里,对操作系统透明。我曾在项目中遇到过一个问题:SMM 里的代码访问了非法的内存地址,导致系统随机蓝屏。查了三天才定位到。所以,SMM 代码一定要谨慎,尤其是指针操作。
我的建议:如果你不确定某个功能该放哪个阶段,先放 DXE。DXE 最灵活,调试也最方便。等稳定了再考虑移到 PEI 或 SMM。
3.3 固件体积管理:别让固件“发福”
固件体积是个很现实的问题。Flash 芯片就那么几 MB,你塞的东西多了,就装不下。我见过一个项目,固件编译出来 8MB,但 Flash 只有 4MB,最后只能砍功能。所以,从一开始就要管好体积。
3.3.1 体积从哪里来?
固件体积主要来自三部分:
- 代码段:.text 段,存放可执行代码
- 数据段:.data 段,存放全局变量和静态变量
- 资源文件:比如图片、字体、字符串表
其中,资源文件往往是“体积杀手”。一张 BMP 图片可能就占几百 KB。我建议能用文字代替的,就别用图片。
3.3.2 压缩与解压缩
EDK2 支持 LZMA 压缩。你可以把不常用的模块压缩存放,启动时再解压。但要注意:解压会占用额外的 RAM 和时间。我一般只对 DXE 阶段的驱动做压缩,PEI 阶段的不压缩,因为 PEI 阶段 RAM 有限。
警告:不要对所有模块都做压缩。有些模块(比如 SMM)对延迟敏感,解压时间可能影响系统响应。
3.3.3 体积优化实战技巧
我总结了几条实用的:
- 去掉调试信息:Release 版本一定要关掉 DEBUG 宏,能省不少空间
- 合并小模块:把多个功能相近的小 .inf 合并成一个,减少模块头部的开销
- 使用 PCD 代替硬编码:PCD 可以动态配置,避免为不同平台编译多个版本
- 裁剪不必要的驱动:比如你的平台没有 USB 3.0,就别编译 XHCI 驱动
3.4 知识体系图:EDK2 工程结构全景
下面这张图,是我自己画的一个结构图。它把 EDK2 的工程结构、模块类型和体积管理串在了一起。你一看就明白。
3.5 避坑指南:我踩过的几个坑
最后,分享几个我亲身经历过的坑,希望能帮你少走弯路:
- 我曾经在 .dsc 里漏加了一个库的引用,结果编译出来的固件在 DXE 阶段直接挂掉。查了两天才发现是库没链接上。所以,每次改 .dsc 后,建议先全量编译一次。
- 我曾经把一个 PEI 模块写得太大,超过了 CAR 的 64KB 限制。结果固件在 PEI 阶段就崩溃了。后来我把那个模块拆成了两个,问题解决。
- 我曾经在 SMM 里用了一个全局变量,结果每次从 SMM 返回后,那个变量的值就变了。后来才发现是 SMM 的上下文切换把数据弄丢了。SMM 里尽量用局部变量,或者用固定的内存地址。
一个小技巧:编译固件时,加上 --report-file 参数,可以生成一个详细的体积报告。哪个模块占了多少空间,一目了然。我每次编译都会看这个报告,心里有数。
好了,EDK2 的基础框架就聊到这里。记住:.dsc、.fdf、.inf 是三个核心文件;PEI、DXE、SMM 是三个关键阶段;体积管理要从一开始就重视。下一章咱们会深入讲 PEI 阶段的移植细节,到时候再聊。