笔者研究了一下磁盘相关的 Protocol,本文描述了uEFI下如何获取磁盘信息。
简介 #
与本次探究相关的主要有三个 Protocol:EFI_BLOCK_IO_PROTOCOL
、EFI_DISK_IO_PROTOCOL
以及 EFI_DISK_INFO_PROTOCOL
。前两个是在 UEFI SPEC 定义的,最后一个则是在 PI SPEC。
BlockIo 与 DiskIo #
BlockIo
与 DiskIo
两个Protocol 均是用于访问存储设备的协议,只是它们可以进行操作的级别不同。前者能以 块
的级别对存储设备进行操作,而后者则提供更加底层的能力,它可以以 字节
为单位对设备进行访问。
DiskInfo #
这两个Protocol 的主要目的是提供一种标准化的方式来获取磁盘设备的信息。这些信息可能包括磁盘的类型
、制造商
、序列号
、固件版本
等。
实例 #
描述 #
写一个程序,枚举出所有物理磁盘,并打印磁盘的型号,SN 以及容量大小。
思路 #
使用 BlockIo
获取所有块设备的实例,然后再使用 DiskIo
进行筛选,得到所有的物理磁盘。接着将物理磁盘的实例传给 DiskInfo,通过 Identify
函数可获取 Identify Data
,再根据磁盘的接口类型对数据进行解析,便能打印型号等信息。
代码 #
/**
* @file DiskInfo.c
*
* @version 0.2
* @date 2024-09-09
*
* @copyright Copyright (c) 2015 - 2024
*
*/
#include <Uefi.h>
#include <Library/PcdLib.h>
#include <Library/UefiLib.h>
#include <Library/UefiApplicationEntryPoint.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/MemoryAllocationLib.h>
#include <Protocol/BlockIo.h>
#include <Protocol/DiskIo.h>
#include <Protocol/DiskInfo.h>
#include <Library/BaseMemoryLib.h>
#include <Library/PrintLib.h>
#include <Protocol/IdeControllerInit.h>
VOID HexDump (UINT8 *Buffer, UINT8 RowNum)
{
UINT8 Cols;
UINT8 Rows;
for (Rows = 0; Rows < RowNum; Rows ++) {
for (Cols = 0; Cols < 16; Cols ++) {
Print (L"%2X ", Buffer[Cols+Rows*16]);
}
Print (L" ");
for (Cols = 0; Cols < 16; Cols ++) {
if ((Buffer[Cols+Rows*16] >= '0' && Buffer[Cols+Rows*16] <= '9') ||
(Buffer[Cols+Rows*16] >= 'a' && Buffer[Cols+Rows*16] <= 'z') ||
(Buffer[Cols+Rows*16] >= 'A' && Buffer[Cols+Rows*16] <= 'Z'))
Print (L"%c", Buffer[Cols+Rows*16]);
else
Print (L".");
}
Print (L"\n\r");
}
Print (L"\n\r");
}
/**
Eliminate the extra spaces in the Str to one space.
@param Str Input string info.
**/
VOID
BmEliminateExtraSpaces (
IN CHAR16 *Str
)
{
UINTN Index;
UINTN ActualIndex;
for (Index = 0, ActualIndex = 0; Str[Index] != L'\0'; Index++) {
if ((Str[Index] != L' ') || ((ActualIndex > 0) && (Str[ActualIndex - 1] != L' '))) {
Str[ActualIndex++] = Str[Index];
}
}
Str[ActualIndex] = L'\0';
}
EFI_STATUS
EFIAPI
GetDiskIdentifyData (EFI_HANDLE Handle)
{
EFI_STATUS Status;
EFI_DISK_INFO_PROTOCOL *DiskInfo;
EFI_ATAPI_IDENTIFY_DATA IdentifyData;
UINT32 IdentifyDataSize;
CHAR16 *ModelName;
CHAR16 *SerialNo;
CONST UINTN ModelNameLength = 40;
CONST UINTN SerialNoLength = 20;
UINTN Index;
Status = gBS->HandleProtocol(Handle, &gEfiDiskInfoProtocolGuid, (VOID**)&DiskInfo);
if (EFI_ERROR(Status)) {
return Status;
}
//
// AHCI or IDE
//
if (CompareGuid (&DiskInfo->Interface, &gEfiDiskInfoAhciInterfaceGuid) ||
CompareGuid (&DiskInfo->Interface, &gEfiDiskInfoIdeInterfaceGuid)) {
IdentifyDataSize = sizeof (EFI_ATAPI_IDENTIFY_DATA);
Status = DiskInfo->Identify(DiskInfo, &IdentifyData, &IdentifyDataSize);
if (!EFI_ERROR (Status)) {
ModelName = AllocatePool(sizeof(CHAR16) * ModelNameLength);
SerialNo = AllocatePool(sizeof(CHAR16) * SerialNoLength);
// According to the ATA specification, the model name and serial number fields
// in the identify data are stored as an array of 16-bit words.
// Each word is stored in little-endian format, meaning the least significant byte comes first.
for (Index = 0; Index + 1 < ModelNameLength; Index += 2) {
ModelName[Index] = (CHAR16) IdentifyData.ModelName[Index + 1];
ModelName[Index + 1] = (CHAR16) IdentifyData.ModelName[Index];
}
for (Index = 0; Index + 1 < SerialNoLength; Index += 2) {
SerialNo[Index] = (CHAR16) IdentifyData.SerialNo[Index + 1];
SerialNo[Index + 1] = (CHAR16) IdentifyData.SerialNo[Index];
}
BmEliminateExtraSpaces(ModelName);
BmEliminateExtraSpaces(SerialNo);
HexDump((UINT8 *)&IdentifyData, 16);
Print (L"Model Name: %s\n\r", ModelName);
Print (L"Serial No : %s\n\r", SerialNo);
Print (L"Disk type: %g\n\r", DiskInfo->Interface);
}
else {
return Status;
}
}
return EFI_SUCCESS;
}
/**
The user Entry Point for Application. The user code starts with this function
as the real entry point for the application.
@param[in] ImageHandle The firmware allocated handle for the EFI image.
@param[in] SystemTable A pointer to the EFI System Table.
@retval EFI_SUCCESS The entry point is executed successfully.
@retval other Some error occurs when executing this entry point.
**/
EFI_STATUS
EFIAPI
UefiMain (
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
)
{
EFI_STATUS Status;
UINTN HandleCount = 0;
EFI_HANDLE *HandleBuffer = NULL;
UINTN Index;
EFI_BLOCK_IO_PROTOCOL *BlockIo;
EFI_DISK_IO_PROTOCOL *DiskIo;
Status = gBS->LocateHandleBuffer(
ByProtocol,
&gEfiBlockIoProtocolGuid,
NULL,
&HandleCount,
&HandleBuffer
);
if (EFI_ERROR(Status)) {
Print(L"Failed to locate block I/O handles: %r\n", Status);
return Status;
}
for (Index = 0; Index < HandleCount; Index++) {
Status = gBS->HandleProtocol(
HandleBuffer[Index],
&gEfiBlockIoProtocolGuid,
(VOID**)&BlockIo
);
if (EFI_ERROR(Status) || BlockIo == NULL || BlockIo->Media == NULL) {
continue;
}
Status = gBS->HandleProtocol(
HandleBuffer[Index],
&gEfiDiskIoProtocolGuid,
(VOID**)&DiskIo
);
if (!EFI_ERROR(Status) && DiskIo != NULL && !BlockIo->Media->RemovableMedia) {
if (BlockIo->Media->LogicalPartition == FALSE &&
BlockIo->Media->BlockSize > 0 &&
BlockIo->Media->LastBlock > 0) {
GetDiskIdentifyData(HandleBuffer[Index]);
Print (L"Size : %d MB\n\r", (BlockIo->Media->LastBlock + 1) * BlockIo->Media->BlockSize / (1024 * 1024));
Print(L"\n\r");
}
}
}
if (HandleBuffer != NULL) {
gBS->FreePool(HandleBuffer);
}
return EFI_SUCCESS;
}
上面代码参考了 MdeModulePkg\Library\UefiBootManagerLib\BmBootDescription.c
,其中 BmGetDescriptionFromDiskInfo
函数的实现,需要注意的是,EDK2
默认的代码中只对 IDE / AHCI
两种有解析的定义,对于 NVME
、SD
之类的还没有,所以只写了这一部分。
代码中有个解析算法也值得说一下:
// According to the ATA specification, the model name and serial number fields
// in the identify data are stored as an array of 16-bit words.
// Each word is stored in little-endian format, meaning the least significant byte comes first.
for (Index = 0; Index + 1 < ModelNameLength; Index += 2) {
ModelName[Index] = (CHAR16) IdentifyData.ModelName[Index + 1];
ModelName[Index + 1] = (CHAR16) IdentifyData.ModelName[Index];
}
for (Index = 0; Index + 1 < SerialNoLength; Index += 2) {
SerialNo[Index] = (CHAR16) IdentifyData.SerialNo[Index + 1];
SerialNo[Index + 1] = (CHAR16) IdentifyData.SerialNo[Index];
}
在ATA规范中,型号名称和序列号等字段存储为16位的数组。每个16位字以小端格式存储,这意味着最低有效字节(LSB)先存储,然后是最高有效字节(MSB)。