Mastering UEFI Protocol Calls: A Comprehensive Guide for Application Developers
Introduction
Previous discussions established that a Protocol's essence consists of a C language structure containing a series of function pointers. For example, the Simple Text Output Protocol appears as follows:
typedef struct {
EFI_TEXT_RESET Reset;
EFI_TEXT_OUTPUT_STRING OutputString;
EFI_TEXT_TEST_STRING TestString;
EFI_TEXT_QUERY_MODE QueryMode;
// ... additional function pointers
EFI_SIMPLE_TEXT_OUTPUT_MODE *Mode; // Data pointer to protocol state information
} EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL;This article demonstrates how to call functions defined within Protocols from UEFI applications, providing practical guidance for firmware developers.
Example Program: Graphics Output Protocol
We'll use the Graphics Output Protocol (GOP) as our working example, writing code that demonstrates how applications locate and utilize specific protocols.
Understanding Handles and Protocols
A Handle represents an instance of a particular object. Protocols typically attach to corresponding Handles, indicating the capabilities that Handle possesses. For instance, a hard drive Handle requires block read/write functionality, which corresponds to the Block IO Protocol.
This relationship between Handles and Protocols forms the foundation of UEFI's extensible architecture.
Program Logic Overview
Our example code follows this logical flow:
- Locate Handles: Find handles supporting the specified protocol using the Protocol's GUID. Note that multiple handles may support the same protocol.
- Obtain Protocol Interface: After acquiring handles, retrieve the specific protocol interface to call its functions.
This two-step process—location followed by interface acquisition—represents the standard pattern for protocol access in UEFI applications.
Key Boot Service Functions
This article introduces two critical Boot Service functions used throughout the example:
LocateHandleBuffer and LocateHandle
Both functions search for handles within the UEFI system, differing primarily in memory management approach.
Function Signatures
typedef
EFI_STATUS
(EFIAPI *EFI_LOCATE_HANDLE)(
IN EFI_LOCATE_SEARCH_TYPE SearchType, // Search method
IN EFI_GUID *Protocol OPTIONAL, // Protocol GUID to locate
IN VOID *SearchKey OPTIONAL, // Search key (related to SearchType)
IN OUT UINTN *BufferSize, // Input: buffer size, Output: required size
OUT EFI_HANDLE *Buffer // Output buffer
);
typedef
EFI_STATUS
(EFIAPI *EFI_LOCATE_HANDLE_BUFFER)(
IN EFI_LOCATE_SEARCH_TYPE SearchType,
IN EFI_GUID *Protocol OPTIONAL,
IN VOID *SearchKey OPTIONAL,
OUT UINTN *NoHandles, // Output: number of handles found
OUT EFI_HANDLE **Buffer // Output: allocated buffer pointer
);LocateHandle Usage Pattern
LocateHandle requires the caller to allocate the buffer. The usage flow proceeds as follows:
First Call: Pass a small BufferSize. The function returns EFI_BUFFER_TOO_SMALL and populates the required size.
UINTN BufferSize = 0;
EFI_HANDLE *Buffer;
// First call to get required size
LocateHandle(ByProtocol, &gEfiDriverBindingProtocolGuid,
NULL, &BufferSize, NULL);
// Allocate memory based on returned size
Buffer = AllocatePool(BufferSize);
// Second call to get actual handles
LocateHandle(ByProtocol, &gEfiDriverBindingProtocolGuid,
NULL, &BufferSize, Buffer);This two-call pattern ensures proper buffer allocation without wasting memory.
LocateHandleBuffer Usage Pattern
LocateHandleBuffer internally allocates the buffer automatically:
UINTN NoHandles;
EFI_HANDLE *Buffer;
// Single call completes the operation
LocateHandleBuffer(ByProtocol, &gEfiDriverBindingProtocolGuid,
NULL, &NoHandles, &Buffer);
// Use Buffer...
FreePool(Buffer); // Don't forget to release!This simplified interface reduces code complexity but requires careful memory management to prevent leaks.
OpenProtocol Function
OpenProtocol obtains protocol interface functions, opening an installed protocol to gain access to its function pointers or data structures.
Function Signature
typedef
EFI_STATUS
(EFIAPI *EFI_OPEN_PROTOCOL)(
IN EFI_HANDLE Handle, // Handle where protocol resides
IN EFI_GUID *Protocol, // Protocol GUID
OUT VOID **Interface OPTIONAL, // Output: protocol interface pointer
IN EFI_HANDLE AgentHandle, // Requester handle (driver image handle)
IN EFI_HANDLE ControllerHandle, // Controller handle
IN UINT32 Attributes // Open mode
);Attribute Modes
The Attributes parameter controls how the protocol opens:
| Mode | Meaning | Typical Scenario | |
|---|---|---|---|
EFI_OPEN_PROTOCOL_BY_DRIVER | Driver exclusive control | When driver begins managing device | |
| `EFI_OPEN_PROTOCOL_BY_DRIVER \ | EFI_OPEN_PROTOCOL_EXCLUSIVE` | Forced exclusive | Exclusive access required |
EFI_OPEN_PROTOCOL_BY_CHILD_CONTROLLER | Child controller open | Bus driver opens for child device | |
EFI_OPEN_PROTOCOL_BY_HANDLE_PROTOCOL | Simple acquisition (no relationship establishment) | Temporary information reading | |
EFI_OPEN_PROTOCOL_GET_PROTOCOL | Get only (no registration to protocol database) | Internal queries |
Simple Usage Example
EFI_STATUS Status;
EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *ConOut;
// Open Console Output Protocol from system table handle
Status = gBS->OpenProtocol(
gST->ConsoleOutHandle, // Handle
&gEfiSimpleTextOutProtocolGuid, // Protocol GUID
(VOID **)&ConOut, // Output interface
gImageHandle, // This driver handle
NULL, // No controller
EFI_OPEN_PROTOCOL_GET_PROTOCOL // Simple acquisition
);Relationship Between OpenProtocol, HandleProtocol, and LocateProtocol
Understanding these three related functions prevents confusion:
- HandleProtocol: A simplified wrapper around
OpenProtocol, usingEFI_OPEN_PROTOCOL_BY_HANDLE_PROTOCOLmode. Suitable for straightforward protocol access. - LocateProtocol: Combines two steps—first locating the handle, then opening the protocol. It directly finds the first handle supporting the specified Protocol in the system and opens it. This convenience comes with less control over the process.
- OpenProtocol: The lowest-level function, enabling precise control over driver-device relationships (who opened whom). Essential for driver start/stop management where relationship tracking matters.
Protocol Reference Counting Mechanism
OpenProtocol implements an important mechanism: protocol reference counting. Each successful OpenProtocol call increments the protocol's reference count. A paired CloseProtocol call must decrement this count. Only when the count reaches zero can the protocol unload.
This mechanism prevents premature protocol卸载 while drivers still depend on it, ensuring system stability.
Complete Example Code
The following complete example demonstrates locating and opening the Graphics Output Protocol:
#include <Uefi.h>
#include <Library/UefiLib.h>
#include <Library/UefiBootServicesTableLib.h>
EFI_STATUS
EFIAPI
UefiMain (
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
) {
EFI_STATUS Status;
UINTN NoHandles;
EFI_HANDLE *Buffer;
EFI_GRAPHICS_OUTPUT_PROTOCOL *GraphicsOutput;
// Locate all handles that support the Graphics Output Protocol
Status = gBS->LocateHandleBuffer(
ByProtocol,
&gEfiGraphicsOutputProtocolGuid,
NULL,
&NoHandles,
&Buffer
);
Print(L"Status of LocateHandleBuffer: %r\n", Status);
if (EFI_ERROR(Status)) {
Print(L"Failed to locate handles for Graphics Output Protocol: %r\n", Status);
return Status;
}
Print(L"Hello, Protocol!\n");
Print(L"Number of handles that support Graphics Output Protocol: %d\n", NoHandles);
// Open the Graphics Output Protocol on the first handle found
Status = gBS->OpenProtocol(
Buffer[0],
&gEfiGraphicsOutputProtocolGuid,
(VOID**)&GraphicsOutput,
ImageHandle,
NULL,
EFI_OPEN_PROTOCOL_GET_PROTOCOL
);
Print(L"Status of OpenProtocol: %r\n", Status);
if (EFI_ERROR(Status)) {
Print(L"Failed to open Graphics Output Protocol: %r\n", Status);
return Status;
}
// At this point, GraphicsOutput pointer can be used to call protocol functions
// For example: GraphicsOutput->QueryMode(), GraphicsOutput->SetMode(), etc.
// Remember to close the protocol when done
gBS->CloseProtocol(
&gEfiGraphicsOutputProtocolGuid,
Buffer[0],
ImageHandle,
NULL
);
// Free the handle buffer
FreePool(Buffer);
return EFI_SUCCESS;
}Code Analysis
This example demonstrates several important patterns:
Error Handling: Each operation checks the returned Status and handles errors appropriately. This defensive programming style proves essential in firmware development where failures can have serious consequences.
Resource Management: The code properly frees allocated memory (FreePool(Buffer)) and closes opened protocols (CloseProtocol). Neglecting these cleanup steps leads to memory leaks and resource exhaustion.
Protocol Usage: After successfully opening the protocol, the GraphicsOutput pointer enables calling all protocol-defined functions such as QueryMode() and SetMode().
Common Protocols in UEFI Development
Essential Protocols
UEFI defines numerous protocols for various system functions. Commonly encountered protocols include:
- Simple Text Output Protocol: Console text display
- Simple Text Input Protocol: Keyboard input handling
- Graphics Output Protocol: Video mode and framebuffer access
- Block I/O Protocol: Block device read/write operations
- Simple File System Protocol: File system access
- Loaded Image Protocol: Information about loaded images
- Device Path Protocol: Device path representation
Protocol Discovery Strategy
When developing UEFI applications, understanding which protocols your system supports proves valuable. The enumeration pattern demonstrated in our example applies to any protocol:
// Generic pattern for protocol enumeration
EFI_GUID *TargetProtocol = &gYourProtocolGuid;
UINTN HandleCount;
EFI_HANDLE *Handles;
Status = gBS->LocateHandleBuffer(
ByProtocol,
TargetProtocol,
NULL,
&HandleCount,
&Handles
);
if (!EFI_ERROR(Status)) {
for (UINTN i = 0; i < HandleCount; i++) {
// Open and use protocol on each handle
// ...
}
FreePool(Handles);
}Best Practices
Always Check Return Status
UEFI functions return status codes indicating success or failure types. Never ignore these returns:
// Bad practice - ignoring status
gBS->OpenProtocol(Handle, &ProtocolGuid, &Interface, ...);
// Good practice - checking status
Status = gBS->OpenProtocol(Handle, &ProtocolGuid, &Interface, ...);
if (EFI_ERROR(Status)) {
// Handle error appropriately
return Status;
}Match OpenProtocol with CloseProtocol
Every successful OpenProtocol requires a corresponding CloseProtocol:
// Open
Status = gBS->OpenProtocol(Handle, &ProtocolGuid, &Interface,
AgentHandle, ControllerHandle, Attributes);
if (!EFI_ERROR(Status)) {
// Use protocol...
// Close when done
gBS->CloseProtocol(&ProtocolGuid, Handle, AgentHandle, ControllerHandle);
}Free Allocated Memory
Memory allocated through UEFI services must be freed:
// Allocate
Buffer = AllocatePool(Size);
// Use buffer...
// Free when done
FreePool(Buffer);Choose Appropriate Open Mode
Select the OpenProtocol attribute matching your use case:
- Use
GET_PROTOCOLfor simple, temporary access - Use
BY_DRIVERwhen your driver manages the device - Use
EXCLUSIVEwhen exclusive access is required
Debugging Tips
Using Debug Print
The Print function provides basic output capability. For more sophisticated debugging, consider:
#ifdef DEBUG
Print(L"Debug: Value = %d\n", Value);
#endifStatus Code Interpretation
UEFI status codes follow conventions:
EFI_SUCCESS(0): Operation succeededEFI_ERRORcodes (non-zero): Various failure conditions- Use
EFI_ERROR(Status)macro to test for failures
Common Failure Modes
- EFI_NOT_FOUND: Protocol or handle doesn't exist
- EFI_ACCESS_DENIED: Insufficient permissions
- EFI_ALREADY_STARTED: Protocol already opened exclusively
- EFI_BUFFER_TOO_SMALL: Provided buffer insufficient (for LocateHandle)
Advanced Topics
Protocol Notification
UEFI supports protocol installation notifications, enabling drivers to respond when new protocols appear:
EFI_VOID *Notification;
Status = gBS->RegisterProtocolNotify(
&gYourProtocolGuid,
YourNotificationFunction,
&Notification
);Multiple Protocol Interfaces
Some handles support multiple protocols. Iterate through all protocols on a handle using ProtocolsPerHandle:
EFI_GUID **Protocols;
UINTN Count;
Status = gBS->ProtocolsPerHandle(Handle, &Protocols, &Count);Driver Binding Protocol
For driver development, the Driver Binding Protocol provides start/stop callbacks for device management. This represents a more advanced pattern beyond simple protocol access.
Conclusion
Understanding how to call Protocol functions in UEFI applications forms a fundamental skill for firmware developers. The pattern—locate handles, open protocol, use interface, close protocol, free resources—applies consistently across all protocol interactions.
Key takeaways from this guide:
- Handles represent objects, Protocols represent capabilities
- LocateHandleBuffer simplifies handle discovery with automatic memory allocation
- OpenProtocol provides flexible access control through attribute modes
- Reference counting ensures protocols remain available while in use
- Proper cleanup prevents resource leaks and system instability
The example code provided offers a template adaptable to any protocol. By understanding these patterns, developers can confidently interact with the rich ecosystem of UEFI protocols, enabling sophisticated firmware functionality.
For further learning, explore the UEFI Specification documentation and examine open-source UEFI implementation code. Practical experimentation with QEMU or physical hardware solidifies these concepts through hands-on experience.
Remember: firmware development demands careful attention to detail. Every allocation requires freeing, every open requires closing, and every status code deserves checking. These disciplines separate reliable firmware from problematic implementations.
Reference: Additional video tutorial available at Bilibili for visual learners seeking complementary explanation of these concepts.