Chapter 16: Block I/O
Access raw storage devices, read disk sectors, and detect partition schemes using the Block I/O Protocol.
16.1 Block I/O Overview
The Block I/O Protocol provides raw sector-level access to storage devices. It sits below the file system layer and is used by file system drivers to read and write data on disk.
graph TD
A[UEFI Application] --> B[EFI_SIMPLE_FILE_SYSTEM_PROTOCOL]
B --> C[FAT Driver]
C --> D[EFI_BLOCK_IO_PROTOCOL]
A --> D
D --> E[EFI_DISK_IO_PROTOCOL]
D --> F[Storage Driver: NVMe / AHCI / USB Mass Storage]
F --> G[Physical Disk Hardware]
subgraph "Partition Handles"
D1[Block I/O - Whole Disk]
D2[Block I/O - Partition 1]
D3[Block I/O - Partition 2]
end
UEFI creates one Block I/O handle for the entire physical disk and one child handle for each detected partition. Each partition handle exposes its own Block I/O instance that maps logical block 0 to the first block of that partition.
16.2 The Block I/O Protocol Interface
typedef struct _EFI_BLOCK_IO_PROTOCOL {
UINT64 Revision;
EFI_BLOCK_IO_MEDIA *Media;
EFI_BLOCK_RESET Reset;
EFI_BLOCK_READ ReadBlocks;
EFI_BLOCK_WRITE WriteBlocks;
EFI_BLOCK_FLUSH FlushBlocks;
} EFI_BLOCK_IO_PROTOCOL;
The Media Descriptor
typedef struct {
UINT32 MediaId; // Current media identifier (changes on media swap)
BOOLEAN RemovableMedia; // TRUE if media is removable
BOOLEAN MediaPresent; // TRUE if media is currently inserted
BOOLEAN LogicalPartition; // TRUE if this is a partition (not whole disk)
BOOLEAN ReadOnly; // TRUE if media is write-protected
BOOLEAN WriteCaching; // TRUE if write caching is enabled
UINT32 BlockSize; // Bytes per block (typically 512 or 4096)
UINT32 IoAlign; // Required buffer alignment (0 or power of 2)
EFI_LBA LastBlock; // LBA of the last block on the device
// Revision 2 fields:
EFI_LBA LowestAlignedLba;
UINT32 LogicalBlocksPerPhysicalBlock;
UINT32 OptimalTransferLengthGranularity;
} EFI_BLOCK_IO_MEDIA;
The
MediaIdfield must be passed to everyReadBlocksandWriteBlockscall. If the media has changed (e.g., a USB drive was swapped), the call will fail withEFI_MEDIA_CHANGED, and you must re-query the media descriptor.
16.3 Enumerating Storage Devices
#include <Uefi.h>
#include <Library/UefiLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Protocol/BlockIo.h>
#include <Protocol/DevicePath.h>
#include <Library/DevicePathLib.h>
EFI_STATUS
ListBlockDevices(VOID)
{
EFI_STATUS Status;
EFI_HANDLE *Handles;
UINTN HandleCount;
EFI_BLOCK_IO_PROTOCOL *BlockIo;
EFI_DEVICE_PATH_PROTOCOL *DevicePath;
Status = gBS->LocateHandleBuffer(
ByProtocol,
&gEfiBlockIoProtocolGuid,
NULL,
&HandleCount,
&Handles
);
if (EFI_ERROR(Status)) {
Print(L"No block devices found: %r\n", Status);
return Status;
}
Print(L"Found %d block I/O device(s):\n\n", HandleCount);
Print(L" %-4s %-10s %-8s %-12s %-10s %s\n",
L"#", L"Type", L"BlkSize", L"Blocks", L"Size(MB)", L"Device Path");
Print(L" %-4s %-10s %-8s %-12s %-10s %s\n",
L"--", L"--------", L"------", L"----------", L"--------", L"-----------");
for (UINTN i = 0; i < HandleCount; i++) {
Status = gBS->HandleProtocol(
Handles[i],
&gEfiBlockIoProtocolGuid,
(VOID **)&BlockIo
);
if (EFI_ERROR(Status)) {
continue;
}
EFI_BLOCK_IO_MEDIA *Media = BlockIo->Media;
if (!Media->MediaPresent) {
Print(L" [%2d] No media present\n", i);
continue;
}
CHAR16 *TypeStr = Media->LogicalPartition ? L"Partition" : L"Disk";
UINT64 SizeMB = ((UINT64)(Media->LastBlock + 1) * Media->BlockSize)
/ (1024 * 1024);
//
// Get the device path string for identification.
//
CHAR16 *PathStr = L"(unknown)";
Status = gBS->HandleProtocol(
Handles[i],
&gEfiDevicePathProtocolGuid,
(VOID **)&DevicePath
);
if (!EFI_ERROR(Status)) {
PathStr = ConvertDevicePathToText(DevicePath, FALSE, FALSE);
}
Print(L" [%2d] %-10s %-8d %-12ld %-10ld %s\n",
i,
TypeStr,
Media->BlockSize,
Media->LastBlock + 1,
SizeMB,
PathStr);
if (PathStr != L"(unknown)") {
FreePool(PathStr);
}
}
gBS->FreePool(Handles);
return EFI_SUCCESS;
}
16.4 Reading Blocks
/**
Read raw sectors from a block device.
@param[in] BlockIo The Block I/O protocol instance.
@param[in] Lba Starting logical block address.
@param[in] Blocks Number of blocks to read.
@param[out] Buffer Caller-allocated buffer (must be aligned per Media->IoAlign).
@retval EFI_SUCCESS Data read successfully.
@retval EFI_DEVICE_ERROR Hardware error.
@retval EFI_MEDIA_CHANGED Media was swapped since last access.
**/
EFI_STATUS
ReadSectors(
IN EFI_BLOCK_IO_PROTOCOL *BlockIo,
IN EFI_LBA Lba,
IN UINTN Blocks,
OUT VOID *Buffer
)
{
UINTN BufferSize = Blocks * BlockIo->Media->BlockSize;
return BlockIo->ReadBlocks(
BlockIo,
BlockIo->Media->MediaId,
Lba,
BufferSize,
Buffer
);
}
16.4.1 Buffer Alignment
The IoAlign field specifies the minimum alignment of the buffer address. If IoAlign is 0 or 1, any alignment is acceptable. Otherwise, the buffer address must be a multiple of IoAlign.
#include <Library/MemoryAllocationLib.h>
VOID *
AllocateAlignedBuffer(
IN EFI_BLOCK_IO_MEDIA *Media,
IN UINTN Size
)
{
if (Media->IoAlign <= 1) {
return AllocatePool(Size);
}
return AllocateAlignedPages(
EFI_SIZE_TO_PAGES(Size),
Media->IoAlign
);
}
In practice, allocating with AllocatePages or AllocateAlignedPages guarantees page alignment (4 KB), which satisfies any realistic IoAlign requirement.
16.5 Writing Blocks
EFI_STATUS
WriteSectors(
IN EFI_BLOCK_IO_PROTOCOL *BlockIo,
IN EFI_LBA Lba,
IN UINTN Blocks,
IN VOID *Buffer
)
{
EFI_STATUS Status;
UINTN BufferSize = Blocks * BlockIo->Media->BlockSize;
if (BlockIo->Media->ReadOnly) {
Print(L"Error: media is read-only.\n");
return EFI_WRITE_PROTECTED;
}
Status = BlockIo->WriteBlocks(
BlockIo,
BlockIo->Media->MediaId,
Lba,
BufferSize,
Buffer
);
if (EFI_ERROR(Status)) {
return Status;
}
//
// Flush to ensure data reaches the physical media.
//
return BlockIo->FlushBlocks(BlockIo);
}
Writing to a block device bypasses the file system. Incorrect writes can destroy partition tables, file system metadata, or boot records. Always verify the target device and LBA before writing.
16.6 Detecting GPT and MBR Partitions
16.6.1 Reading the MBR
The Master Boot Record occupies LBA 0 and contains a partition table at offset 446.
#pragma pack(1)
typedef struct {
UINT8 BootIndicator;
UINT8 StartHead;
UINT8 StartSector;
UINT8 StartTrack;
UINT8 OSType;
UINT8 EndHead;
UINT8 EndSector;
UINT8 EndTrack;
UINT32 StartingLBA;
UINT32 SizeInLBA;
} MBR_PARTITION_ENTRY;
typedef struct {
UINT8 BootCode[440];
UINT32 DiskSignature;
UINT16 Reserved;
MBR_PARTITION_ENTRY Partitions[4];
UINT16 Signature; // Must be 0xAA55
} MASTER_BOOT_RECORD;
#pragma pack()
EFI_STATUS
ReadMbr(
IN EFI_BLOCK_IO_PROTOCOL *BlockIo,
OUT MASTER_BOOT_RECORD *Mbr
)
{
EFI_STATUS Status;
UINT8 SectorBuffer[512];
Status = BlockIo->ReadBlocks(
BlockIo,
BlockIo->Media->MediaId,
0, // LBA 0
512,
SectorBuffer
);
if (EFI_ERROR(Status)) {
return Status;
}
CopyMem(Mbr, SectorBuffer, sizeof(MASTER_BOOT_RECORD));
if (Mbr->Signature != 0xAA55) {
return EFI_NOT_FOUND; // Not a valid MBR
}
return EFI_SUCCESS;
}
VOID
PrintMbrPartitions(
IN MASTER_BOOT_RECORD *Mbr
)
{
Print(L"\nMBR Partition Table:\n");
Print(L" %-4s %-8s %-12s %-12s %s\n",
L"#", L"Type", L"Start LBA", L"Size (LBA)", L"Boot");
for (UINTN i = 0; i < 4; i++) {
MBR_PARTITION_ENTRY *Entry = &Mbr->Partitions[i];
if (Entry->OSType == 0) {
continue; // Empty entry
}
Print(L" [%d] 0x%02x %-12d %-12d %s\n",
i,
Entry->OSType,
Entry->StartingLBA,
Entry->SizeInLBA,
Entry->BootIndicator == 0x80 ? L"Active" : L"");
}
}
16.6.2 Detecting GPT
GPT (GUID Partition Table) disks have a protective MBR at LBA 0 and a GPT header at LBA 1. The protective MBR contains a single partition entry of type 0xEE.
#pragma pack(1)
typedef struct {
EFI_TABLE_HEADER Header; // Signature = "EFI PART"
UINT32 MyLBA_Lo;
UINT32 MyLBA_Hi;
UINT32 AlternateLBA_Lo;
UINT32 AlternateLBA_Hi;
UINT32 FirstUsableLBA_Lo;
UINT32 FirstUsableLBA_Hi;
UINT32 LastUsableLBA_Lo;
UINT32 LastUsableLBA_Hi;
EFI_GUID DiskGUID;
UINT32 PartitionEntryLBA_Lo;
UINT32 PartitionEntryLBA_Hi;
UINT32 NumberOfPartitionEntries;
UINT32 SizeOfPartitionEntry;
UINT32 PartitionEntryArrayCRC32;
} GPT_HEADER;
#pragma pack()
#define GPT_HEADER_SIGNATURE 0x5452415020494645ULL // "EFI PART"
BOOLEAN
IsGptDisk(
IN EFI_BLOCK_IO_PROTOCOL *BlockIo
)
{
EFI_STATUS Status;
UINT8 Buffer[512];
//
// Read LBA 1 where the GPT header lives.
//
Status = BlockIo->ReadBlocks(
BlockIo,
BlockIo->Media->MediaId,
1, // LBA 1
512,
Buffer
);
if (EFI_ERROR(Status)) {
return FALSE;
}
//
// Check for the "EFI PART" signature at the start of the header.
//
UINT64 *Signature = (UINT64 *)Buffer;
return (*Signature == GPT_HEADER_SIGNATURE);
}
In practice, you rarely need to parse GPT structures manually. UEFI firmware automatically creates child Block I/O handles for each GPT partition. The
EFI_PARTITION_INFO_PROTOCOL(available on partition handles) provides parsed partition type GUIDs and attributes.
16.7 The Disk I/O Protocol
While Block I/O works in whole-block units, the EFI_DISK_IO_PROTOCOL allows byte-granularity reads and writes at arbitrary offsets. It is automatically installed alongside Block I/O.
#include <Protocol/DiskIo.h>
EFI_STATUS
ReadBytesFromDisk(
IN EFI_HANDLE DeviceHandle,
IN UINT64 Offset,
IN UINTN Size,
OUT VOID *Buffer
)
{
EFI_STATUS Status;
EFI_DISK_IO_PROTOCOL *DiskIo;
EFI_BLOCK_IO_PROTOCOL *BlockIo;
Status = gBS->HandleProtocol(
DeviceHandle,
&gEfiDiskIoProtocolGuid,
(VOID **)&DiskIo
);
if (EFI_ERROR(Status)) {
return Status;
}
Status = gBS->HandleProtocol(
DeviceHandle,
&gEfiBlockIoProtocolGuid,
(VOID **)&BlockIo
);
if (EFI_ERROR(Status)) {
return Status;
}
return DiskIo->ReadDisk(
DiskIo,
BlockIo->Media->MediaId,
Offset,
Size,
Buffer
);
}
16.8 Block I/O 2: Asynchronous Operations
The Block I/O 2 Protocol (EFI_BLOCK_IO2_PROTOCOL) supports non-blocking reads and writes. This is useful for overlapping I/O with computation.
#include <Protocol/BlockIo2.h>
EFI_STATUS
AsyncReadExample(
IN EFI_HANDLE DeviceHandle
)
{
EFI_STATUS Status;
EFI_BLOCK_IO2_PROTOCOL *BlockIo2;
EFI_BLOCK_IO2_TOKEN Token;
UINT8 Buffer[4096];
Status = gBS->HandleProtocol(
DeviceHandle,
&gEfiBlockIo2ProtocolGuid,
(VOID **)&BlockIo2
);
if (EFI_ERROR(Status)) {
Print(L"Block I/O 2 not available: %r\n", Status);
return Status;
}
//
// Create a completion event.
//
Status = gBS->CreateEvent(0, 0, NULL, NULL, &Token.Event);
if (EFI_ERROR(Status)) {
return Status;
}
Token.TransactionStatus = EFI_SUCCESS;
//
// Start asynchronous read.
//
Status = BlockIo2->ReadBlocksEx(
BlockIo2,
BlockIo2->Media->MediaId,
0, // LBA 0
&Token,
sizeof(Buffer),
Buffer
);
if (EFI_ERROR(Status)) {
gBS->CloseEvent(Token.Event);
return Status;
}
//
// Do other work here while I/O is in progress...
//
Print(L"I/O submitted, doing other work...\n");
//
// Wait for completion.
//
UINTN EventIndex;
gBS->WaitForEvent(1, &Token.Event, &EventIndex);
Print(L"I/O completed with status: %r\n", Token.TransactionStatus);
gBS->CloseEvent(Token.Event);
return Token.TransactionStatus;
}
16.9 Practical Example: Hex Dump of a Disk Sector
#include <Uefi.h>
#include <Library/UefiLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/MemoryAllocationLib.h>
#include <Library/BaseMemoryLib.h>
#include <Protocol/BlockIo.h>
VOID
HexDump(
IN UINT8 *Data,
IN UINTN Size
)
{
for (UINTN Offset = 0; Offset < Size; Offset += 16) {
Print(L" %04x: ", Offset);
// Hex bytes
for (UINTN j = 0; j < 16 && (Offset + j) < Size; j++) {
Print(L"%02x ", Data[Offset + j]);
}
// ASCII representation
Print(L" |");
for (UINTN j = 0; j < 16 && (Offset + j) < Size; j++) {
UINT8 Ch = Data[Offset + j];
Print(L"%c", (Ch >= 0x20 && Ch <= 0x7E) ? Ch : L'.');
}
Print(L"|\n");
}
}
EFI_STATUS
DumpSector(
IN EFI_BLOCK_IO_PROTOCOL *BlockIo,
IN EFI_LBA Lba
)
{
EFI_STATUS Status;
UINTN BlockSize = BlockIo->Media->BlockSize;
UINT8 *Buffer;
Buffer = AllocatePool(BlockSize);
if (Buffer == NULL) {
return EFI_OUT_OF_RESOURCES;
}
Status = BlockIo->ReadBlocks(
BlockIo,
BlockIo->Media->MediaId,
Lba,
BlockSize,
Buffer
);
if (EFI_ERROR(Status)) {
Print(L"ReadBlocks failed at LBA %ld: %r\n", Lba, Status);
FreePool(Buffer);
return Status;
}
Print(L"\nSector at LBA %ld (Block Size = %d bytes):\n\n", Lba, BlockSize);
HexDump(Buffer, BlockSize);
FreePool(Buffer);
return EFI_SUCCESS;
}
16.10 Distinguishing Whole Disks from Partitions
When enumerating Block I/O handles, you often need to distinguish physical disks from partition handles:
BOOLEAN
IsWholeDisk(
IN EFI_BLOCK_IO_PROTOCOL *BlockIo
)
{
//
// The Media->LogicalPartition field is FALSE for the raw disk
// and TRUE for partition child handles.
//
return !BlockIo->Media->LogicalPartition;
}
You can also use device path inspection to determine the disk type (NVMe, SATA, USB, etc.) and partition number by examining the last node of the device path.
Complete source code: The full working example for this chapter is available at
examples/UefiMuGuidePkg/BlockIoExample/.
Summary
| Concept | Key Points |
|---|---|
| Block I/O | Sector-level access; one handle per disk and per partition |
| MediaId | Must match current media; check for EFI_MEDIA_CHANGED |
| ReadBlocks | Reads whole blocks; buffer must satisfy IoAlign |
| WriteBlocks | Bypasses file system; use with extreme caution |
| MBR/GPT | MBR at LBA 0, GPT header at LBA 1; firmware auto-creates partition handles |
| Disk I/O | Byte-granularity access layered on top of Block I/O |
| Block I/O 2 | Asynchronous variant for non-blocking operations |
| LogicalPartition | TRUE for partition handles, FALSE for whole-disk handles |
In the next chapter, we move up to the network stack and explore UEFI’s layered networking architecture.