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
file 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
as
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 menuentry
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