UEFI Driver Development - Part 1

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.

EntryPoint

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 Supported(), Start() and 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.

Supported

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.

Start/Stop

Start() and 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.

UnloadImage<

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 FS2:.

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 drivers command:

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 9E.

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.

social