DEV Community

MachineHunter
MachineHunter

Posted on

How to make custom UEFI Protocol

DXE Driver's main task is to create and install Protocols. In this post, I will explain how to create Protocol in EDK2 and how to use that protocol from other UEFI module.

UEFI Protocol Explanation

DXE Driver abstract the access to the device by creating Protocols. For example, you can use Tcg2Protocol to access TPM2.0 functionality rather than accessing it via things like MMIO or Port IO. Protocols are data structures containing function pointers and the actual function's contents are in the DXE Driver which installed that Protocol. The Protocol (data structure) itself is also in that DXE Driver (usually in .data section).
Image description
(Image from EDK2 UEFI Driver Writer's Guide 3.6.2)

Each Protocol has its own GUID and this is used for other UEFI modules to locate this Protocol. Several Protocols can be grouped and this group is identified by Handle. Therefore, you can use gBS->LocateHandle first to locate Handle and then locate the Protocol inside. However, it might be more simpler to use gBS->LocateProtocol to locate Protocol by GUID directly.

Protocols are stored into a single database because it has to be accessible via any UEFI modules. In order to store the protocol to this database, the DXE Driver which create the protocol has to use gBS->InstallMultipleProtocolInterfaces inside.

Implementation

MyDxe2.inf

Let's start with the inf file. The main point is to write gEfiDummyProtocol to the [Protocols]. We have to write the Protocols this DXE Driver produce and also consume.

[Defines]
  INF_VERSION                    = 0x00010006
  BASE_NAME                      = MyDxe2
  FILE_GUID                      = 9d46dccd-ea11-4808-93e9-9b7021889b2d
  MODULE_TYPE                    = DXE_DRIVER
  VERSION_STRING                 = 1.0
  ENTRY_POINT                    = MyDxeEntry

[Sources]
    MyDxe2.h
    MyDxe2.c

[Packages]
    MdePkg/MdePkg.dec
    MdeModulePkg/MdeModulePkg.dec

[LibraryClasses]
    UefiDriverEntryPoint
    UefiRuntimeServicesTableLib
    UefiBootServicesTableLib
    UefiLib
    PrintLib

[Protocols]
    gEfiDummyProtocolGuid   ## PRODUCES

[Depex]
    TRUE
Enter fullscreen mode Exit fullscreen mode

MyDxe2.h

This is the header file. This mainly defines types like EFI_DUMMY_PROTOCOL or DUMMY_FUNC1, so that you can use it in MyDxe2.c. Definition of gEfiDummyProtocolGuid will be defined in the separate place.

#include <Uefi.h>
#include <Library/UefiLib.h>
#include <Library/PrintLib.h>
#include <Library/UefiDriverEntryPoint.h>
#include <Library/UefiRuntimeServicesTableLib.h>
#include <Library/UefiBootServicesTableLib.h>

typedef EFI_STATUS (EFIAPI *DUMMY_FUNC1)();
typedef EFI_STATUS (EFIAPI *DUMMY_FUNC2)();

// e9811f61-14bd-4637-8d17-aec540d8f508
#define EFI_DUMMY_PROTOCOL_GUID \
    { 0xe9811f61, 0x14bd, 0x4637, { 0x8d, 0x17, 0xae, 0xc5, 0x40, 0xd8, 0xf5, 0x08 } }

extern EFI_GUID gEfiDummyProtocolGuid;

typedef struct _EFI_DUMMY_PROTOCOL {
    DUMMY_FUNC1 DummyFunc1;
    DUMMY_FUNC2 DummyFunc2;
} EFI_DUMMY_PROTOCOL;
Enter fullscreen mode Exit fullscreen mode

MyDxe2.c

This is the DXE Driver's contents. mDummy is the actual data structure of this Protocol and this Protocol is installed by gBS->InstallMultipleProtocolInterfaces. The content of DummyFunc1 and DummyFunc2 can be any, but in this example, I'm storing the message "DummyFuncN called" in UEFI variable called MyDxeStatus, so that, you can check if the Protocols is really running correctly.

#include "MyDxe2.h"

UINT32 myvarSize      = 30;
CHAR8  myvarValue[30] = {0};
CHAR16 myvarName[30]  = L"MyDxeStatus";

// eefbd379-9f5c-4a92-a157-ae4079eb1448
EFI_GUID myvarGUID = { 0xeefbd379, 0x9f5c, 0x4a92, { 0xa1, 0x57, 0xae, 0x40, 0x79, 0xeb, 0x14, 0x48 }};

EFI_HANDLE mDummyHandle = NULL;

EFI_STATUS EFIAPI DummyFunc1() {
    AsciiSPrint(myvarValue, 18, "DummyFunc1 called");
    gRT->SetVariable(
            myvarName,
            &myvarGUID,
            EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_BOOTSERVICE_ACCESS | EFI_VARIABLE_RUNTIME_ACCESS,
            myvarSize,
            myvarValue);
    return EFI_SUCCESS;
}

EFI_STATUS EFIAPI DummyFunc2() {
    AsciiSPrint(myvarValue, 18, "DummyFunc2 called");
    gRT->SetVariable(
            myvarName,
            &myvarGUID,
            EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_BOOTSERVICE_ACCESS | EFI_VARIABLE_RUNTIME_ACCESS,
            myvarSize,
            myvarValue);
    return EFI_SUCCESS;
}

EFI_DUMMY_PROTOCOL mDummy = {
    DummyFunc1,
    DummyFunc2
};

EFI_STATUS EFIAPI MyDxeEntry(IN EFI_HANDLE ImageHandle, IN EFI_SYSTEM_TABLE *SystemTable) {
    gBS->InstallMultipleProtocolInterfaces(&mDummyHandle, &gEfiDummyProtocolGuid, &mDummy, NULL);

    EFI_TIME time;
    gRT->GetTime(&time, NULL);
    AsciiSPrint(myvarValue, 12, "%2d/%2d %2d:%2d", time.Month, time.Day, time.Hour, time.Minute);

    gRT->SetVariable(
            myvarName,
            &myvarGUID,
            EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_BOOTSERVICE_ACCESS | EFI_VARIABLE_RUNTIME_ACCESS,
            myvarSize,
            myvarValue);

    return EFI_SUCCESS;
}
Enter fullscreen mode Exit fullscreen mode

MdeModulePkg.dec

We should define gEfiDummyProtocolGuid to MdeModulePkg.dec in order to use gEfiDummyProtocolGuid in the other UEFI module. Inside MdeModulePkg.dec, add the definition like below.

[Protocols]
...
  ## MyDxe/MyDxe2.h
  gEfiDummyProtocolGuid = { 0xe9811f61, 0x14bd, 0x4637, { 0x8d, 0x17, 0xae, 0xc5, 0x40, 0xd8, 0xf5, 0x08 } }
Enter fullscreen mode Exit fullscreen mode

MdeModulePkg.dsc

[Components]
  MdeModulePkg/MyDxe/MyDxe2.inf
  ...
Enter fullscreen mode Exit fullscreen mode

Build it

Make sure you have ACTIVE_PLATFORM = MdeModulePkg/MdeModulePkg.dsc in Conf/target.txt and let's build it. Then, construct FFS file like I explained in the previous post, add the FFS file to the BIOS image, and flash the SPI flash chip with that image.

Using DummyProtocol

Next, let's write a module that uses this Protocol. I will be writing a UEFI module that runs from a USB memory in BDS phase, so that I can print the output to the screen easily.
Create a folder named AppPkg/ under edk2/ and will be creating these files.

AppPkg/
├── AppPkg.dec
├── AppPkg.dsc
└── Applications/
    └── UseDummyProtocol/
        ├── UseDummyProtocol.c
        └── UseDummyProtocol.inf
Enter fullscreen mode Exit fullscreen mode

AppPkg.dec

FirstPkg can be any name.

[Defines]
  DEC_SPECIFICATION              = 0x00010005
  PACKAGE_NAME                   = FirstPkg
  PACKAGE_GUID                   = 75281f80-8657-4cf2-837f-04d73179c590
  PACKAGE_VERSION                = 0.1
Enter fullscreen mode Exit fullscreen mode

AppPkg.dsc

[Defines]
  PLATFORM_NAME                  = FirstPkg
  PLATFORM_GUID                  = 7b86cb02-9fbb-4f77-8d93-35c8c90f2488
  PLATFORM_VERSION               = 0.1
  DSC_SPECIFICATION              = 0x00010005
  OUTPUT_DIRECTORY               = Build/FirstPkg$(ARCH)
  SUPPORTED_ARCHITECTURES        = IA32|X64
  BUILD_TARGETS                  = DEBUG|RELEASE|NOOPT
  DEFINE DEBUG_ENABLE_OUTPUT     = TRUE

[LibraryClasses]
  # Entry point
  UefiApplicationEntryPoint|MdePkg/Library/UefiApplicationEntryPoint/UefiApplicationEntryPoint.inf
  ShellCEntryLib|ShellPkg/Library/UefiShellCEntryLib/UefiShellCEntryLib.inf

  # Common Libraries
  BaseLib|MdePkg/Library/BaseLib/BaseLib.inf
  BaseMemoryLib|MdePkg/Library/BaseMemoryLib/BaseMemoryLib.inf
  PcdLib|MdePkg/Library/BasePcdLibNull/BasePcdLibNull.inf
  UefiBootServicesTableLib|MdePkg/Library/UefiBootServicesTableLib/UefiBootServicesTableLib.inf
  !if $(DEBUG_ENABLE_OUTPUT)
    DebugLib|MdePkg/Library/UefiDebugLibConOut/UefiDebugLibConOut.inf
    DebugPrintErrorLevelLib|MdePkg/Library/BaseDebugPrintErrorLevelLib/BaseDebugPrintErrorLevelLib.inf
  !else   ## DEBUG_ENABLE_OUTPUT
    DebugLib|MdePkg/Library/BaseDebugLibNull/BaseDebugLibNull.inf
  !endif  ## DEBUG_ENABLE_OUTPUT

  UefiLib|MdePkg/Library/UefiLib/UefiLib.inf
  PrintLib|MdePkg/Library/BasePrintLib/BasePrintLib.inf
  MemoryAllocationLib|MdePkg/Library/UefiMemoryAllocationLib/UefiMemoryAllocationLib.inf
  UefiRuntimeServicesTableLib|MdePkg/Library/UefiRuntimeServicesTableLib/UefiRuntimeServicesTableLib.inf
  UefiBootServicesTableLib|MdePkg/Library/UefiBootServicesTableLib/UefiBootServicesTableLib.inf
  DevicePathLib|MdePkg/Library/UefiDevicePathLib/UefiDevicePathLib.inf
  RegisterFilterLib|MdePkg/Library/RegisterFilterLibNull/RegisterFilterLibNull.inf

[Components]
  AppPkg/Applications/UseDummyProtocol/UseDummyProtocol.inf
Enter fullscreen mode Exit fullscreen mode

UseDummyProtocol.inf

[Defines]
  INF_VERSION                    = 0x00010006
  BASE_NAME                      = UseDummyProtocol
  FILE_GUID                      = 5765e8dd-c97a-4ce2-891e-9e24b94d654b
  MODULE_TYPE                    = UEFI_APPLICATION
  VERSION_STRING                 = 0.1
  ENTRY_POINT                    = UefiMain

[Sources]
  UseDummyProtocol.c

[Packages]
  MdePkg/MdePkg.dec
  MdeModulePkg/MdeModulePkg.dec

[LibraryClasses]
  UefiLib
  ShellCEntryLib
  UefiBootServicesTableLib
  UefiRuntimeServicesTableLib

[Protocols]
  gEfiDummyProtocolGuid  ## CONSUME
Enter fullscreen mode Exit fullscreen mode

UseDummyProtocol.c

This is checking UEFI variable MyDxeStatus by GetVariable before and after calling DummyFunc1 (the DummyProtocol's function). DummyFunc1 will change the value of MyDxeStatus to a DummyFunc1 called which was originally the current time set when this DXE Driver was executed.

#include <Uefi.h>
#include <Library/UefiLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/UefiRuntimeServicesTableLib.h>
#include <MyDxe/MyDxe2.h>
#include <Library/BaseMemoryLib.h>

EFI_STATUS EFIAPI UefiMain(IN EFI_HANDLE ImageHandle, IN EFI_SYSTEM_TABLE *SystemTable) {
    EFI_DUMMY_PROTOCOL *DummyProtocol;
    gBS->LocateProtocol(&gEfiDummyProtocolGuid, NULL, (VOID**)&DummyProtocol);

    CHAR8 myvarValue[30];
    UINT64 myvarSize = 30;
    UINT32 Attributes = EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_BOOTSERVICE_ACCESS | EFI_VARIABLE_RUNTIME_ACCESS;
    EFI_GUID myvarGUID = { 0xeefbd379, 0x9f5c, 0x4a92, { 0xa1, 0x57, 0xae, 0x40, 0x79, 0xeb, 0x14, 0x48 }};

    Print(L"[Before Calling Protocol] MyDxeStatus:\r\n");
    gRT->GetVariable(
            L"MyDxeStatus",
            &myvarGUID,
            &Attributes,
            &myvarSize,
            myvarValue);

    AsciiPrint("%a\n\n", myvarValue);

    DummyProtocol->DummyFunc1();

    Print(L"[After Calling Protocol] MyDxeStatus:\r\n");
    gRT->GetVariable(
            L"MyDxeStatus",
            &myvarGUID,
            &Attributes,
            &myvarSize,
            myvarValue);

    AsciiPrint("%a\n", myvarValue);

    while(1);
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Build and Execute it

Before build, change to ACTIVE_PLATFORM = AppPkg/AppPkg.dsc in the Conf/target.txt. Then, build it.
This time, I will store the generated UseDummyProtocol.efi to the USB memory. In USB memory, make a directory EFI/BOOT/ and rename UseDummyProtocol.efi into bootx64.efi and put that into the folder.

Plug the USB into the PC you flashed the BIOS before, and boot that machine. If you see below output, then it's a success.
Image description
(I missed specifing the string length...)

Top comments (0)