One of the key features of UEFI is that the specification and the APIs/ABIs it guarantees provides the ability to produce portable applications, drivers and libraries (in the form of protocols). On the simpler side, by letting you compile the driver once for each architecture - and on the more space age side by letting you build a single driver that works across all architectures (using EFI Byte Code). The extra magic comes in the form of Option ROM support, which lets plug-in PCI cards keep a driver onboard, informing UEFI to load it on boot. (Any jokes about Forth drivers for Open Firmware go here.)
So, having never actually written a UEFI driver from scratch, and most of the drivers I have come across having really been platform drivers, I figured that would be a good start to write a standalone driver from scratch. And the outcome is this slightly hands-on blog post series. This part covers:
- creating a new driver from scratch
- building it as a standalone driver
- loading it from the UEFI Shell
- having it detect the presence of a device it recognizes
- unloading it from the UEFI Shell
Having just come across a hardware random number generator called ChaosKey, I figured this would make an excellent candidate.
Creating a standalone UEFI driver from scratch
Since UEFI drivers are meant to be standalone buildable, they tend to be kept in separate directories. Since this driver is intended to run during/after DXE phase, let's put everything in a directory called ChaosKeyDxe (CamelCase mandatory - link takes you to the directory in git, where you can view the files directly).
Create a build information file
First of all, we need a build information file (.inf). The format of the build information file is described in the EDK II INF File Spec, which can be found in the EDK2 github pages.
Start with a [Defines] section
[Defines] INF_VERSION = 0x00010019 BASE_NAME = ChaosKeyDxe FILE_GUID = 9A54122B-F5E4-40D8-AE61-A71E406ED449 MODULE_TYPE = UEFI_DRIVER VERSION_STRING = 1.0 ENTRY_POINT = ChaosKeyEntryPoint UNLOAD_IMAGE = ChaosKeyUnload
The INF_VERSION reflects which version of the INF file format is being followed, in this case 1.25 (
0x0001.0x0019). Interestingly, nearly all build information files I have come across before specify 0x00010005 ... cargo culting from earlier examples.
Followed by BASE_NAME, the single word identifier used for this component. To keep things simple, I'm reusing the directory name, for the same reasons.
And then a FILE_GUID, generated uniquely for this file - for example through this online generator, ensuring Uppcase and Hyphens are both ticked. If this string is copied from an existing template rather than uniquely generated, really bad stuff will happen.
And then MODULE_TYPE to tell the build system we are producing a UEFI_DRIVER.
A VERSION_STRING is also required - this is simply a UCS2 string indicating the version of the driver.
The ENTRY_POINT function is automatically called at driver load time, and needs to contain code that registers the driver with the system, and do any other global setup needed to make the driver ready to set up individual devices.
UNLOAD_IMAGE points to the function cleaning up after the driver when it is to be unloaded. This is not really mandatory for a driver expected to be used for booting the system (it will be discarded by the operating system anyway, unless it takes specific actions to keep bits resident), but it comes in very handy for development.
The build information file usually includes a (commentary only) stanza stating which architectures the executable is expected to work on.
# VALID_ARCHITECTURES = AARCH64 ARM EBC IA32 IPF X64 #
The remainder of the file simply specifies which source files are used to build the driver, which declaration files it uses (
MdePkg/MdePkg.dec), which library classes it needs (resolved into specific libraries by the build description file) and which protocols it consumes.
[Sources] ChaosKeyDriver.h DriverBinding.c [Packages] MdePkg/MdePkg.dec [LibraryClasses] UefiBootServicesTableLib UefiDriverEntryPoint UefiLib [Protocols] gEfiUsbIoProtocolGuid
Adding some actual code
Since we are not yet implementing any actual functionality beyond discovery, the only C source file added at this point is
DriverBinding.c. This all comes down to implementing an instance of
EFI_DRIVER_BINDING_PROTOCOL. Let us go through that, function by function.
All the entry point function does is register the protocol instance, as defined in the
gUsbDriverBinding struct, with the system - and return
EFI_SUCCESS, printing an informational message as it does so. The
gUsbDriverBinding struct contains pointers to the
Stop() functions defined by the protocol, as well as a
Version number which lets UEFI pick the most up to date driver if multiple are available.
When a new device is detected in the system, UEFI will ask all of the plausible drivers whether they know how to deal with it, by calling their
Supported() function. This implementation is probably one of the few bits of this driver that is pretty much feature complete - all it really needs to do is to find the USB manufacturer/device IDs and see if they are ones the driver knows how to handle. It then returns
EFI_UNSUPPORTED if this is a device it does not support, or
EFI_SUCCESS if it is a device it supports.
Stop() are left empty for now, returning
EFI_UNSUPPORTED whenever they are called. This is something that will be filled in in part 2 of this series.
Finally, when (if!) we unload the driver,
UnloadImage() ensures that the bits that were registered by
EntryPoint() are unregistered again.
Building and using the driver
Building the standalone driver
In order to build a standalone driver, you need a platform description (
.dsc) file, mapping your library dependencies to actually available libraries. One way of doing this is to implement your own complete
.dsc. However, this is exactly what EDK2's
OptionRomPkg/OptionRomPkg.dsc provides. So a simpler way can be to simply add the
.inf to the
[Components] section of
OptionRomPkg.dsc. After that, the build should be as easy as:
GCC5_AARCH64_PREFIX=aarch64-linux-gnu- build -a AARCH64 -t GCC5 -p OptionRomPkg/OptionRomPkg.dsc -m OpenPlatformPkg/Drivers/Usb/Misc/ChaosKeyDxe/ChaosKeyDxe.inf
Loading the driver
For this example (and because Juno's built-in magical program-over-USB filesystem is insane), let's load the driver from a USB key. For simplicity's sake, have it plugged in when powering on and drop into the UEFI Shell. In my case, the USB key ended up as filesystem
Shell> FS2: FS2:\> load ChaosKeyDxe.efi add-symbol-file /home/leif/work/git/edk2/Build/OptionRomPkg/DEBUG_GCC5/AARCH64/OpenPlatformPkg/Drivers/Usb/Misc/ChaosKeyDxe/ChaosKeyDxe/DEBUG/ChaosKeyDxe.dll 0xF85E4000 Loading driver at 0x000F85E3000 EntryPoint=0x000F85E4044 ChaosKeyDxe.efi *** Installed ChaosKey driver! *** Image 'FS2:\ChaosKeyDxe.efi' loaded at F85E3000 - Success FS2:\>
At this point, you can verify that the driver has loaded by invoking the
FS2:\> drivers T D Y C I P F A DRV VERSION E G G #D #C DRIVER NAME IMAGE PATH === ======== = = = === === =================================== ========== 23 00000030 D N N 1 0 <null string> Fv(B73FE497-B92E-416E-8326-45AD0D270092)/FvFile(1DF18DA0-A18B-11DF-8C3A-0002A5D5C51B) ... 6F 0000000A D N N 1 0 <null string> Fv(B73FE497-B92E-416E-8326-45AD0D270092)/FvFile(4579B72D-7EC4-4DD4-8486-083C86B182A7) 9E 0000000A ? N N 0 0 <null string> FS2:\ChaosKeyDxe.efi
You can see how the driver is loaded, and has the driver handle number
Now, plug in the ChaosKey:
FS2:\> ChaosKey (0x1D50:0x60C6) is my homeboy! UsbSelectConfig: failed to connect driver Not Found, ignored
We can see how the
Supported() function is invoked, and we then see an error message. The error is triggered by our empty
Start() function returning EFI_UNSUPPORTED when UEFI attempts to bring the device online. We'll fix that bit in part 2 of the series.
Unloading the driver
During development, it can be handy to be able to load/unload without a full reboot. To do so, just call
unload with the driver handle you got from the
drivers command. Add the
-v option to get some extra output.
FS2:\> unload -v 9E Revision......: 0x00001000 ParentHandle..: FD317918 SystemTable...: FDF30018 DeviceHandle..: FD243918 FilePath......: FC98FF98 OptionsSize...: 0 LoadOptions...: <null string> ImageBase.....: F85E3000 ImageSize.....: 8000 CodeType......: EfiBootServicesCode DataType......: EfiBootServicesData Unload........: F85E4000 Unload - Handle [FC990598]. [y/n]? y remove-symbol-file /home/leif/work/git/edk2/Build/OptionRomPkg/DEBUG_GCC5/AARCH64/OpenPlatformPkg/Drivers/Usb/Misc/ChaosKeyDxe/ChaosKeyDxe/DEBUG/ChaosKeyDxe.dll 0xF85E4000 Unload - Handle [FC990598] Result Success. FS2:\>
You can also verify the unload was successful by running
drivers again, and seeing this driver no longer listed.