operating-systemuefi

Calling the entry point of a separate EFI file from a UEFI bootloader


I am looking to call a function from a separate EFI file from my UEFI-utilizing bootloader, both of which are in the same virtual hard disk. I've looked into both the LoadImage and StartImage functions defined in the specification, but I cannot figure out how I am supposed to use the EFI_DEVICE_PATH_PROTOCOL type to my advantage.

From my research, I've found that close to all examples or problems that other people have had are solved by using some FileDevicePath function provided in EDK2 and GNU-EFI, which I'm not utilizing at the moment as I find the learning experience of starting from scratch very interesting and enlightening. My observations have concluded that this FileDevicePath function is capable of smashing data related to the file path name and file media type into an EFI_DEVICE_PATH_PROTOCOL variable, but I am not sure how I can do this myself as both source code samples use "helper" functions and macros for "magic numbers". Namely, I'm struggling with understanding how the required data for the device path protocol structure (path, type attributes, length, etc.) fits into the specification's declaration of the struct. If somebody can point me in the right direction on this topic, I have massive respect for your intellect and knowledge using this system. Thank you all for taking time out of your days for this.


Solution

  • A device path consists of one or more nodes. Every node is a struct with the same header but can contain different data after the header. To iterate over the device path you have to advance Length bytes from the start of the header to get to the next one. If the device path is valid, it is terminated with an end node of type 0x7F and subtype 0x01 or 0xFF. I have seen systems that use 0xFF as end node type, you might check for that as well.

    You need to get the device path of the parent device (the partition from which your bootloader was loaded) via the EFI_LOADED_IMAGE_PROTOCOL protocol. The field you need is DeviceHandle, use HandleProtocol or OpenProtocol to get the device path from this handle.

    Create a copy of the device path, if you don't want to use the helper functions you have to iterate over the device path nodes until you find the end node to get the length. You need to add some space to the copy, so you can insert a file path node.

    Insert the file path node with the path to the efi file before the end node.

    Call LoadImage with the new device path, and StartImage with the image handle returned by LoadImage.

    Here is a EDK2 based sample, that tries to load and start another file from the same device/partition:

    EFI_STATUS EFIAPI DevicePathLengthWithoutEnd(
        IN  CONST EFI_DEVICE_PATH_PROTOCOL* DevicePath,
        OUT UINTN* Length)
    {
        UINTN TotalLength;
        UINT16 CurrentLength;
        CONST EFI_DEVICE_PATH_PROTOCOL* CurrentNode = NULL;
    
        if (DevicePath == NULL || Length == NULL) {
            return EFI_INVALID_PARAMETER;
        }
    
        *Length = 0;
    
        TotalLength = 0;
        for (
            CurrentNode = DevicePath;
            // You may want to check for the SubType also
            CurrentNode->Type != 0x7F;
            CurrentNode = ((CONST EFI_DEVICE_PATH_PROTOCOL*)(((UINT8*)CurrentNode) + (*(UINT16*)CurrentNode->Length)))) {
    
            CurrentLength = *((UINT16*)CurrentNode->Length);
            // Check for invalid nodes
            if (CurrentLength < sizeof(EFI_DEVICE_PATH_PROTOCOL)) {
                return EFI_INVALID_PARAMETER;
            }
    
            TotalLength += (UINTN)CurrentLength;
        }
    
        *Length = TotalLength;
    
        return EFI_SUCCESS;
    }
    
    EFI_STATUS EFIAPI LoadFile(
        IN  CONST CHAR16* PathOnCurrentDevice)
    {
        EFI_STATUS Status;
    
        UINTN PathSize;
        UINTN OriginalDevicePathLength;
        UINTN NewDevicePathLength;
    
        EFI_LOADED_IMAGE_PROTOCOL* LoadedImage = NULL;
        CONST EFI_DEVICE_PATH_PROTOCOL* OriginalDevicePath = NULL;
        EFI_DEVICE_PATH_PROTOCOL* NewDevicePath = NULL;
        EFI_DEVICE_PATH_PROTOCOL* NodeHelper = NULL;
        UINT8* ByteHelper = NULL;
    
        EFI_HANDLE ImageHandle;
    
        if (PathOnCurrentDevice == NULL) {
            return EFI_INVALID_PARAMETER;
        }
    
        // Get the size of the string (with \0) in bytes
        PathSize = StrSize(PathOnCurrentDevice);
    
        // Get the loaded image protocol from the current images handle
        Status = gBS->HandleProtocol(
            // ImageHandle parameter from UefiMain/efi_main
            gImageHandle,
            &gEfiLoadedImageProtocolGuid,
            (VOID**)&LoadedImage);
        if (EFI_ERROR(Status)) {
            Print(u"%a:%d -> %r\r\n", __FILE__, __LINE__, Status);
            goto finish;
        }
    
        // Get the device path from the current images source device handle
        Status = gBS->HandleProtocol(
            LoadedImage->DeviceHandle,
            &gEfiDevicePathProtocolGuid,
            (VOID**)&OriginalDevicePath);
        if (EFI_ERROR(Status)) {
            Print(u"%a:%d -> %r\r\n", __FILE__, __LINE__, Status);
            goto finish;
        }
    
        // Get the length of the device path without the end node
        Status = DevicePathLengthWithoutEnd(OriginalDevicePath, &OriginalDevicePathLength);
        if (EFI_ERROR(Status)) {
            Print(u"%a:%d -> %r\r\n", __FILE__, __LINE__, Status);
            goto finish;
        }
    
        // You need the old path + the file node + an end node
        NewDevicePathLength = 
            OriginalDevicePathLength +
            sizeof(EFI_DEVICE_PATH_PROTOCOL) + PathSize +
            sizeof(EFI_DEVICE_PATH_PROTOCOL);
    
        Status = gBS->AllocatePool(
            EfiLoaderData,
            NewDevicePathLength,
            (VOID**)&NewDevicePath);
        if (EFI_ERROR(Status)) {
            Print(u"%a:%d -> %r\r\n", __FILE__, __LINE__, Status);
            goto finish;
        }
    
        ByteHelper = (UINT8*)NewDevicePath;
    
        // Clone the old path
        gBS->CopyMem(ByteHelper, (VOID*)OriginalDevicePath, OriginalDevicePathLength);
    
        ByteHelper += OriginalDevicePathLength;
    
        // Insert the file node after the old path
        NodeHelper = (EFI_DEVICE_PATH_PROTOCOL*)ByteHelper;
        NodeHelper->Type = 0x04;    // 0x04: Media
        NodeHelper->SubType = 0x04; // 0x04: File
        *((UINT16*)NodeHelper->Length) = (UINT16)(sizeof(EFI_DEVICE_PATH_PROTOCOL) + PathSize);
    
        ByteHelper += sizeof(EFI_DEVICE_PATH_PROTOCOL);
    
        gBS->CopyMem(ByteHelper, (VOID*)PathOnCurrentDevice, PathSize);
    
        ByteHelper += PathSize;
    
        // Add the end node
        NodeHelper = (EFI_DEVICE_PATH_PROTOCOL*)ByteHelper;
        NodeHelper->Type = 0x7F;    // 0x7f: End
        NodeHelper->SubType = 0xFF; // 0xFF: End entire device path
        *((UINT16*)NodeHelper->Length) = sizeof(EFI_DEVICE_PATH_PROTOCOL);
    
        // Load and start the image
        Status = gBS->LoadImage(
            FALSE,
            gImageHandle,
            NewDevicePath,
            NULL,
            0,
            &ImageHandle);
        if (EFI_ERROR(Status)) {
            Print(u"%a:%d -> %r\r\n", __FILE__, __LINE__, Status);
            goto finish;
        }
    
        Status = gBS->StartImage(
            ImageHandle,
            NULL,
            NULL);
        if (EFI_ERROR(Status)) {
            Print(u"%a:%d -> %r\r\n", __FILE__, __LINE__, Status);
            goto finish;
        }
    
        Status = EFI_SUCCESS;
        goto finish;
    
    finish:
        if (NewDevicePath) gBS->FreePool(NewDevicePath);
    
        return Status;
    }