news 2026/10/1 2:15:15

UEFI Shell脚本语法详解:从入门到固件调试实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
UEFI Shell脚本语法详解:从入门到固件调试实战

1. UEFI Shell到底是什么,为什么调试固件绕不开它

先讲个亲身经历。前两年帮朋友修一台进不去系统的老机器,开机黑屏,连BIOS Setup都进不去,风扇转、电源灯亮,就是没画面。折腾半天,最后是靠着UEFI Shell进去,用几条命令定位到启动项损坏,直接从Shell里把默认启动项拉回来,机器才救活。那时候我就觉得,UEFI Shell这玩意儿看着不起眼,真到关键时刻比什么PE盘、LiveCD都管用。

UEFI Shell是运行在UEFI固件环境下的一个命令行解释器,你可以把它理解成BIOS年代的DOS——系统没起来之前,它先给你一个能操作硬件、读写文件、执行脚本的入口。它不依赖硬盘上的操作系统,只要板子上的UEFI固件能跑起来,Shell就能用。

很多人分不清UEFI Shell和BIOS Setup的关系。简单说,BIOS Setup是图形化或半图形化的配置界面,能改的东西被厂商锁死了,无非是启动顺序、电源管理、虚拟化开关这些。UEFI Shell则是另一层东西,它面向的是"真正能操作固件和硬件的人"——你可以直接访问EFI系统分区里的文件,可以加载驱动,可以读写内存和IO端口,可以用脚本批量执行命令,甚至可以操作ACPI表、管理启动项、刷写固件。如果你在做固件开发、主板验证、系统集成,或者单纯喜欢折腾底层,这玩意儿是绕不开的。

这篇文章我不会去贴那份几百页的UEFI Shell规范文档,而是按我自己实际使用的路径,把脚本语法这块彻底讲透:变量、参数、分支循环、文件操作、外部命令加载,再到真实场景下的完整脚本拆解。最后一部分我会把这几年前前后后踩过的大坑一并列出来,这些才是文档里查不到的东西。

如果你是完全没接触过UEFI Shell的新手,建议你把前两节看完再上手;如果你已经写过几个.nsh脚本,可以直接跳到第4和第6节,里面有不少细节值得对照。

2. 搭建实操环境,Shell不是所有机器都自带

2.1 主流板卡上怎么进UEFI Shell

先解决一个实际问题:Shell从哪来?

大多数商业主板默认不预装UEFI Shell,你得自己想办法。常见的入口有几个:

  • 部分工作站和服务器主板(比如某些品牌的商务机型)在BIOS Setup里提供一个"Launch UEFI Shell"的选项,打开后重启就能直接进。
  • 没有内置Shell的机器,可以把Shell.efi放到FAT32格式的U盘或EFI系统分区里,然后在启动菜单里选"UEFI: U盘名"进入。前提是U盘根目录下有EFI/BOOT/BOOTX64.EFI(64位x86机器),或者你把Shell.efi命名为这个路径下的启动文件。
  • 在EDK2开发环境里,直接用QEMU加OVMF固件跑一个虚拟机,启动时也能进Shell,这个方法对学习语法来说反而是最省事的。

我自己平时调试用的组合是:一台装了Linux的笔记本跑QEMU,OVMF固件里直接集成Shell,配合虚拟的FAT32磁盘镜像,脚本写完不用烧到真实板卡就能验证语法。这个方法强烈推荐——因为真机上反复重启测试,时间成本太高了,而且有些机器进Shell的快捷键和菜单路径差异很大,会打断你调试的思路。

2.2 版本差异:Shell 2.0与细节变化

UEFI Shell有两种主版本,Shell 1.0和Shell 2.0。2.0是EDK2默认的版本,功能强很多,比如支持for循环、支持-b分页参数、支持环境变量的字符串操作等。1.0基本只在内嵌的老固件里能碰到。

到了2.0之后,不同固件版本的具体实现其实也有差异。有的厂商自己编译的Shell会砍掉一些命令,有的则集成了一堆EFI Shell下的DXE驱动。所以在某个机器上能用的脚本,换台机器可能就提示命令找不到。这种事儿我遇到过不止一次,后面第6节会专门讲怎么排查。现阶段你只需要记住一个原则:写脚本时尽量用最基础的内置命令,少依赖厂商自定义的扩展命令。

2.3 文件系统支持与Shell下的盘符逻辑

进到Shell之后,第一件事通常是敲map命令看当前有哪些可用映射。UEFI Shell下没有C盘D盘的概念,它用FS0:、FS1:这样的格式来表示文件系统,BLK0:、BLK1:表示块设备。比如:

Shell> map

输出会列出当前能访问的所有设备映射。你输入FS0:回车就能切换到第一个文件系统,然后ls看目录结构。这里有个容易懵的地方:一个U盘分一个区是FS0:,如果某个磁盘上有多个FAT分区,可能会映射成FS0:、FS1:,而且映射顺序不一定等于你的物理顺序,和UEFI固件对设备的枚举顺序有关。

还有一点,Shell只认FAT12/16/32格式的文件系统,NTFS、ext4这些在纯Shell环境下是不认的。你要想从U盘加载脚本,U盘必须是FAT32格式,别踩这个坑。

3. UEFI Shell脚本的"母语":内置命令与帮助体系

3.1 先弄清楚Shell的命令从哪里来

在动手写脚本之前,得先搞明白Shell命令的分类。UEFI Shell下的命令大致分三类:

  • Shell内置命令:Shell自己实现的,比如cd、ls、cp、mv、rm、set、alias、for、if等。这些不依赖外部文件,永远可用。
  • EFI应用程序:编译出来的.efi文件,放在某一路径下,通过Shell去加载执行。最典型的就是Shell.efi本身,以及你写的一些.efi小工具。
  • Shell命令脚本(.nsh):本次重点。本质是一个包含若干Shell命令的文本文件,Shell逐行解释执行。

你在Shell里敲一条命令,Shell会先查内置命令表,然后查当前目录下有没有对应的.efi或.nsh文件,再根据path环境变量的设置去一系列目录里查找。这个查找顺序很重要,后面会讲一个因此引发的诡异Bug。

3.2 常用内置命令的功能分组

我按使用场景把常用命令分了几组,这比死记命令列表高效得多:

文件操作类:ls、cd、cp、mv、rm、mkdir、touch、type(查看文本文件内容)、edit(文本编辑器)、hexedit(十六进制编辑器)。其中edit写短脚本非常实用,不用来回在Shell和操作系统之间切换。

系统信息类:mem(内存信息)、devices(设备列表)、drivers(驱动列表)、bcfg(启动项管理)、smbiosview(SMBIOS信息)、acpi(ACPI表信息)、ver(版本信息)。

内存与端口操作类:mm(内存读写)、io(端口读写)、dmem(dump内存)、dmesg(类似Linux的dmesg)。这些属于固件调试的利器,也是Shell最危险的一面——改错一个内存地址系统直接就挂。

脚本控制类:set、if、for、while、goto、shift、pause、stall(延时)、exit(退出脚本或Shell)、reset(重启)。这些是脚本语法的核心。

Shell环境类:alias(命令别名)、path(可执行文件搜索路径)、map(设备映射)、echo、cls。其中map命令本身还能用来修改映射关系。

3.3 用好-help参数,比查文档快十倍

UEFI Shell每个命令都支持-?(也就是help的缩写)参数。比如:

Shell> ls -?

就会输出ls的完整用法、参数说明、示例。这个参数信息是命令自己内置的,不需要联网查文档。我写脚本的日常流程基本上是:拿不准某个参数就先命令 -?看一遍,然后开一个Shell窗口在机器上验证,另一个窗口用edit写脚本。

有一个小细节:-?和-h不一定等价。部分命令只支持-?,你要敲-h反而会被当成非法参数。这个和Linux下的习惯不一样,很多人上来就敲-h,然后提示参数错误,还以为命令坏了。

4. 脚本语法核心:变量、参数、流程控制写法

到这里开始进入正题。UEFI Shell脚本文件是.nsh后缀的纯文本文件,一行一条命令,支持#注释。我把语法拆成几个相对独立的部分来讲。

4.1 变量与环境变量:%符号的用法

UEFI Shell里所有变量都是环境变量,没有单独的局部变量概念。你用它,它就是一个全局的环境变量;你删它,用set命令。

设置和引用的语法:

set MYVAR hello echo %MYVAR%

第一行把环境变量MYVAR设成hello,第二行输出hello。注意引用变量时必须用%包裹,这和Windows批处理里的%VAR%写法一样。

删除变量用:

set MYVAR

后面不跟值,就是删除。或者set -d MYVAR。

看当前所有环境变量,直接敲set不带参数。

有一个必须记住的坑:**环境变量的值里如果包含特殊字符,解析顺序会导致意想不到的结果。**Shell对%的解析是在执行命令之前做替换,所以如果你在脚本里想输出一个字面上的%,得写两个%转义——echo %%输出一个%。这一点和批处理脚本一模一样。

Shell自带一些内置变量,经常用到的有:

  • %path%:可执行文件搜索路径,多个目录用分号分隔。
  • %cwd%:当前工作目录。
  • %lasterror%:上一条命令的返回值/错误码。
  • %startup%:是否以startup.nsh方式启动,脚本里可以用它判断当前是不是开机自启。

我实际写脚本用到%lasterror%的频率非常高。因为脚本本身没有try-catch机制,判断命令是否执行成功,唯一的办法就是检查错误码。

4.2 脚本参数与shift处理

执行一个.nsh脚本时可以带参数,比如:

Shell> test.nsh arg1 arg2

在脚本内部,%0拿到的是脚本名自身,%1到%9依次是参数1到参数9,%*是全部参数(从参数1开始)。

有一个比较坑的地方:UEFI Shell虽然支持shift命令,但它的shift不太好用——它是把参数整体左移一位,%1变成原来的%2,以此类推。如果你拿%1做循环取出所有参数,写到第三四个参数之后容易乱套。我自己写脚本处理不定长参数时,通常直接绕开%1~%9,改用%*配合字符串截取操作。

字符串操作指令在UEFI Shell里是%VAR:~start,len%这种风格,比如:

set fullarg %* set firsttwo %fullarg:~0,2%

这可以截取参数的前两个字符。语法风格接近Windows批处理的%VAR:~m,n%。但因为Shell对%的解析比较严格,这种写法在嵌套环境里偶尔会出问题。所以遇到特别复杂的字符串处理,我更推荐在脚本里调用echo加重定向生成一个新的临时脚本,再执行这个临时脚本——虽然笨,但可控。

4.3 分支:if/then/else/endif

UEFI Shell的if语法支持三种基础形式:

if 条件 then 命令 endif
if 条件 then 命令 else 命令 endif
if 条件 then 命令 elseif 条件 then 命令 endif

条件判定的写法分两种。一种是判断环境变量是否存在或等于某个值:

if "%MYVAR%" == "hello" then echo match endif

另一种是文件存在性判断:

if exist FS0:\EFI\BOOT\BOOTX64.EFI then echo found bootloader endif

注意==两边我习惯都加引号,这是从批处理时代养成的习惯——如果%MYVAR%是空值,不加引号会导致整个条件表达式语法不完整,脚本会报错。加了引号之后,空值就变成了一个空字符串参与比较,稳妥很多。

还有三个逻辑运算符可以用:AND、OR、NOT。比如:

if exist FS0:\flag.txt AND "%mode%" == "test" then echo enter test mode endif

4.4 循环:for和while

for循环的语法是:

for %i in (val1 val2 val3) echo %i endfor

注意循环变量用单个%,不是双%:

for %i in (1 2 3 4) echo %i endfor

如果想遍历文件列表,可以这么写:

for %i in FS0:\*.nsh echo %i endfor

Shell会自己展开通配符,把匹配到的文件路径依次赋给%i。

while循环的语法也类似:

while 条件 命令 endwhile

这里有一个重要限制:UEFI Shell的循环没有计数器自动递增的能力,如果你要用while计数,必须自己修改变量。

set counter 0 while %counter% < 5 echo count is %counter% set counter %counter%+1 endwhile

等一下,UEFI Shell的set命令不支持算术运算。set counter %counter%+1的结果是字符串0+1,不是数字1。这是Shell脚本最让人抓狂的地方——它没有内置的整数运算能力。

那怎么实现计数?两个办法。一是借助for %i in (1 2 3 4 5)这种显式列表实现固定次数循环;二是用外部工具,比如edk2自带的parse命令或者其他小工具去处理数值。或者退一步,循环次数已知就用for,循环条件是基于环境变量且不需要自增的情况才用while。这个思路能让你的脚本少走很多弯路。

4.5 传说中的goto和标签

UEFI Shell支持goto跳转和标签:

goto :start ... :start echo start here

标签是冒号加名字,goto语句跳转到指定标签。注意:goto不能跳出当前脚本文件的边界——你没法在一个.nsh里goto到另一个.nsh的标签。这和批处理一样。

但goto在Shell里还有一个比较危险的行为:如果标签不存在,Shell不会报错退出,而是直接跳到脚本末尾,然后返回一个%lasterror%错误码。如果脚本是开机自启动的startup.nsh,这种静默失败很容易被忽略,后面真正要用的操作没执行,你还不明所以。

4.6 把多个命令串起来:重定向和管道

UEFI Shell支持重定向,>是覆盖写,>>是追加写。比如把命令输出保存到文件:

ls > FS0:\filelist.txt smbiosview -b > FS0:\smbios.txt

管道|在UEFI Shell里的支持比我想象中弱一些。实测下来,管道在某些版本的Shell里能工作,在另一些版本里会直接报语法错误。比如:

ls | grep .efi

这个命令在部分EDK2版本里能用(Shell内置了grep的简易实现),但换到另一块主板上可能就失败了。我的经验是:正式脚本里尽量少用管道,需要过滤输出就先把结果重定向到临时文件,再用search命令或type加parse去处理。宁可多写两行,也不要在管道这个特性上赌固件实现。

5. 一个能直接上机跑的诊断备份脚本,逐步拆解

语法讲再多,不如一个完整的例子。下面这个脚本是我在某次批量主板测试中实际用的一个简化版本,功能包括:检查设备映射、备份当前BIOS区域(模拟)、收集SMBIOS信息、检查关键启动文件是否存在。

5.1 脚本的目标与运行逻辑

这个脚本解决一个很实际的问题:现场运维人员拿U盘插到机器上跑一下,自动把诊断信息和备份文件落盘,然后退出。不需要他们懂Shell命令,不需要手动敲一堆指令。

脚本放在U盘根目录,启动进入Shell后执行FS0:\debug.nsh,所有输出写到同一个日志目录下。

5.2 完整脚本内容

# debug.nsh - 简易固件诊断与备份脚本 # 用法: debug.nsh [日志目录] set LOGDIR %1 if "%LOGDIR%" == "" then set LOGDIR FS0:\LOGS endif if not exist %LOGDIR% then mkdir %LOGDIR% endif set LOGFILE %LOGDIR%\debug_%lasterror%.log # 1. dump设备映射 echo "====================" > %LOGFILE% echo "Device Map" >> %LOGFILE% map >> %LOGFILE% # 2. dump SMBIOS信息 echo "====================" >> %LOGFILE% echo "SMBIOS Info" >> %LOGFILE% smbiosview -b >> %LOGFILE% # 3. 检查关键EFI启动文件 echo "====================" >> %LOGFILE% echo "Check BootLoaders" >> %LOGFILE% set FOUND 0 if exist FS0:\EFI\BOOT\BOOTX64.EFI then echo "FS0: BOOTX64.EFI found" >> %LOGFILE% set FOUND 1 endif if exist FS1:\EFI\BOOT\BOOTX64.EFI then echo "FS1: BOOTX64.EFI found" >> %LOGFILE% set FOUND 1 endif # 4. dump启动项 echo "====================" >> %LOGFILE% echo "Boot Option List" >> %LOGFILE% bcfg boot dump >> %LOGFILE% # 5. 备份当前启动项配置 echo "====================" >> %LOGFILE% echo "Boot Option Backup" >> %LOGFILE% bcfg boot dump -v >> %LOGFILE% echo "====================" >> %LOGFILE% echo "Done. Log saved to %LOGFILE%" pause exit

5.3 逐段讲解:为什么这样写

参数与默认值

脚本开头先取%1作为日志目录,如果没传参,就默认用FS0:\LOGS。这里用到了前面讲过的空判断技巧——if "%LOGDIR%" == "",空值时%LOGDIR%被替换成空字符串,表达式就变成了if "" == "",成立,进入默认赋值逻辑。

日志文件的命名

我在这里耍了个小聪明:debug_%lasterror%.log。第一次执行时%lasterror%可能为空或0,第二次执行时它等于上一次某个命令的返回值。这个命名其实不够严谨(最终生成的文件名可能不唯一),但胜在简单。严格一点的做法是用%time%或%date%拼一个时间戳,但UEFI Shell里的时间格式依赖固件实现,输出经常是10:20:30带冒号,Windows文件系统不认冒号,所以用时间戳反而容易踩坑。

smbiosview -b参数

smbiosview是EDK2自带的SMBIOS查看命令,-b表示分页输出。在重定向到文件时,分页参数不仅不会打断输出,反而能让内容一次性完整落盘,不会因为控制台缓冲区限制丢行。这个细节最开始我不知道,导致dump出来的SMBIOS信息只有前几行,排查了半天。

bcfg boot dump

bcfg是启动项管理命令。bcfg boot dump列出当前所有启动项;-v参数会输出更详细的信息(比如关联的设备路径)。这个命令对排查"为什么开不了机"类问题非常有用。注意bcfg在某些精简固件里可能没被编译进去,如果提示命令不存在,回头看我第6节的排查方法。

5.4 执行与验证

这个脚本写完后,实际执行时我用了一个小技巧:先在QEMU里跑一遍,确认语法没问题,再放到真实U盘上跑。QEMU里跑的时候,第一次执行会报一堆smbiosview: command not found之类的错误吗?其实不会,因为OVMF固件里带了SHELL的完整命令集。但真机上确实可能缺命令,所以我在脚本里做了一件事:所有可能失败的命令,后面都靠%lasterror%去判断。

更关键的一点,写脚本时每条命令的返回值都是独立的。比如mkdir %LOGDIR%在目录已存在时会返回错误码,但这不影响脚本继续执行——Shell脚本默认不会因为单条命令失败而中止(除非你显式写-exit或使用abort命令)。这也是很多新手感到困惑的地方:明明上一行报错,脚本还是往下跑了。理解这一点,就能明白为什么脚本里要手动检查关键步骤的%lasterror%。

6. 踩坑实录:六年UEFI Shell使用中遇到的典型问题

这一部分单独拎出来,因为这些坑我几乎都实实在在踩过,每一类都花过不少时间排查。写出来帮你省点时间。

6.1 文件系统大小写与路径分隔符

UEFI Shell的文件系统和UEFI规范一样,不区分大小写,但输出时可能混合大小写。有次我写了一个判断:

if exist fs0:\EFI\BOOT\BOOTX64.EFI then

注意我写的是小写fs0。Shell能认出这个盘符吗?在大多数固件实现里可以,但有些严谨的实现可能要求必须用FS0:(全部大写)。我的建议是脚本里统一用大写盘符、大写路径分隔符,避免在固件兼容性上赌运气。

路径分隔符一律用反斜杠\,不要用正斜杠/。UEFI规范里正斜杠在某些上下文里有特殊含义(比如作为参数前缀),万一你把路径写错,排查起来非常痛苦。

6.2 命令找不到:环境变量和当前目录的坑

path环境变量影响Shell对可执行文件的查找范围。有次我脚本里用了一个自研的诊断.efi工具,放在U盘的EFI\Tools目录下。脚本第一行执行:

set path FS0:\EFI\Tools;%path%

结果下一行调用这个工具时,Shell提示命令找不到。排查了半天,发现是set path这个操作本身的问题——当一个环境变量名是path时,Shell在执行set path ...之后,当前这条命令的解析还没结束,它用的还是旧的path。等到下一条命令时新的path才生效。这看起来很正常,但问题在于:脚本第一行设置path,第二行就要用,应该没问题啊?

后来发现真正的原因是:UEFI Shell在解析脚本时,某些实现会在每行执行前重新读取path变量,而另一些实现只在脚本开始执行时读取一次path快照。第一种实现里,你中途改path对后续行生效;第二种实现里,你必须把path设置放在脚本最开头,或者用一个外层的wrapper脚本去设置path再调用真正的脚本。 这个行为差异没有写在规范里,纯靠实测发现。

现在的处理策略是:把path设置独立放在一个env.nsh文件里,所有需要环境变量的脚本第一行都改成include env.nsh或直接复制那两行set命令。绕开实现差异。

6.3include指令:别对它寄予太多希望

UEFI Shell有一个include命令,可以理解为类似C语言的#include。用法:

include FS0:\env.nsh

但实测下来,include在多个版本里行为不太稳定:有的要求被include的文件必须带.nsh扩展名否则拒绝执行,有的在相对路径下解析失败。而且它不能传参数,被include的脚本里%1~%9会被清空还是保留原调用者的参数,不同版本表现不一样。

我后来基本不用include,需要复用代码就直接把内容复制进主脚本,或者通过执行子脚本的方式传递参数:

call FS0:\subtask.nsh %parm%

call是执行另一个脚本的正确姿势,它会等子脚本执行完再回到当前脚本继续。虽然每启动一次子脚本有开销,但在脚本规模不大的前提下,可维护性比include强太多。

6.4 脚本编码与不可见字符

UEFI Shell脚本文本文件的编码要求不算严格,但必须注意两点。

第一,**保存为Unix换行(LF)还是Windows换行(CRLF)?**实测而言,Shell对两种换行都能处理,但混合换行会出问题。如果脚本在Windows下编辑完,放到Linux环境改了一行再拷回U盘,文件中同时出现LF和CRLF,Shell会在某些版本里把一个\r当成命令的一部分,导致"命令不存在"之类的报错,而且报错指向的行和你实际出错的行对不上——因为逐行解析时CR被吞掉与否取决于实现。

第二,如果脚本里含非ASCII字符(比如中文注释),必须在文件头部加BOM,否则Shell的文本解析可能乱码。这个问题在UEFI Shell 2.0之后有所缓解,但保险起见,脚本里我一般只用纯英文注释。

6.5%lasterror%的时效性

%lasterror%变量在每条命令执行后都会更新,但它有个特点:内置变量对set命令本身也会被更新。也就是说你执行set foo 1之后,%lasterror%变成的是这次set是否成功的状态,而不是最初那条你想检查的命令的状态。

所以正确的检查姿势是这样的:

bcfg boot dump > FS0:\boot.txt set ERRCODE %lasterror% if "%ERRCODE%" == "0" then echo dump ok else echo dump failed with %ERRCODE% endif

先把%lasterror%存到一个普通环境变量里,再拿去判断。直接写if "%lasterror%" == "0"虽然也能用,但中间一旦插入别的命令(比如echo),值可能已经变了。

还有一个小细节:%lasterror%虽说是错误码,但Shell命令成功时的返回值不一定是0。有些命令返回的不是标准0,而是自定义状态码。所以判断"成功"时,更可靠的方式是检查输出文件的内容是不是符合预期,而不是只看返回码。

6.6 内存读写命令的危险性

mm命令可以在Shell里直接读写物理内存和IO端口:

Shell> mm 0xE0000000 # 读这个地址的内容 Shell> mm 0xE0000000 0x1F # 往这个地址写0x1F

这是固件调试的利器,但也是最大的危险源。写错一个地址,轻则系统死机,重则把SPI Flash里的固件搞坏。我见过同事在调试时用mm往某个不知道是什么的地址写了个值,重启后板子直接起不来,最后靠编程器重新烧固件才救回来。

在脚本里涉及mm操作,我的建议是:

  • 先dmem或mem把目标区域读一遍,确认地址有效。
  • 写操作前人工确认三次地址没错。
  • 不要在自动化脚本里放未验证的mm写操作。
  • 操作完马上reset重启观察,不要继续跑后面的脚本。

6.7startup.nsh的自动执行机制

如果你把U盘插上,Shell启动时会自动尝试执行U盘根目录下的startup.nsh。这是一个很方便的机制,但也容易出事故。

有一次我调试脚本,把startup.nsh放到U盘根目录,内容是一个死循环等待某个条件。结果每次开机进Shell就卡死,拔U盘都来不及——因为Shell启动时读不到文件系统,U盘被占住,拔掉就报错。最后只能重启,进BIOS把启动顺序改成直接进系统,再把U盘格式化才解决。

所以我对startup.nsh的态度是:

  • 开发阶段不要把调试脚本命名为startup.nsh,用普通文件名,手动执行。
  • 需要自动执行时,startup.nsh里只放一个调用其他脚本的语句,不要写长逻辑。
  • 重要操作前先pause,给你自己留一个取消执行的机会。

6.8 脚本超时与交互

Shell脚本执行过程中如果需要人工输入,通常用pause命令(等按任意键)。但pause在无人值守场景下会卡住整个流程。如果需要倒计时自动继续,可以写一个循环配合stall命令(单位是微秒)实现延时:

set countdown 5 while %countdown% > 0 echo Continue in %countdown% ... stall 1000000 set countdown ... endwhile

这里有个矛盾:set不支持算术运算,所以set countdown %countdown%-1是无效的。实际做法是把倒计时变量拆成多个if判断,或者干脆用固定次数的for循环。比如:

for %i in (5 4 3 2 1) echo Continue in %i ... stall 1000000 endfor

这个方案最简单,既不依赖算术运算,也不容易出错。

7. 把Shell脚本用在自动化测试里的工程化思路

最后一部分聊聊怎么把.nsh脚本写得像工程代码,而不是随手写的玩具。这几年我在固件验证项目里逐渐摸索出一套自己的规范,分享出来供参考。

7.1 约定目录结构

一个完整的U盘工具包,目录结构建议如下:

FS0:\ ├── startup.nsh # 入口脚本(可选) ├── scripts\ │ ├── common.nsh # 公共函数(用call调用) │ ├── debug.nsh │ ├── backup.nsh │ └── flash.nsh ├── tools\ │ ├── shell.efi # 备用Shell │ ├── xxxdiag.efi # 自定义诊断工具 │ └── ... └── logs\ # 日志输出目录

脚本里统一用scripts\和tools\的绝对路径,不要在几十行里到处写FS0:\这种硬编码。如果U盘盘符变了(比如插到不同USB口变成FS1:),至少还有救——你只需要改顶部的几个set变量。

7.2 输出规范与日志级别

UEFI Shell的echo命令没有彩色输出,也没法区分日志级别。我的做法是在脚本里统一一个日志函数的概念(用子脚本实现):

:log_info echo [INFO] %1 >> %LOGFILE% goto :EOF

但前面说过,goto :EOF在UEFI Shell里的支持不完全可靠,所以这种"函数"写法我很少用。更实际的做法是:每条输出都同时打到屏幕和日志文件,屏幕输出帮助操作者实时了解进展,日志文件用于事后审计。

具体写法是这样:

echo "Step 1: dump smbios..." | tee %LOGFILE%

等等,tee命令在UEFI Shell里通常没有。所以实际做法是写两行:

echo "Step 1: dump smbios..." >> %LOGFILE% echo "Step 1: dump smbios..."

麻烦是麻烦一点,但可靠。我已经不指望Shell脚本能写出多优雅的日志框架了,能稳定执行比什么都强。

7.3 与UEFI固件内部的交互:变量命名空间

环境变量名不要起得太通用。比如你脚本里用了一个set boot,后面某段代码也用了set boot,互相覆盖了,排查起来非常费劲。我建议所有脚本变量统一加前缀,比如项目名缩写:

set DBG_LOGDIR FS0:\LOGS set DBG_MODE 1

这样不同脚本之间的变量冲突概率大大降低。

另外注意,UEFI Shell的环境变量会随脚本退出而保留(除非你显式删除或整个Shell重启)。也就是说一个脚本设置的变量,另一个脚本能看到。这既是便利也是隐患——一个写坏了的变量可能在半小时后的另一个脚本里莫名出现。

7.4 错误码传递与失败处理

脚本失败时的退出码是%lasterror%。在自动化测试框架里,我们经常在Shell脚本的最外层套一个Linux或Windows侧的驱动脚本,用串口或网络和Shell交互。Shell侧接收参数、执行测试、返回错误码,宿主侧解析。这种模式下,exit %lasterror%就非常关键,确保Shell退出时把最后的错误码带出去。

但有两点要特别小心:

  • 不是所有版本的Shell都支持exit %lasterror%的语法。部分实现只支持exit不带参数,返回码永远是0。
  • 如果你在.nsh里执行了另一个.efi程序,该程序的退出码会通过%lasterror%暴露出来,但前提是Shell没在这之间执行其他命令。任何一条命令都可能覆盖它。

为了确保错误码正确传递,在测试框架里我会专门写一个report.nsh:

set TEST_RESULT %lasterror% if "%TEST_RESULT%" == "0" then echo PASS > FS0:\logs\result.txt else echo FAIL:%TEST_RESULT% > FS0:\logs\result.txt endif exit %TEST_RESULT%

宿主侧的脚本只认result.txt里的内容,不依赖Shell退出码——因为实测下来,有的固件版本退出码头几位的字节会被截断,导致宿主侧收到奇怪的值。

7.5 与DXE驱动和EFI应用的配合

UEFI Shell最有意思的地方是可以加载DXE驱动和运行各种EFI应用。比如在测试过程中,你可能需要先加载一个自定义的协议驱动,再跑测试工具。典型写法:

load FS0:\tools\MyDriver.efi if "%lasterror%" == "0" then echo "driver loaded" else echo "driver load failed" endif

load命令加载驱动后,驱动常驻内存。此时可以用drivers命令查看驱动列表,用devices查看设备节点。如果有新设备出现,说明驱动生效了。

加载驱动时特别需要注意:驱动一旦加载成功,直到系统重启前都不会被卸载。你连续加载同一驱动两次,第二次会返回错误,但这个错误并不影响第一次加载的效果。所以脚本里不要因为某次加载报错就判定驱动有问题,要结合drivers输出确认是否已在列表里。

7.6 QEMU联调:不烧板子练脚本

最后安利一下QEMU调试法。EDK2项目官方就提供OVMF固件,配合QEMU可以完整模拟UEFI环境。具体步骤:

  1. 下载编译好的OVMF.fd固件,或者自己从EDK2源码编译。
  2. 准备一个FAT32格式的磁盘镜像,把写好的.nsh文件放进去。
  3. 启动命令大致是:
qemu-system-x86_64 -drive if=pflash,format=raw,file=OVMF.fd -drive format=raw,file=fat:rw:./disk
  1. QEMU启动时会自动进入OVMF的菜单,选择"UEFI Shell"即可。

这个方法有两个好处:一是完全不用碰真实硬件,脚本写错了重启一下虚拟机就行;二是QEMU支持GDB调试,你可以结合EDK2源码单步跟踪Shell命令的执行流程,理解每一条命令背后的逻辑。不过对大多数应用场景来说,能快速验证语法就够了,也不必深入到那个程度。

8. 最后分享几个关于Shell脚本的心得

回想这几年跟UEFI Shell打交道的经历,最深的感触是:这工具看着像Linux终端,用起来像DOS批处理,但它的很多细节都夹在固件实现和硬件平台的缝隙里。同一个脚本在这块板子上跑得好好的,换一块板子可能就行为迥异。所以我的建议一直没变——脚本要向最基础的命令看齐,不要赌固件实现的一致性和文档没有覆盖的边界行为。

具体到日常使用,有几件事是我每次上手都会做的:

  • 拿到一台新机器,先敲ver看Shell版本,再敲map看设备,最后ls -?、bcfg -?看命令支持情况。花五分钟快速评估这台机器的Shell能力边界,比后面盲写脚本踩坑强得多。
  • 写任何涉及硬件操作的脚本前,先在虚拟机里跑通逻辑,再在真机上人工跑一遍验证命令可用性和输出格式,最后才正式使用。
  • 遇到诡异问题,先用type或hexedit看脚本文本有没有不可见字符,然后再怀疑语法。这个排查路径能解决相当一部分"奇怪"的问题。

如果你平时做BIOS开发或固件测试,UEFI Shell脚本看着不起眼,但熟练之后真的能帮你把很多重复的调试工作变成一条命令的事。把这篇文章里的语法和踩坑点消化掉,你大概率就能写出属于自己的第一版可复用脚本了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 2:15:10

基于OpenCV的双目视觉物体尺寸测量:从标定到三维坐标计算

简介&#xff1a;这套基于 Python 与 OpenCV 的双目视觉尺寸测量项目源码及配套文档&#xff0c;面向需要完成毕业设计、期末大作业或课程设计的计算机视觉方向读者&#xff0c;旨在帮助快速搭建物体尺寸测量系统&#xff0c;理解双目视差、相机标定与三角测量等核心原理。资源…

作者头像 李华
网站建设 2026/10/1 2:14:55

基于YOLOv8的果园果实自动计数:从数据标注到Gradio部署全流程

简介&#xff1a;这份资源面向计算机、人工智能、自动化等专业的在校学生与教师&#xff0c;提供一套基于YOLOv8的果园成熟果实自动计数完整方案&#xff0c;可用于毕业设计、课程设计或大作业。压缩包共8个文件&#xff0c;约15.91MB&#xff0c;包含3个Python脚本、3个模型权…

作者头像 李华
网站建设 2026/10/1 2:14:52

现代智能雷达技术16——算法(2)

二次雷达&#xff08;SSR&#xff09;通过“一问一答”实现精准空域管理与敌我识别&#xff0c;其核心在于地面询问机与飞机应答机的数字对话。民用模式从广播式&#xff08;Mode A/C&#xff09;演进至点名式&#xff08;Mode S&#xff09;和自动广播&#xff08;ADS-B&#…

作者头像 李华
网站建设 2026/10/1 2:14:20

res-downloader 资源嗅探下载器三步上手完整指南

res-downloader 资源嗅探下载器三步上手完整指南 【免费下载链接】res-downloader 视频号、小程序、抖音、快手、小红书、直播流、m3u8、酷狗、QQ音乐等常见网络资源下载! 项目地址: https://gitcode.com/GitHub_Trending/re/res-downloader res-downloader 是一款跨平台…

作者头像 李华