UEFI Shell脚本实战:变量、流程控制与startup.nsh自动执行
聊 UEFI Shell 的人其实不少但大部分都停在“进去以后敲两行 ls、map、bcfg”的层面。真正把 UEFI Shell 脚本语法用明白、能写出一套能自动收集诊断信息甚至修复引导入口的 nsh 脚本这种人就要少很多了。这篇文章我不会只列语法表而是把变量、流程控制、参数传递、startup.nsh 自动执行这些平时最容易被忽略的细节结合我实际折腾过的场景一次讲清楚。UEFI Shell 的定位很特殊它运行在操作系统接管硬件之前属于固件阶段的命令解释器。你在里面做的每一件事都发生在 BIOS/UEFI 固件这个层面所以它能操作 NVRAM 启动项、能直接读取映射出来的块设备、能加载 EFI 驱动这些都是 Windows/Linux 里做不到的。也正因为这样修引导、刷固件、查无显示故障、批量给老显卡刷 GOP这些脏活累活几乎都要靠它。这篇文章适合三类人一是搞固件、驱动、BIOS 相关开发的工程师二是喜欢折腾双系统引导、经常把引导修坏的人三是运维和服务器管理人员在没系统可进的时候需要靠 UEFI Shell 做诊断和恢复。1. UEFI Shell 是什么先把它和普通 Shell 对齐1.1 它到底运行在什么环境里UEFI Shell 不是 Linux 的 bash也不是 Windows 的命令提示符它更像是一个定制化的 EFI 应用程序。机器上电后CPU 进入固件初始化流程UEFI 固件会加载 DXE 驱动、连接控制台设备然后根据 NVRAM 里的启动顺序去加载启动项。如果某个启动项指向的是 Shell.efi或者你在固件设置里手动选择了 UEFI Internal Shell那么恭喜你进入了一个完全工作在 EFI 环境下的命令行。这个环境有几个很反直觉的特点。第一它没有“当前系统盘”的概念所有存储设备都以 map 的形式映射成 fs0:、blk0: 这样的名字你必须通过 map 命令看映射关系。第二路径分隔符是反斜杠 \不是 Linux 的斜杠 /这种细节刚接触时很容易栽跟头。第三它里面运行的程序是 .efi 格式不是 .exe 也不是 ELF脚本后缀则是 .nsh这和平时写的 .sh 完全是两回事。1.2 搞清楚什么时候需要写脚本有人会问既然是命令行那交互式敲命令不就行了为什么非得写脚本我的经验是一旦你面对的场景超过三台机器、或者一条诊断链路超过五个步骤纯手工敲命令的效率就低得没法看了。举个最常见的例子你要检查一批服务器的启动项配置是否正常需要依次执行 map -r、ls EFI、bcfg boot dump、memmap再把结果保存下来。手工操作的话每台机器至少五分钟还容易漏项。写成脚本后插上 U 盘开机进 Shell执行一个 collect.nsh日志自动重定向到 U 盘文件里一分钟收工。再比如很多人折腾老显卡刷 UEFI GOP、或者给特殊主板改 NVRAM 启动项这类操作本身有风险脚本化之后至少能做到可重复、可追溯比现场盲敲安全多了。2. UEFI Shell 脚本语法变量、流程控制、子脚本一次讲清2.1 变量定义与引用%var% 是核心${} 是辅助UEFI Shell 脚本里的变量核心规则就一条用 set 定义用 %变量名% 引用。注意不是 $var也不是 ${var} 为主。很多从 bash 转过来的人第一行就写echo $var然后一脸懵。set myvarhello echo %myvar%这里要特别提醒一个在 Windows cmd 里也有类似特点的细节变量名不区分大小写。我一开始习惯用大写后来发现小写也能引用但我不建议你在同一个脚本里混用大小写否则排错的时候真的会怀疑人生。UEFI Shell 也支持${var}这种写法它和%var%基本等价。我在实际使用中的感受是在字符串拼接场景下${var}可读性更好但官方示例和大量现有脚本都习惯用%var%所以我会建议你以%var%为主把${var}当作一个备用写法去了解就行。set prefixboot set filename%prefix%_backup.nsh echo %filename% set filename2${prefix}_backup2.nsh echo %filename2%说一个容易踩的坑set 等号后面的空格处理规则在不同固件版本下不完全一样。有的版本会保留空格有的版本会帮你去掉所以最稳妥的做法是——不要在等号后面加空格老老实实写set varvalue。我见过不少人写set var value结果变量值变成了空格加 value后续比较字符串时怎么都对不上。2.2 注释、输出和命令回显控制UEFI Shell 脚本的注释用 # 开头。这个和 bash 一样但和 Windows 的 rem 不一样。# 这是一行注释 echo hello输出用 echo这个没什么好说的。重点说一下命令回显的控制。默认情况下.nsh 脚本执行时会把每一条命令都打印出来输出会很乱。你可以在脚本开头加一行echo -off这里 的作用是抑制当前这一行命令本身被回显echo -off 则负责关闭后续命令的回显。这行几乎是所有实用脚本的标准开头否则你跑一个循环的时候满屏都是命令本身真正的输出反而不容易看到。如果你想重新打开回显就用echo on。另外脚本执行过程中如果需要等待用户确认可以用 pause这个和 Windows 的 pause 一样会提示按任意键继续。2.3 流程控制if、for、goto、shift 一个都不能少UEFI Shell 的语法和 bash 差别很大反而更像 DOS 批处理和早期 BASIC 的混合体。if 的写法是 if ... then ... endif注意必须显式写 then 和 endif不是 bash 那种用 fi 收尾。set valabc if %val% abc then echo match else echo not match endif字符串比较时我强烈建议把变量用双引号包起来也就是写成if %val% abc then。如果不加引号当变量为空时这句会变成if abc then语法直接报错。这算是脚本语言里的经典坑了。还要注意一点UEFI Shell 的 if 只支持字符串比较不支持数字大小比较。你想判断一个数字是否大于另一个不能直接写if %n% GE 3得想办法转换成字符串处理或者用 for 循环遍历、用条件判断多个可能值。这确实很笨但 EFI Shell 环境讲究的就是轻量和可控不能指望它像 bash 那么灵活。for 循环支持遍历一个值列表也支持遍历通配符匹配到的文件或目录。语法如下for %i in (fs0 fs1 fs2) echo drive %i ls %i:\ endfor注意这里循环变量是%i不是 bash 的$i。我最初在这个地方反复翻车总是不自觉地写成for $i in ...。循环体内引用变量时也要用%i保持始终一致。如果你想在循环里拿当前目录下所有 .efi 文件可以这样for %f in (*.efi) echo %f endforgoto 语句用于跳转到标签标签以冒号开头。我一般用它来模拟 while 循环因为 UEFI Shell 本身没有 while所以典型套路是“标签 判断 goto”set cnt0 :again if %cnt% 3 then goto done endif echo count is %cnt% set cnt%cnt%1 goto again :done echo all done说句实话上面这个“累加字符串”的写法并不优雅但确实能跑。如果涉及到参数列表的处理shift 命令就非常有用了。shift 会把脚本参数整体左移一位原来的 %1 变成 %0%2 变成 %1以此类推。配合 goto 可以写出一个参数遍历循环:param_loop if %1 then goto param_done endif echo param: %1 shift goto param_loop :param_done echo params finished2.4 脚本参数、调用子脚本和错误码.nsh 脚本支持参数%0是脚本自身名%1到%9是传入参数。传参时用空格分隔参数本身如果要包含空格要用双引号包起来。:: mytask.nsh :: 用法: mytask.nsh target_dir debug_flag echo target is %1 echo debug flag is %2在脚本里调用另一个 .nsh直接写脚本名即可比如child.nsh arg1 arg2。子脚本执行结束后会返回到父脚本继续执行。如果你希望子脚本里设置的变量在父脚本里继续生效实测直接以脚本名调用是可以的变量会保留在同一个 shell 会话中。关于错误码UEFI Shell 提供了一个内置环境变量%lasterror%上一条命令执行完后的错误状态会存在里面。这是个宝藏变量排错全靠它。比如你在脚本里跑了一个 .efi 程序发现没执行成功可以先 echo 一下%lasterror%看看返回值。如果脚本中途出错你也可以用 exit 命令终止脚本执行并指定一个退出码。2.5 常用内置命令和外部工具速查UEFI Shell 的常用命令数量不算多但每一个都很实用。我整理一份自己常用的清单命令作用典型用法map / map -r显示/重建设备映射map -r 后执行 map 查看 fs0: 对应关系ls、cd、mkdir、rm文件目录操作ls fs0:\EFIedit文本编辑edit script.nshecho输出文字/控制回显echo hello / echo -offset查看/设置环境变量set varvalueif / for / goto / shift流程控制见上文示例pause等待按键pausereset重启系统resetbcfg管理启动项 NVRAMbcfg boot dump、bcfg boot rm 0dmpstore查看 UEFI 变量dmpstoredrivers、devices、pci、memmap查看硬件/驱动/内存信息pciconnect -r重新连接控制器和驱动connect -rhelp查看命令帮助help -b 分页显示load加载 EFI 驱动load fs0:\driver.efi这里特别说下 bcfg。它操作的是 NVRAM 里的 BootOrder 和 BootXXXX 变量是修复启动项最核心的命令。想查看当前启动项执行bcfg boot dump想删除第 0 个启动项bcfg boot rm 0想添加一个新的启动入口指向某个 EFI 应用bcfg boot add 0 fs0:\EFI\BOOT\BOOTX64.EFI My Boot Entry我曾经遇到过一台机器Windows 引导因为分区表问题起不来进 UEFI Shell 后用 bcfg boot dump 发现启动项指向的路径根本不对改掉之后马上就能进系统。这种问题你用 Windows 安装盘修半天都不一定能找对思路但在 UEFI Shell 里两三分钟就定位了。3. 实操从零搭一个 UEFI Shell 脚本验证环境3.1 用 QEMU OVMF 搭一个免费测试环境很多人以为学 UEFI Shell 脚本必须得有真机其实完全可以在开发机上用 QEMU 模拟。只要你有一台 Linux 开发机按下面几步走就能在虚拟机里跑 UEFI Shell写脚本、验证语法比真机还方便不用担心把固件搞坏。以 Ubuntu/Debian 为例先装 QEMU 和 OVMF 固件sudo apt install qemu-system-x86 ovmf mtools dosfstoolsOVMF 是 UEFI 固件的开源实现qemu 用它作为虚拟机的 BIOS。然后准备一个 FAT 格式的磁盘镜像里面放好 Shell.efi 和你的脚本文件dd if/dev/zero offat.img bs1M count64 mkfs.vfat fat.img mcopy -i fat.img Shell.efi ::EFI mkdir -p esp/EFI mcopy -i fat.img -s esp ::/ mcopy -i fat.img mytest.nsh ::mytest.nsh上面命令的意思是把 Shell.efi 放到镜像的 EFI 目录下再把写好的脚本拷进去。启动时让 OVMF 直接加载 Shell.efiqemu-system-x86_64 -machine q35 -bios /usr/share/ovmf/OVMF.fd -drive filefat.img,formatraw -net none启动后如果固件没有自动进入 Shell你可以在 UEFI 设置界面里选 UEFI Shell 启动项。OVMF 默认通常会把 Shell 显示在启动菜单里没有的话也可以把 Shell.efi 重命名为 EFI/BOOT/BOOTX64.EFI这样固件会优先去加载它。进入 Shell 后运行map -r重建映射然后ls fs0:就能看到你的 fat.img 里的文件直接执行fs0:\mytest.nsh就能测试脚本。这套环境跑起来之后基本等于你拥有了一台随时可以重启进 Shell 的测试机写脚本的效率会高非常多。3.2 实战脚本一自动收集系统诊断信息我先分享一个诊断脚本这个脚本在服务器和嵌入式设备排障时非常有用能一次性收集设备映射、驱动、PCI 设备、内存映射和启动项信息并把所有结果重定向到 U 盘文件里。脚本开头加echo -off关闭回显用 echo 打印执行阶段方便你观察进度。echo -off set logfilediag.txt echo fs0:\%logfile% echo UEFI Shell Diagnostic fs0:\%logfile% echo fs0:\%logfile% echo. fs0:\%logfile% echo [Shell Version] fs0:\%logfile% ver fs0:\%logfile% echo. fs0:\%logfile% echo [Device Mapping] fs0:\%logfile% map fs0:\%logfile% echo. fs0:\%logfile% echo [PCI Devices] fs0:\%logfile% pci fs0:\%logfile% echo. fs0:\%logfile% echo [Memory Map] fs0:\%logfile% memmap fs0:\%logfile% echo. fs0:\%logfile% echo [Drivers] fs0:\%logfile% drivers fs0:\%logfile% echo. fs0:\%logfile% echo [Boot Entries] fs0:\%logfile% bcfg boot dump fs0:\%logfile% echo. fs0:\%logfile% echo Diagnostic Finished. fs0:\%logfile% pause注意上面我用了双大于号做追加重定向UEFI Shell 支持覆盖写入和追加写入。第一次写日志时用清空旧文件后续都用。我用显式的echo.来输出空行目的是让日志分段更清晰。跑完后U 盘里会生成一个 diag.txt拿回电脑上打开硬件状态一目了然。这里有一个小细节重定向路径我写的是fs0:\%logfile%前提是 U 盘被映射成了 fs0:。实际环境中 U 盘不一定总是 fs0:可能是 fs1: 甚至 fs2:所以脚本执行前最好先 map 确认一下。这也是排障环境最常见的不确定因素之一。3.3 实战脚本二自动扫描并列出 EFI 引导文件第二个脚本用来扫描各个文件系统映射根目录下是否存在常见的引导文件。这个思路非常适合处理“这台机器到底引导文件在哪个盘里”的问题。因为 UEFI Shell 里你看到的 fs 映射顺序和系统内的盘符顺序不一定一致经常需要逐个 fs 去找引导文件。echo -off for %i in (fs0 fs1 fs2 fs3) if exist %i:\EFI\BOOT\BOOTX64.EFI then echo [%i] Found BOOTX64.EFI endif if exist %i:\EFI\Microsoft\Boot\bootmgfw.efi then echo [%i] Found Windows Boot Manager endif if exist %i:\EFI\ubuntu\shimx64.efi then echo [%i] Found Ubuntu Shim endif endfor pauseif exist 是 UEFI Shell 支持的文件存在性判断结合 for 循环能把所有文件系统都扫一遍。这个脚本虽然简单但在双系统引导丢失、磁盘分区表不对导致系统无法启动的场景里能帮你迅速定位引导文件到底在哪个分区。找到位置之后用 bcfg boot add 把正确的启动项加回去引导就恢复了。3.4 startup.nsh 自动执行逻辑UEFI Shell 有一个特殊约定进入 Shell 后会自动寻找当前目录下的 startup.nsh 并执行。这个自动执行能力是脚本化运维的关键。只要把诊断脚本命名为 startup.nsh 放到 U 盘根目录插入机器后自动进 Shell它会自动运行。更妙的是startup.nsh 不一定要放在 U 盘根目录Shell 会按照一定顺序查找 startup.nsh包括当前映射的根目录、shell 所在目录等。具体顺序有时因固件而异但最保险的做法是把 startup.nsh 放在识别到的 U 盘根目录同时把该 U 盘作为 Shell 的启动介质。我实际用过的一个套路是这样的U 盘里放一份完整的诊断脚本命名为 startup.nsh然后在脚本最后加一句reset这样机器启动后自动进 Shell、自动诊断、自动把日志写到 U 盘、然后自动重启。运维人员要做的只是第二天读取 U 盘里的日志文件整个批量排障流程完全无人值守。对几十台机器来说省下来的时间非常可观。4. 常见报错与排查这些坑我基本都踩过4.1 cannot find required map name到底哪里出问题了这个是新手进入 UEFI Shell 后最常撞见的报错几乎每天都有搜这个词的人。报错的含义很简单你访问了一个当前环境里不存在的设备映射。比如你直接敲fs0:但当前机器根本没有映射出 fs0:Shell 就会抛出类似 cannot find required map name 的错误。处理方式也很固定先执行map -r重新扫描所有控制器和块设备让 UEFI 固件重新枚举一遍设备再执行map查看当前实际存在的映射名。很多时候U 盘插上去之后没有及时被识别map -r 扫一次就出来了。还有一种情况是块设备存在但文件系统没有正确加载你可以在 Firmware 设置界面检查磁盘模式比如 SATA 控制器是否工作在 AHCI 模式下这也会直接影响 Shell 能否识别到文件系统。如果map -r之后仍然找不到映射还可以试connect -r它的作用是重新连接所有驱动控制器能驱动一些初始化失败的外设。这两个命令在排查“设备明明插着但 Shell 里看不到”的问题时序列是固定搭配。4.2 脚本文件存在但执行时提示找不到或没反应在 UEFI Shell 下执行.nsh脚本最直接的方式是输入完整路径比如fs0:\mydir\test.nsh。如果你先cd到了对应目录也可以直接输入脚本名。但有一点容易误解.nsh 文件本身不需要可执行权限它靠 Shell 解释器逐行读取所以你不用像 Linux 那样给 chmod x。我遇到过的“脚本没反应”案例绝大多数原因是路径写错。UEFI Shell 里路径大小写不敏感这点比 Linux 友好。真正的问题是反斜杠使用不一致。例如你想进入 fs0 下的 EFI 目录要写fs0:\EFI而不是fs0:/EFI。虽然 Shell 的命令解析在某些情况下能容忍斜杠但为了让脚本在更多固件实现上稳定运行统一使用反斜杠是最稳的。另外如果你把脚本放在了一个通过 USB Hub 连接的设备上偶发性的设备枚举失败也会导致脚本无法访问。我碰到过一次换了一个 USB 口插 U 盘就正常了这种事看着离奇但确实存在。4.3 脚本跑起来后变量打印为空变量为空最常见的两个原因一是变量真的没有赋值成功二是变量名写错。前面说过 set 等号不要加空格这里再补充一个相对隐蔽的问题环境变量的作用域。如果你在交互式命令行里 set 了一个变量然后执行脚本脚本内部读不到这个变量是完全正常的——除非你是用.命令让脚本与当前会话共享变量环境。反过来也一样脚本里 set 的变量在脚本用直接命令方式调用结束后某些固件实现下可能会保留某些不会。最好的做法是不要依赖交互环境的变量传递。把参数通过%1、%2显式传给脚本脚本内部需要初始化变量就自己在开头 set。4.4 换行符、编码和 BOM 问题这个坑比较隐蔽但危害很大。UEFI Shell 对脚本文件的换行符和编码容忍度并不高我实测下来最佳组合是UTF-8 无 BOM 换行符使用 LF 或 CRLF 都行但建议统一用 LF 保证跨平台编辑不出问题。如果你在 Windows 上用记事本编辑脚本保存出来的 UTF-8 文件可能带 BOM 头UEFI Shell 解析第一行时就会看到一堆不可见字符于是第一行命令直接不执行或者报语法错误。这种情况我用编辑器把文件转成 UTF-8 无 BOM 后问题就消失了。在 Linux 下用 vim 编辑比较安全但也要注意确认 :set fileformat 的值。简单说编辑 .nsh 别用记事本也别用带 BOM 的编辑器直接上 VS Code、vim、Notepad 这类能控制编码的工具。4.5 分号、引号和特殊字符的处理UEFI Shell 的语法里;在某些情况下被当作命令分隔符解释所以千万不要在脚本的 echo 字符串里随意用分号否则可能被拆成多条命令。如果确实要输出特殊字符用双引号把整个字符串包起来可以减少被解析的风险。引号本身也要注意Shell 里的引号并不像 bash 那样作为强引用把内容原样包装实际行为更加粗糙。比如你要拼接带空格的路径双引号能帮你保持成一个整体但内部如果又套了引号行为就容易变得不可预测。我的原则是能用变量拼接就不用复杂引号能避免特殊字符就尽量避免。4.6 UEFI Shell 和 bash/cmd 语法对照速查为了让从 Linux/Windows 过来的朋友快速切换思路我把常见写法放在一张表里方便对比功能bashWindows cmdUEFI Shell变量引用$var%var%%var%设置变量varvalueset varvalueset varvalue条件结束fi无/括号endif循环结束done无/括号endfor比较字符串[ $a b ]if abif %a% b then注释#rem#路径分隔符/\\调用子脚本./sub.shcall sub.batsub.nsh记住一个核心判断UEFI Shell 既不像 bash 那样灵活也没有 cmd 那么庞大的内置工具集它的设计目标就是在固件阶段提供一个“够用且可控”的脚本环境。所以别指望写复杂算法也别对语法糖抱期待能用 it/for/goto 组合出清晰的流程就是好脚本。5. 写在最后最能提高效率的一个组合套路如果只给一个建议我会说把 startup.nsh 自动执行、日志重定向、reset 自动重启这三个能力组合起来是你用 UEFI Shell 脚本能获得的最大红利。不管是批量诊断、远程协助排障还是给一批机器统一调整启动项只要把脚本放到 U 盘里插上开机它就能自己跑完整个流程并留下日志不需要人工守着屏幕一条条敲命令。我最初在真机上调试这些脚本时也踩过不少坑最惨的一次是在一台没有显示器、只能靠串口看输出的设备上跑诊断脚本日志因为重定向路径写错直接丢了一整个上午的数据。后来我养成了一个习惯脚本里所有重定向路径都用变量统一管理开头先 echo 一条确认信息并写入日志这样脚本一旦跑起来我立刻知道它有没有正确访问到目标文件系统。这个习惯帮我避免了不少重复劳动。UEFI Shell 脚本的学习曲线不算陡真正费时间的其实是各种固件实现之间的细微差异。同一份脚本在 OVMF 下跑得很顺换到真实主板上可能就遇到 map 顺序变化、命令版本不一致的问题。所以我的建议是先在 QEMU 里把逻辑调通再到真机上小范围验证最后再批量运行。这样既安全又能让你对这套环境形成一种“可控感”。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →