☰
UEFI Shell App开发实战:从环境搭建到协议调用与调试
2026/10/14 1:26:39 网站建设 项目流程

1. 从一次主板调试说起:UEFI Shell 到底是个什么东西

第一次接触 UEFI Shell 的人,多半是在一个比较尴尬的场景里:系统起不来,BIOS 界面翻来覆去看不出问题,PE 又懒得插,最后有人丢给你一句"进 Shell 里看看"。然后你面对一个黑底白字、长得像 DOS 又不太像 DOS 的命令行,敲了个ls发现不认,敲了个dir居然能跑,瞬间有点恍惚——这到底是个什么环境?

简单说,UEFI Shell 是 UEFI 固件环境下的一个交互式命令行程序,它本身就是一个标准的 UEFI Application(.efi 文件),由固件在启动过程中加载并执行。它运行在 UEFI Boot Services 还活着的阶段,能直接访问固件提供的各种协议(Protocol),比如文件系统协议、块设备协议、简单文本输出协议等等。你可以把它理解成"固件层的救援终端":操作系统还没起来,但硬件已经被固件初始化得七七八八,Shell 就是在这个窗口期给你一个能敲命令、能跑小工具的地方。

它和 Linux Shell、Windows CMD 最大的区别在于运行层级。Linux Shell 跑在内核之上,依赖系统调用;UEFI Shell 跑在固件之上,依赖的是 UEFI Boot Services 和 Runtime Services。这意味着它能看到的东西更底层——分区表、EFI 系统分区里的文件、PCI 设备、内存映射,甚至能直接读写物理内存和 IO 端口。也正因为如此,它成了固件开发、系统部署、故障排查里绕不开的一环。

这篇内容主要面向三类人:一是做固件/BIOS 相关工作的开发者,需要经常在 Shell 里验证驱动和协议;二是做系统部署和运维的工程师,需要批量处理 EFI 分区、配置启动项;三是对底层启动流程好奇、想自己写一个.efi小工具的技术爱好者。我会从 Shell 的运行机制讲起,然后落到"怎么自己写一个 Shell App"这个核心问题上,把编译、加载、调试、踩坑的完整链路都过一遍。

提示:本文提到的所有命令和代码示例,建议在虚拟机(如 QEMU + OVMF)里先跑通再上真机,避免误操作破坏引导记录。

2. Shell App 的运行底座:Boot Services、Protocol 与句柄数据库

要写 Shell App,不能只把它当成"写个 C 程序",得先搞清楚它脚下的地基。UEFI 环境和我们熟悉的用户态程序差别很大,很多新手卡住不是因为语法,而是因为不理解这套运行时模型。

2.1 Boot Services 是 Shell App 的"操作系统"

UEFI 把运行时分成两个阶段:Boot Services 阶段和 Runtime Services 阶段。Shell App 运行在 Boot Services 阶段,这个阶段固件提供了一大批服务函数,通过EFI_BOOT_SERVICES结构体暴露出来。常用的几类:

  • 内存服务:AllocatePool、FreePool、AllocatePages、FreePages,分别对应池内存和页内存的分配释放。
  • 协议服务:LocateProtocol、LocateHandleBuffer、OpenProtocol、HandleProtocol,用来查找和使用各种 Protocol。
  • 事件与定时:CreateEvent、SetTimer、WaitForEvent,做异步和延时。
  • 镜像服务:LoadImage、StartImage、Exit,用来加载和启动其他 efi 程序。
  • 文本输出:通过EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL实现,Shell 里的echo最终就是走到这里。

关键点在于:这些服务只在 Boot Services 阶段有效。一旦调用ExitBootServices(通常由操作系统加载器调用),这些函数指针就全部失效,再调用就是未定义行为。所以 Shell App 的生命周期天然被限制在"系统启动完成之前"。

2.2 Protocol 机制:UEFI 的"接口即服务"

UEFI 不用传统的"驱动注册设备节点"那套,而是用Protocol这个概念。一个 Protocol 就是一组函数指针加数据结构的集合,用 GUID 唯一标识。比如:

Protocol 名称GUID 用途典型场景
Simple File System文件系统访问读写 EFI 分区文件
Block IO块设备读写直接操作磁盘扇区
Simple Text Output文本输出打印信息到屏幕
Simple Text Input键盘输入读取用户按键
Loaded Image当前镜像信息获取自身路径、命令行参数
Device Path设备路径定位设备节点

Shell App 想用某个 Protocol,标准流程是:先LocateHandleBuffer找到所有支持该 Protocol 的句柄,再OpenProtocol打开,拿到接口指针后调用。用完要CloseProtocol,否则可能影响其他程序。

2.3 句柄数据库:一切皆句柄

UEFI 内部维护一个句柄数据库(Handle Database),每个句柄代表一个"对象",可以挂载多个 Protocol。比如一个磁盘控制器句柄上可能同时挂着 Block IO、Device Path、Driver Binding 等 Protocol。Shell 里的dh命令就是用来 dump 这个数据库的,devices命令则列出所有设备句柄。

理解这一点很重要,因为 Shell App 里经常要做的事就是"遍历句柄,找到我要的那个设备"。比如你想找到所有可写的文件系统,就得遍历句柄、筛选出带 Simple File System Protocol 的那些。

2.4 Shell 与普通 UEFI App 的差别

严格说,Shell App 就是 UEFI App,只是它被 Shell 加载并运行,能接收 Shell 传来的命令行参数。入口函数签名是:

EFI_STATUS EFIAPI UefiMain ( IN EFI_HANDLE ImageHandle, IN EFI_SYSTEM_TABLE *SystemTable )

ImageHandle是当前镜像的句柄,SystemTable是系统表,里面挂着 Boot Services、Runtime Services、Configuration Table 等。Shell 在加载你的 efi 时,会把命令行参数通过EFI_LOADED_IMAGE_PROTOCOL的LoadOptions字段传进来,你需要自己解析。

3. 动手写第一个 Shell App:从环境搭建到编译产物

理论讲够了,直接上手。我以 EDK2 这套开源固件开发框架为例,因为它是目前最主流、文档最全的选择。整个流程分四步:装工具链、拉代码、写模块、编译。

3.1 工具链准备:别在版本上栽跟头

EDK2 对工具链版本比较敏感,尤其是 GCC 和 Python。我踩过的坑是:系统自带的 GCC 太新,编译某些老模块会报 warning 当 error。建议用相对稳定的版本组合:

  • GCC:9.x 或 10.x 比较稳,太新(13+)容易触发新警告。
  • Python:3.8 到 3.10,EDK2 的构建脚本对 3.11+ 支持有时不完整。
  • NASM:2.15 以上,汇编部分需要。
  • iASL:ACPI 编译用,做 Shell App 一般用不到,但 EDK2 全量构建会检查。

Linux 下大致这样装(以 Ubuntu 系为例):

sudo apt update sudo apt install build-essential uuid-dev nasm acpica-tools python3 python3-pip

Windows 下建议用 WSL 或者 MSYS2,原生 Windows 编译 EDK2 配置起来更折腾。macOS 也能编,但交叉编译到 X64 需要额外配置,新手不推荐。

3.2 拉取 EDK2 与子模块

EDK2 依赖几个子模块,必须一起拉:

git clone https://github.com/tianocore/edk2.git cd edk2 git submodule update --init

如果网络慢,可以只初始化需要的子模块,但新手建议全拉,省得后面缺东西。拉完之后先跑一次环境初始化:

source edksetup.sh

这一步会设置WORKSPACE、EDK_TOOLS_PATH等环境变量,并编译 BaseTools。BaseTools 是 EDK2 的构建工具集,第一次编译要几分钟。

3.3 创建自己的 Shell App 模块

EDK2 的模块组织方式是:每个模块一个目录,里面有.inf(模块描述)、.c(源码)、.dsc(平台描述)等。我建议在edk2/ShellPkg/Application/下新建目录,因为这里本来就是放 Shell 应用的地方,构建配置现成。

假设模块名叫MyShellApp,目录结构:

edk2/ShellPkg/Application/MyShellApp/ ├── MyShellApp.c └── MyShellApp.inf

MyShellApp.inf内容:

[Defines] INF_VERSION = 0x00010005 BASE_NAME = MyShellApp FILE_GUID = 12345678-1234-1234-1234-123456789abc MODULE_TYPE = UEFI_APPLICATION VERSION_STRING = 1.0 ENTRY_POINT = UefiMain [Sources] MyShellApp.c [Packages] MdePkg/MdePkg.dec ShellPkg/ShellPkg.dec [LibraryClasses] UefiApplicationEntryPoint UefiLib ShellLib

几个关键字段解释一下:MODULE_TYPE必须是UEFI_APPLICATION,ENTRY_POINT对应源码里的入口函数名,FILE_GUID随便生成一个不重复的即可。LibraryClasses里ShellLib提供了 Shell 相关的辅助函数,比如解析参数、打印格式化输出。

MyShellApp.c先写个最小可运行版本:

#include <Uefi.h> #include <Library/UefiLib.h> #include <Library/ShellLib.h> EFI_STATUS EFIAPI UefiMain ( IN EFI_HANDLE ImageHandle, IN EFI_SYSTEM_TABLE *SystemTable ) { EFI_STATUS Status; UINTN Argc; CHAR16 **Argv; Status = ShellInitialize (); if (EFI_ERROR (Status)) { Print (L"ShellInitialize failed: %r\n", Status); return Status; } Status = ShellCommandLineGetRawArray (ShellCommandLineGetParsedCommandLine (), &Argc, &Argv); if (EFI_ERROR (Status)) { Print (L"Parse args failed: %r\n", Status); return Status; } Print (L"Hello from MyShellApp, argc=%d\n", Argc); for (UINTN i = 0; i < Argc; i++) { Print (L" argv[%d] = %s\n", i, Argv[i]); } return EFI_SUCCESS; }

这段代码做了三件事:初始化 Shell 库、解析命令行参数、打印参数列表。%r是 EDK2 特有的格式化符,用来打印EFI_STATUS的可读字符串,比直接打十六进制友好得多。

3.4 把模块挂进构建系统

光有模块文件还不够,得让构建系统知道它的存在。编辑ShellPkg/ShellPkg.dsc,在[Components]段里加一行:

ShellPkg/Application/MyShellApp/MyShellApp.inf

然后回到 edk2 根目录,执行构建:

build -a X64 -t GCC5 -p ShellPkg/ShellPkg.dsc -m ShellPkg/Application/MyShellApp/MyShellApp.inf

参数含义:-a X64目标架构,-t GCC5工具链标签(GCC5 是个通用标签,不代表 GCC 版本),-p指定平台 dsc,-m只构建指定模块(加快速度)。构建成功后,产物在Build/ShellPkg/DEBUG_GCC5/X64/MyShellApp.efi。

3.5 在虚拟机里跑起来

真机测试有风险,先用 QEMU + OVMF 验证。OVMF 是开源的 UEFI 固件,QEMU 加载它之后就能进 Shell。

qemu-system-x86_64 \ -bios /usr/share/OVMF/OVMF.fd \ -drive format=raw,file=fat:rw:./esp \ -net none

fat:rw:./esp表示把本地esp目录映射成一个 FAT 格式的虚拟磁盘,你把MyShellApp.efi拷进去,进 Shell 后就能看到。进 Shell 后:

Shell> fs0: FS0:\> MyShellApp.efi hello world Hello from MyShellApp, argc=3 argv[0] = MyShellApp.efi argv[1] = hello argv[2] = world

看到这个输出,说明你的第一个 Shell App 跑通了。别小看这一步,很多人卡在"编译过了但跑不起来",问题往往出在 OVMF 版本、磁盘格式、或者 efi 文件放错分区。

4. 参数解析、文件操作与协议调用:Shell App 的三大实战能力

跑通 Hello World 只是热身,真正干活要掌握三块能力:解析命令行、读写文件、调用协议。这三块覆盖了绝大多数 Shell App 的使用场景。

4.1 命令行参数解析:别自己造轮子

Shell 传进来的参数是原始字符串,自己解析容易出错。EDK2 的ShellLib提供了一套解析机制,核心是SHELL_COMMAND_LINE结构。标准用法:

SHELL_COMMAND_LINE *CmdLine; UINTN Argc; CHAR16 **Argv; CmdLine = ShellCommandLineGetParsedCommandLine (); ShellCommandLineGetRawArray (CmdLine, &Argc, &Argv);

如果你需要支持-f file这种带标志的参数,用ShellCommandLineGetFlag和ShellCommandLineGetValue:

BOOLEAN FlagF; CHAR16 *ValueF; FlagF = ShellCommandLineGetFlag (CmdLine, L"-f"); ValueF = ShellCommandLineGetValue (CmdLine, L"-f"); if (FlagF && ValueF == NULL) { Print (L"Error: -f requires a value\n"); return EFI_INVALID_PARAMETER; }

这里有个容易忽略的点:ShellCommandLineGetValue返回的指针指向 Shell 内部缓冲区,不要 free,也不要在 Shell 生命周期外使用。我见过有人在回调里保存这个指针,结果 Shell 命令执行完就变成野指针。

4.2 文件读写:通过 Simple File System Protocol

Shell App 读写文件不能直接调fopen,得走 Simple File System Protocol。完整流程:

  1. LocateHandleBuffer找到所有带 Simple File System Protocol 的句柄。
  2. 对每个句柄OpenProtocol,拿到EFI_SIMPLE_FILE_SYSTEM_PROTOCOL。
  3. 调OpenVolume拿到根目录的EFI_FILE_PROTOCOL。
  4. 用Open打开具体文件,Read/Write读写,Close关闭。

代码骨架:

EFI_STATUS ReadFileExample ( IN CHAR16 *FilePath ) { EFI_STATUS Status; EFI_HANDLE *Handles; UINTN HandleCount; EFI_SIMPLE_FILE_SYSTEM_PROTOCOL *Fs; EFI_FILE_PROTOCOL *Root; EFI_FILE_PROTOCOL *File; UINTN BufferSize; CHAR8 Buffer[512]; Status = gBS->LocateHandleBuffer ( ByProtocol, &gEfiSimpleFileSystemProtocolGuid, NULL, &HandleCount, &Handles ); if (EFI_ERROR (Status)) { return Status; } for (UINTN i = 0; i < HandleCount; i++) { Status = gBS->OpenProtocol ( Handles[i], &gEfiSimpleFileSystemProtocolGuid, (VOID **)&Fs, gImageHandle, NULL, EFI_OPEN_PROTOCOL_GET_PROTOCOL ); if (EFI_ERROR (Status)) { continue; } Status = Fs->OpenVolume (Fs, &Root); if (EFI_ERROR (Status)) { continue; } Status = Root->Open (Root, &File, FilePath, EFI_FILE_MODE_READ, 0); if (!EFI_ERROR (Status)) { BufferSize = sizeof (Buffer); Status = File->Read (File, &BufferSize, Buffer); File->Close (File); Print (L"Read %d bytes\n", BufferSize); break; } Root->Close (Root); } FreePool (Handles); return Status; }

几个实战注意点:

  • 路径格式:UEFI 文件路径用反斜杠,比如\\EFI\\BOOT\\BOOTX64.EFI,C 字符串里要转义成L"\\EFI\\BOOT\\BOOTX64.EFI"。
  • BufferSize 是输入输出参数:传入时是缓冲区大小,返回时是实际读取字节数。如果文件比缓冲区大,一次读不完,要循环读。
  • 别忘了 Close:EFI_FILE_PROTOCOL打开后必须 Close,否则可能影响后续操作,尤其是写操作。
  • 写文件要先确保分区可写:有些固件把 ESP 挂成只读,写会返回EFI_WRITE_PROTECTED。

4.3 调用其他 Protocol:以 Block IO 为例

除了文件系统,Shell App 还经常直接操作块设备。比如你想读磁盘第一个扇区看分区表,就得用 Block IO Protocol:

EFI_BLOCK_IO_PROTOCOL *BlkIo; UINT8 *SectorBuffer; Status = gBS->OpenProtocol ( Handle, &gEfiBlockIoProtocolGuid, (VOID **)&BlkIo, gImageHandle, NULL, EFI_OPEN_PROTOCOL_GET_PROTOCOL ); if (!EFI_ERROR (Status)) { SectorBuffer = AllocatePool (BlkIo->Media->BlockSize); Status = BlkIo->ReadBlocks ( BlkIo, BlkIo->Media->MediaId, 0, BlkIo->Media->BlockSize, SectorBuffer ); // 解析 SectorBuffer 里的 MBR/GPT FreePool (SectorBuffer); }

这里MediaId很重要,它标识当前介质。如果中途换了盘(比如 U 盘拔插),MediaId会变,用旧值读会失败。健壮的做法是每次读之前检查Media->MediaPresent和MediaId。

4.4 输出与交互:Simple Text Output/Input

打印用Print就够了,但如果你要做交互式界面(比如进度条、菜单),得直接用EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL:

gST->ConOut->SetAttribute (gST->ConOut, EFI_LIGHTGREEN | EFI_BACKGROUND_BLACK); gST->ConOut->OutputString (gST->ConOut, L"Operation complete\n"); gST->ConOut->SetAttribute (gST->ConOut, EFI_WHITE | EFI_BACKGROUND_BLACK);

读键盘用EFI_SIMPLE_TEXT_INPUT_PROTOCOL的WaitForKey事件配合ReadKeyStroke。注意WaitForKey是个事件,要用gBS->WaitForEvent等待,不能死循环轮询,否则 CPU 占用高且响应迟钝。

5. 那些文档里不写的坑:调试、内存与兼容性

写 Shell App 最耗时的往往不是功能实现,而是调试和兼容性。这部分我把自己踩过的坑整理出来,能帮你省不少时间。

5.1 调试手段:没有 printf 的日子怎么过

UEFI 环境没有 gdb 直接 attach(除非用 QEMU 的 gdb stub),最实用的调试手段是串口输出。EDK2 的DebugLib支持把DEBUG宏输出重定向到串口,配置好之后 QEMU 加-serial stdio就能看到。

在 dsc 里配置:

[PcdsFixedAtBuild] gEfiMdePkgTokenSpaceGuid.PcdDebugPrintErrorLevel|0x8000004F gEfiMdePkgTokenSpaceGuid.PcdDebugPropertyMask|0x27

代码里用:

DEBUG ((DEBUG_INFO, "MyShellApp: value = %d\n", SomeValue)); DEBUG ((DEBUG_ERROR, "MyShellApp: failed, status = %r\n", Status));

DEBUG_INFO和DEBUG_ERROR是级别掩码,PcdDebugPrintErrorLevel决定哪些级别会输出。串口输出的好处是不依赖屏幕,即使图形界面挂了也能看到。

另一个手段是QEMU + gdb:

qemu-system-x86_64 -bios OVMF.fd -s -S ...

-s开 gdb server 在 1234 端口,-S启动时暂停。然后gdb连上去,可以设断点、看寄存器、单步。缺点是符号表要自己加载,EDK2 编译出的 efi 是 PE 格式,gdb 需要add-symbol-file手动指定加载地址,比较麻烦。新手建议先用串口调试。

5.2 内存管理:池内存和页内存别搞混

UEFI 有两种内存分配:AllocatePool和AllocatePages。区别是:

类型最小单位用途释放函数
Pool字节级小对象、结构体FreePool
Pages4KB 页DMA 缓冲、大块内存FreePages

新手常犯的错是用AllocatePool分配 DMA 缓冲,结果设备要求物理地址连续且对齐,Pool 内存不保证。需要 DMA 的场景(比如给网卡、存储控制器传缓冲)必须用AllocatePages,并且注意对齐要求。

另一个坑是内存泄漏。Shell App 通常短命,泄漏一点无所谓,但如果你写的是常驻服务或者循环执行的操作,泄漏会累积。养成习惯:每个AllocatePool都要有对应的FreePool,每个OpenProtocol都要有CloseProtocol。

5.3 兼容性:不同固件行为不一致

UEFI 规范虽然统一,但各家固件实现有差异。我遇到过的情况:

  • 某些固件的 Shell 不自动加载:需要手动在启动项里加EFI Shell或者把 Shell.efi 放到\EFI\BOOT\BOOTX64.EFI。
  • 文件系统大小写敏感:规范说 FAT 不区分大小写,但个别固件实现区分,路径写错就找不到文件。
  • LocateHandleBuffer返回顺序不固定:不能假设第一个就是你要的设备,必须遍历筛选。
  • ExitBootServices后行为:如果你的 App 调用了它,之后所有 Boot Services 失效,再调Print会崩。

应对策略是防御性编程:每个可能失败的调用都检查返回值,不要假设某个 Protocol 一定存在,不要假设句柄顺序,不要假设路径大小写。

5.4 一个真实的排查案例

有次我写了个 Shell App 用来批量更新 ESP 里的配置文件,在开发机上跑得好好的,到目标机器上就报EFI_ACCESS_DENIED。排查过程:

  1. 先确认文件路径没问题,用 Shell 自带的ls能看到文件。
  2. 检查分区属性,发现目标机器的 ESP 被固件标记为只读。
  3. 尝试用Block IO直接写扇区,能写,说明硬件没锁。
  4. 结论是固件在文件系统层做了写保护。

解决方案是绕过文件系统,直接用 Block IO 读写扇区,自己解析 FAT 表。这个案例说明:Shell App 的能力边界取决于固件开放了哪些 Protocol,遇到权限问题先查固件策略,别死磕代码。

6. 从能跑到好用:性能、安全与工程化建议

功能跑通之后,如果这个 Shell App 要长期用、要给别人用,就得考虑工程化的问题。这部分聊聊性能优化、安全边界和代码组织。

6.1 性能:Shell App 也能优化

Shell App 通常不追求极致性能,但有些场景下优化很有必要。比如遍历大目录、扫描整个磁盘、批量处理文件。

  • 减少 Protocol 打开次数:OpenProtocol有开销,能复用就复用。比如遍历目录时,根目录的EFI_FILE_PROTOCOL打开一次,后续都用它。
  • 批量读写:文件读写按块来,别一个字节一个字节读。EFI_FILE_PROTOCOL的Read一次读几 KB 比读几百次一字节快得多。
  • 避免不必要的字符串转换:CHAR16和CHAR8互转有开销,能直接用CHAR16就用。
  • 用DEBUG宏而不是Print做调试输出:Print走的是文本输出协议,比较慢;DEBUG在 release 构建里可以完全去掉。

6.2 安全边界:Shell App 能做什么、不该做什么

Shell App 运行在固件层,权限极高,能直接读写磁盘、内存、IO 端口。这意味着:

  • 不要做破坏性操作而不确认:格式化、写引导扇区这类操作,一定要有明确的用户确认,最好加-y之类的强制标志。
  • 不要硬编码敏感信息:Shell App 的二进制可能被反编译,密钥、密码不要写死在代码里。
  • 注意输入校验:命令行参数来自用户,路径、大小、偏移都要校验范围,防止越界。
  • 考虑 Secure Boot:如果目标机器开了 Secure Boot,未签名的 efi 无法加载。要么签名,要么在开发阶段关闭 Secure Boot。

注意:在生产环境使用 Shell App 前,务必在隔离环境充分测试,尤其是涉及写操作的功能。

6.3 代码组织:让 App 可维护

一个稍微复杂的 Shell App 代码量可能上千行,组织不好后期很难维护。我的建议:

  • 按功能分文件:参数解析、文件操作、协议调用、业务逻辑分开,别全塞一个.c。
  • 统一错误处理:定义一个宏或者辅助函数,统一打印错误并返回,避免每个调用点都写一遍if (EFI_ERROR(...))。
  • 配置与代码分离:路径、默认值、阈值这些用宏或者 PCD 定义,别散落在代码里。
  • 写注释说明"为什么":UEFI 代码很多地方不直观,注释要解释为什么这么写,而不是重复代码在做什么。

6.4 发布与分发

Shell App 编译出来就是一个.efi文件,分发很简单,但也有讲究:

  • 命名清晰:MyTool.efi比a.efi好得多,用户一眼知道干什么的。
  • 提供帮助信息:不带参数运行时打印用法,-h或--help显示详细说明。
  • 版本号:在代码里定义版本宏,-v时打印,方便排查问题。
  • 兼容性说明:在 README 里写清楚支持的架构(X64/AA64)、测试过的固件版本。

如果要在多台机器部署,可以把 efi 放到 ESP 的固定路径,然后在固件启动项里加一条,或者写个启动脚本自动执行。

7. 我个人的几条实战心得

写了这么多,最后分享几条我自己在反复折腾中总结出来的经验,都是文档里不太会写、但实际很管用的。

第一条,先跑通再优化。新手容易一上来就想写个功能完整的工具,结果卡在环境配置或者某个 Protocol 调用上,几天没进展。正确做法是先写个最小可运行版本,确认编译、加载、输出都正常,再逐步加功能。我见过太多人卡在"编译过了但 Shell 里找不到文件"这种问题上,其实就是 ESP 目录映射没搞对。

第二条,善用 Shell 自带命令做验证。写代码之前,先用 Shell 自带命令确认环境。map看设备映射,ls看文件,dh看句柄,devices看设备,memmap看内存映射。这些命令的输出能帮你快速定位问题,也能验证你的代码逻辑对不对。

第三条,串口调试是刚需。屏幕输出在图形界面下可能被覆盖,串口输出最可靠。QEMU 里加-serial stdio,真机上接串口线,养成用DEBUG宏而不是Print做调试输出的习惯。

第四条,版本控制要跟上。EDK2 的构建产物很大,但源码和配置文件要纳入版本控制。尤其是.inf、.dsc、.dec这些,改动频繁且容易忘。我习惯每次改完配置就提交一次,出问题能快速回滚。

第五条,多查规范少猜。UEFI 规范(UEFI Specification)和 PI 规范(Platform Initialization Specification)是权威参考,遇到不确定的行为先查规范。网上很多博客写的内容已经过时或者针对特定固件,不一定适用你的场景。

这个方向后续还能扩展很多,比如写一个带图形界面的 Shell App(用 Graphics Output Protocol 画图)、写一个网络工具(用 Simple Network Protocol 发包)、或者写一个固件更新工具(用 Firmware Management Protocol)。每个方向都有不少细节可以挖,等有机会再单独展开。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询