Booting into UEFI shell with GRUB

Ola Redell 3 April 2025

I recently had a need to update the UEFI (Unified Extensible Firmware Interface) firmware of an x86 module that one of our customers is using in their embedded device with Linux based on the Yocto project. The hardware manufacturer had provided us with a bunch of files for doing the update: The firmware binary, fw.bin, a UEFI shell script, GO.nsh, that can be called from the shell and that performs the update, a UEFI application, AfuEfix64.efi, used by GO.nsh to perform the actual programming, and a checksum file for verification.

Until now I have been used to there being a built in shell in the UEFI firmware that can be started during boot (commonly by pressing F7 early in the boot process), and from which a script such as GO.nsh can be executed. But that is not always the case. When there is no built in shell the UEFI shell binary needs to be provided at boot time, and started by the bootloader.

NOTE: An alternative way of starting the UEFI shell on boot this is most probably to use the UEFI boot support and bypass GRUB entirely, but I needed a quick fix for this GRUB booted system.

This is a short writeup of how I solved it with GRUB and a tailored USB image. I did not want to spend time creating a full blown image recipe in Yocto, with a wic image and all, so I took a shortcut using an already existing bootable USB with a FAT32 filesystem on the first partition. It looked something like this from the start

.
├─── EFI
│ └── BOOT
│ ├─── bootx64.efi
│ └── grub.cfg
├─── bzImage
├─── initramfs-image.cpio.gz
└── microcode.cpio

For this purpose however, we do not need any Linux kernel (bzImage), initramfs or microcode.cpio files, so I removed those and kept just the ./EFI/BOOT tree and placed the files from the hardware manufacturer on the top. The ./EFI/BOOT tree includes the grub-efi binary, bootx64.efi, and the GRUB configuration file. With UEFI, if there is a file /EFI/BOOT/bootx64.efi on the partition, and the partition is marked as bootable (active) and has some sort of FAT filesystem, then the bootx64.efi will be loaded and executed on boot.

The layout of the USB was now

.
├─── EFI
│ └── BOOT
│ ├─── bootx64.efi
│ └── grub.cfg
├─── AfuEfix64.efi
├─── Checksum.efi
├─── GO.nsh
└── fw.bin

The UEFI shell binary, Shellx64.efi

Next, I needed the UEFI shell binary that can be loaded by GRUB and that in its turn can be used to load the GO.nsh script. The Tianocore project provides an open source implementation of UEFI, and while it can be built locally following the instructions on Tianocore's github, I decided to just download the binary Shell.efi congestion from here, and try it out. The file was renamed to Shellx64.efi and copied alongsied the other files on the top level of the USB.

Add chainloader command to GRUB

To load a file like Shellx64.efi from GRUB, we need GRUB's chainloader command. Our current grub-efi build was not set up to include that command, so I built a new one using my normal Yocto build setup. I added this line to our bbappend file for grub-efi, grub-efi_2.06.bbappend.

GRUB_BUILDIN += "chain" 

As an alternative, this addition could have been made in e.g. local.conf axis GRUB_BUILDIN:pn-grub-efi += "chain".

When the new grub-efi had been built, the resulting file build/tmp/deploy/images/${MACHINE}/grub-efi-bootx64.efi was copied to the USB as EFI/BOOT/bootx64.efi, replacing the original file.

Modify grub.cfg to load the UEFI shell

Next I modified the grub.cfg file on the USB to contain only one menuentry

serial --unit=0 --speed=115200 --word=8 --parity=no --stop=1
timeout=3

menuentry 'Install new firmware'{
    search --set=root --file /Shellx64.efi
    chainloader /Shellx64.efi
}

The menu entry first issues a search for the partition containing Shellx64.efi and sets it to GRUB's root such that the chainloader on the next line finds the file it needs to load.

Booting into the UEFI shell

By plugging in this USB to the system and booting from it, we get into the UEFI shell, loaded by GRUB. To update the firmware we could do

shell> fs0:
fs0:> GO.nsh

Automatic firmware update on boot

Since we had 50+ systems that needed a firmware update, I wanted an automatic way to run this script directly on boot, so I renamed the script GO.nsh to startup.nsh, such that it is started automatically by the UEFI shell, and modified the contents of the script a bit, from being:

AfuEfix64.efi fw.bin /P /N /R /X /SHUTDOWN

to

if exist fs0:startup.nsh then
   fs0:
else
   fs1:
endif

AfuEfix64.efi fw.bin /P /N /R /X /SHUTDOWN

This starts by selecting the correct device fs0: or fs1: such that the AfuEfix64.efi application and the fw.bin files are found. It also handles the case when the USB gets mapped to fs1: instead of fs0:, which happens when the eMMC on the system is already populated with a filesystem. This specific trick works for this system, but is of course not universally useful.

The final layout of the USB filesystem is

.
├─── EFI
│ └── BOOT
│ ├─── bootx64.efi
│ └── grub.cfg
├─── AfuEfix64.efi
├─── Shellx64.efi
├─── startup.nsh
└── fw.bin