最近我在 EDK II 环境下折腾 UEFI 开发,发现原生的 UEFI Shell 采用纯命令行交互,在进行频繁的文件复制、移动和目录切换时效率很低。为了改善体验,我决定手搓一个类似于 Midnight Commander (mc) 的**双面板、纯文本可视化文件管理器 (Visual File Manager)**。
在开发过程中,我遇到了不少 UEFI 底层的坑和“玄学” Bug。这篇文章将记录整个开发过程以及遇到各类问题的解决思路。
1. 初步实现与双面板结构
整个 APP 的基础是利用了 EDK II 的 ShellLib.h 和系统协议(如 EFI_SIMPLE_FILE_SYSTEM_PROTOCOL)。
核心的数据结构分配了两个 FILE_PANE 实例,分别对应左右面板;利用 UEFI Console 的 gST->ConOut->SetCursorPosition() 把屏幕劈成了左右两半(通常是 80x25 或更大),实现了简单的文件结构展示并用颜色进行了区分(青色代表目录,绿色代表纯 .efi 可执行文件)。
然而随着功能的堆叠,坑开始一个个出现。
2. 踩坑与修复记录
问题一:上下选择时屏幕闪烁极其严重
现象: 每次按按键(哪怕只是上下挪动光标),整个页面都要“黑闪”一下才能刷新完毕,感觉非常不流畅。
原因分析:
为了保持界面干净,在绘制主循环 DrawUI() 中,我每次都调用了 gST->ConOut->ClearScreen() 来清空屏幕。ClearScreen 实际上会先将屏幕直接涂黑,然后再逐个字符覆盖。这样高频次清空重绘就会造成明显的黑屏闪烁。
解决方案:
移除主循环里的 ClearScreen()。既然每一行都有固定宽度,我们在输出文本时,根据 PaneWidth 动态计算还需要多少空格,调用自定义的 PrintCharTimes(L' ', PadLen) 补满剩余空间。
依靠“覆盖替换”而非“先清空后画”,即可做到 100% 丝滑无闪烁更新。
问题二:顶部标题栏神秘消失
现象: 在添加了最后一行底部的“快捷键提示栏”后,屏幕最顶部的那行 [FileManager v1.0] 莫名其妙不见了。
原因分析:
在我的终端输出逻辑中,底部文本填充了大量的空格以试图对齐最右侧。当向 UEFI 控制台的最后一个字符(坐标:X=79, Y=24)打印内容时,终端的光标会自动向前推进一步,导致屏幕被整行向上滚动(Auto-Scroll)!这样位于 Y=0 的顶部栏就被强行顶出了屏幕。
解决方案:
在绘制最底下一行时,即使需要填充空格,也刻意让长度等于 gColumns - 1,将最后一个字符彻底悬空不操作,避免触发底层的屏幕滚屏。
问题三:UI 交互太反人类(无光标、删除死板)
现象:
- 做
F5复制/F6移动敲目标路径时,屏幕上没有闪烁的光标,没法用左右方向键回退修改。 - 目标路径如果是个纯目录名(如
fs1:\test),拷贝直接失败,非得让人手工把文件名也补齐。 F8删除时必须要键盘纯手打输入yes才能删。
解决方案:
为了优化体验,我彻底重写了一套轻量级的窗口组件:
InputBox(光标和修改支持): 使用gST->ConOut->EnableCursor(..., TRUE)开启硬件光标。捕捉SCAN_LEFT / SCAN_RIGHT / SCAN_DELETE及0x08(退格),利用字符串缓冲区的整体左移/右移自行实现了一个单行文本编辑器。- 智能化路径拼接: 当你在目标打下
fs1:\回车时,我的底层AppendFileNameIfDirectory函数通过尝试ShellOpenFileByName和ShellFindFirstFile先判定目标是不是目录。如果是目录或者是盘符标识,代码自动在内存中把\原始文件名拼接上去。 ConfirmBox: 抛弃键盘手敲,只捕捉[Enter]和[Esc]。
问题四:从根目录后退找不到文件系统,并偶发崩溃
现象: 在 fs0:\ 下,如果按 .. 想跳出磁盘,这在传统的 UEFI Shell 环境下是无效的。且如果我们在某个子目录运行该 App(比如 fs0:\test\visual.efi ),有时会根本不显示界面,直接报错 Unsupported。
原因分析:
- Trailing Slash (尾随斜杠) 异常: 在 UEFI FAT 底层驱动中,如果直接以读取模式去 open 一个没有随尾带有
\的纯目录路径(比如fs0:\test),驱动可能会把它强行认定成“没有拓展名的 File”,从而在接下来的ShellFindFirstFile检索它内部节点时,抛出EFI_UNSUPPORTED,直接引发 App 级别的 Abort 崩溃。 - 跨盘符迷失: 根路径没有父节点,用户就变成了“被锁在当前盘符的囚徒”。
终极解决方案与实现:
我为此创造了一个虚拟的超级挂载点—— Drives:
- 处理 Slash: 在
ReadDirectory最开始,无论是fs0:还是复杂的路径,全部做安全化处理:如果没有,强行附加\再去请求底层驱动打开,从源头断绝了因缺少反斜杠被驱动拒绝的问题。 Drives:虚拟挂载点:当识别到用户在根目录的[..]上按 Enter 时,将路径主动劫持为Drives:。随后调用LocateHandleBuffer查找系统内所有的gEfiSimpleFileSystemProtocolGuid句柄,再顺藤摸瓜通过gEfiShellProtocol->GetMapFromDevicePath解析出类似于FS0,FS1,BLK0的真正映射名格式化成了虚拟的内部目录。
这样大家就能像在用真实的 OS 一样,在最外层自如地选择所有的挂载盘了!
3. 演示
主界面

移动/重命名

复制

删除

4. 结语
在现代 OS 里的“基操”,一旦深入到 BIOS 层级的 UEFI 去完全手推,就会涉及到控制台重绘、按键中断监听、底层句柄状态转换等各种“返璞归真”的操作。虽然踩了不少坑,但也借此加深了对 EDK II 以及 UEFI 文件系统通信协议的理解。如果你也需要一款 UEFI 下高效的文件管理工具,这种基于 ShellLib 开发的双面视图绝对是最佳的选择, 后面继续在此基础上继续添加可编辑文本,执行efi等功能。