找回密码
 立即注册→加入我们

QQ登录

只需一步,快速开始

搜索
热搜: 下载 VB C 实现 编写
查看: 3964|回复: 1

【UEFI】用UEFI的磁盘I/O协议接口解析硬盘分区表

[复制链接]

65

主题

116

回帖

1万

积分

用户组: 超级版主

OS与VM研究学者

UID
1043
精华
35
威望
789 点
宅币
8301 个
贡献
1094 次
宅之契约
0 份
在线时间
2068 小时
注册时间
2015-8-15
发表于 2021-3-11 10:17:33 | 显示全部楼层 |阅读模式

欢迎访问技术宅的结界,请注册或者登录吧。

您需要 登录 才可以下载或查看,没有账号?立即注册→加入我们

×

前言

写OS必不可少的就是获取硬盘的分区信息。本文以UEFI的磁盘I/O协议为例解析磁盘分区表。
和以前我玩UEFI的方法一样,编译照样用LLVM,编译EDK的库还是用我自己的非标准方法。
本文的程序不仅能解析MBR(主要)分区表,也能解析GPT分区表。

第一步:枚举所有磁盘

磁盘I/O协议对应的GUID是:

#define EFI_BLOCK_IO_PROTOCOL_GUID \
  { \
    0x964e5b21, 0x6459, 0x11d2, {0x8e, 0x39, 0x0, 0xa0, 0xc9, 0x69, 0x72, 0x3b } \
  }

这里用Block I/O Protocol的原因其实是因为它的内容比较丰富,尽管操作起来有些蛋疼,访问起来得按扇区大小对齐,但是由于分区表正好都在扇区的头上,分区表里的玩意不少都是LBA,因此麻烦就省了。
虽然还有一种访问磁盘的协议叫Disk I/O Protocol,不过两种协议都要MediaId参数,而这个值可以从Block I/O Protocol里拿,但Disk I/O Protocol里拿不到,还得回来在Block I/O Protocol里拿。
SystemTable->BootServices里有个函数叫LocateHandleBuffer,这个函数能枚举出所有支持该协议的所有句柄。其函数原型为:

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,
  OUT    EFI_HANDLE                   **Buffer
  );

我们要的是所有支持磁盘I/O协议的句柄,因此SearchTypeByProtocolSearchKeyNULL,然后Protocol填磁盘I/O的GUID。
NoHandles会返回支持的句柄数量,而Buffer会返回句柄数组。别忘了释放掉。
有一个相似的函数叫LocateHandle,功能差不多,但是要自己分配数组内存,略蛋疼,除非你有特别规划好的内存池,否则不建议用LocateHandle函数。
拿到句柄之后,要拿到磁盘服务,这里拿两个玩意:Block I/O ProtocolDevice Path Protocol
其中Device Path Protocol里面的内容是设备路径,不过不是字符串,是一种挺特别的结构。
初始化磁盘I/O的代码如下:

EFI_STATUS InitializeDiskIoProtocol()
{
    UINTN BuffCount=0;
    EFI_HANDLE *HandleBuffer=NULL;
    // Locate all devices that support Disk I/O Protocol.
    EFI_STATUS st=gBS->LocateHandleBuffer(ByProtocol,&gEfiBlockIoProtocolGuid,NULL,&BuffCount,&HandleBuffer);
    if(st==EFI_SUCCESS)
    {
        DiskDevices=AllocateZeroPool(sizeof(DISK_DEVICE_OBJECT)*BuffCount);
        if(DiskDevices)
        {
            NumberOfDiskDevices=BuffCount;
            for(UINTN i=0;i<BuffCount;i++)
            {
                DiskDevices[i].DevicePath=DevicePathFromHandle(HandleBuffer[i]);
                gBS->HandleProtocol(HandleBuffer[i],&gEfiBlockIoProtocolGuid,&DiskDevices[i].BlockIo);
                if(HandleBuffer[i]==CurrentImage->DeviceHandle)
                {
                    CHAR16* DevPath=ConvertDevicePathToText(DiskDevices[i].DevicePath,FALSE,FALSE);
                    if(DevPath)
                    {
                        Print(L"Image was loaded from Disk Device: %s\r\n",DevPath);
                        FreePool(DevPath);
                    }
                    CurrentDiskDevice=&DiskDevices[i];
                }
            }
        }
        else
        {
            st=EFI_OUT_OF_RESOURCES;
            StdOut->OutputString(StdOut,L"Failed to build list of Disk Devices!\r\n");
        }
        FreePool(HandleBuffer);
    }
    else
        Print(L"Failed to locate Disk I/O handles! Status=0x%p\n",st);
    return st;
}

第二步:区分磁盘性质

刚才给出的代码枚举出了所有的磁盘设备,但磁盘设备里不光是整个的硬盘,就连硬盘里的分区也会被视为磁盘设备,因为里面的分区也支持Block I/O Protocol
这里看一下EFI_BLOCK_IO_PROTOCOL结构体的定义:

struct _EFI_BLOCK_IO_PROTOCOL {
  ///
  /// The revision to which the block IO interface adheres. All future
  /// revisions must be backwards compatible. If a future version is not
  /// back wards compatible, it is not the same GUID.
  ///
  UINT64              Revision;
  ///
  /// Pointer to the EFI_BLOCK_IO_MEDIA data for this device.
  ///
  EFI_BLOCK_IO_MEDIA  *Media;

  EFI_BLOCK_RESET     Reset;
  EFI_BLOCK_READ      ReadBlocks;
  EFI_BLOCK_WRITE     WriteBlocks;
  EFI_BLOCK_FLUSH     FlushBlocks;

};

我们会用ReadBlocks函数读磁盘。结构体的Media里有这个磁盘的详细信息:

typedef struct {
  ///
  /// The curent media Id. If the media changes, this value is changed.
  ///
  UINT32  MediaId;

  ///
  /// TRUE if the media is removable; otherwise, FALSE.
  ///
  BOOLEAN RemovableMedia;

  ///
  /// TRUE if there is a media currently present in the device;
  /// othersise, FALSE. THis field shows the media present status
  /// as of the most recent ReadBlocks() or WriteBlocks() call.
  ///
  BOOLEAN MediaPresent;

  ///
  /// TRUE if LBA 0 is the first block of a partition; otherwise
  /// FALSE. For media with only one partition this would be TRUE.
  ///
  BOOLEAN LogicalPartition;

  ///
  /// TRUE if the media is marked read-only otherwise, FALSE.
  /// This field shows the read-only status as of the most recent WriteBlocks () call.
  ///
  BOOLEAN ReadOnly;

  ///
  /// TRUE if the WriteBlock () function caches write data.
  ///
  BOOLEAN WriteCaching;

  ///
  /// The intrinsic block size of the device. If the media changes, then
  /// this field is updated.
  ///
  UINT32  BlockSize;

  ///
  /// Supplies the alignment requirement for any buffer to read or write block(s).
  ///
  UINT32  IoAlign;

  ///
  /// The last logical block address on the device.
  /// If the media changes, then this field is updated.
  ///
  EFI_LBA LastBlock;

  ///
  /// Only present if EFI_BLOCK_IO_PROTOCOL.Revision is greater than or equal to
  /// EFI_BLOCK_IO_PROTOCOL_REVISION2. Returns the first LBA is aligned to
  /// a physical block boundary.
  ///
  EFI_LBA LowestAlignedLba;

  ///
  /// Only present if EFI_BLOCK_IO_PROTOCOL.Revision is greater than or equal to
  /// EFI_BLOCK_IO_PROTOCOL_REVISION2. Returns the number of logical blocks
  /// per physical block.
  ///
  UINT32 LogicalBlocksPerPhysicalBlock;

  ///
  /// Only present if EFI_BLOCK_IO_PROTOCOL.Revision is greater than or equal to
  /// EFI_BLOCK_IO_PROTOCOL_REVISION3. Returns the optimal transfer length
  /// granularity as a number of logical blocks.
  ///
  UINT32 OptimalTransferLengthGranularity;
} EFI_BLOCK_IO_MEDIA;

其中LogicalPartition可以区分这个Block I/O Protocol是否为逻辑分区。而MediaPresent可以确认这个磁盘是否仍然在位。不在位的磁盘暂时是读不出东西的,因此不在位的磁盘也要跳过。
代码如下:

void EnumAllDiskPartitions()
{
    for(UINTN i=0;i<NumberOfDiskDevices;i++)
    {
        // Skip absent media and partition media.
        if(DiskDevices[i].BlockIo->Media->MediaPresent && !DiskDevices[i].BlockIo->Media->LogicalPartition)
        {
            CHAR16 *DiskDevicePath=ConvertDevicePathToText(DiskDevices[i].DevicePath,FALSE,FALSE);
            if(DiskDevicePath)
            {
                StdOut->OutputString(StdOut,L"=============================================================================\r\n");
                Print(L"Partition Info of Device Path: %s\n",DiskDevicePath);
                FreePool(DiskDevicePath);
                Print(L"Block Size: %d bytes. I/O Alignment: 0x%X. Last LBA: 0x%llX.\n",DiskDevices[i].BlockIo->Media->BlockSize,DiskDevices[i].BlockIo->Media->IoAlign,DiskDevices[i].BlockIo->Media->LastBlock);
                EnumDiskPartitions(DiskDevices[i].BlockIo);
            }
        }
    }
    StdOut->OutputString(StdOut,L"=============================================================================\r\n");
}

第三步:根据磁盘设备解析分区表

我认为UEFI的文档里有关分区表的描述是最为权威的。但不可否认的是,维基百科的内容确实比UEFI文档里的内容多。
在论坛上,坛主曾经也发过GPT分区表资料MBR分区表资料
这里还是简单说一下步骤:
第一步:读取LBA0,即第零扇区,这里存着MBR,即主启动记录。不用去鸟MBR头上存着的系统启动代码,直接去解析那里面存着的分区表。里面就四个玩意,如果里面的OSIndicator不为零则该分区有效。如果它的值是0xEE或者是0xEF,那就是GPT分区,通常是前者。
第二步:按照UEFI的文档,OSIndicator的值定义如下:

#define PMBR_GPT_PARTITION          0xEE
#define EFI_PARTITION               0xEF

前者的意思是GPT Protective,按照UEFI的规定,GPT Protective的分区应当覆盖整个磁盘。而后者的意思是UEFI System Partition,但没有具体解释。我认为所谓“混合MBR+GPT”的磁盘里,给GPT分区的就只能是后者,而不可以是前者。
如果该分区描述的是GPT分区表,则StartingLBA域存的是GPT表头所在的扇区。直接把它传给ReadBlocks函数去读就完事了。其函数原型如下:

typedef
EFI_STATUS
(EFIAPI *EFI_BLOCK_READ)(
  IN EFI_BLOCK_IO_PROTOCOL          *This,
  IN UINT32                         MediaId,
  IN EFI_LBA                        Lba,
  IN UINTN                          BufferSize,
  OUT VOID                          *Buffer
  );

别的没啥好说的,也就是那个意思,其中那个MediaId参数可以去EFI_BLOCK_IO_MEDIA结构体里拿。
读到GPT表头后,可以依据PartitionEntryLBA域取得存放分区表项数组的扇区起始位置。而NumberOfPartitionEntries域存放表项数量,这个地方一般都是128,原则上必须是2的整数次幂且大于等于128。
读到表项数组之后,需要判断每个表项的PartitionTypeGUID域,如果为全零则表示该表项不使用。那么就得跳过。这个域和MBR中的OSIndicator很是相像,不过GPT的分区类型比MBR更为自由,写OS的话可以直接用GUID生成器给自己分配一个专属的分区类型GUID。
UniquePartitionGUID用于标识一个分区。原则上GUID是不会冲突的,但要是冲突了,那可就很尴尬了,固件或者操作系统处理分区的行为就无法确定了。
StartingLBAEndingLBA两个域标识分区的起始扇区和终止扇区。依据小学数学的种树原理,这个分区的总扇区数是EndingLBA-StartingLBA+1
说到这,代码如下:

EFI_STATUS EnumDiskPartitions(IN EFI_BLOCK_IO_PROTOCOL *BlockIoProtocol)
{
    EFI_STATUS st=EFI_DEVICE_ERROR;
    if(!BlockIoProtocol->Media->LogicalPartition)
    {
        MASTER_BOOT_RECORD MBRContent;
        st=BlockIoProtocol->ReadBlocks(BlockIoProtocol,BlockIoProtocol->Media->MediaId,0,sizeof(MASTER_BOOT_RECORD),&MBRContent);
        if(st==EFI_SUCCESS)
        {
            if(MBRContent.Signature!=MBR_SIGNATURE)
                StdOut->OutputString(StdOut,L"Invalid MBR Signature! MBR might be corrupted!\r\n");
            for(UINT8 i=0;i<MAX_MBR_PARTITIONS;i++)
            {
                MBR_PARTITION_RECORD *Partition=&MBRContent.Partition[i];
                if(Partition->OSIndicator)
                {
                    UINT32 StartLBA=*(UINT32*)Partition->StartingLBA;
                    UINT32 SizeInLBA=*(UINT32*)Partition->SizeInLBA;
                    CHAR16 ScaledStart[32],ScaledSize[32];
                    DisplaySize(__emulu(StartLBA,BlockIoProtocol->Media->BlockSize),ScaledStart,sizeof(ScaledStart));
                    DisplaySize(__emulu(SizeInLBA,BlockIoProtocol->Media->BlockSize),ScaledSize,sizeof(ScaledSize));
                    Print(L"MBR-Defined Partition %d: OS Type: 0x%02X  Start Position: %s  Partition Size: %s\n",i,Partition->OSIndicator,ScaledStart,SizeInLBA==0xFFFFFFFF?L"Over 2TiB":ScaledSize);
                    if(Partition->OSIndicator==PMBR_GPT_PARTITION || Partition->OSIndicator==EFI_PARTITION)
                    {
                        EFI_PARTITION_TABLE_HEADER *GptHeader=AllocatePool(BlockIoProtocol->Media->BlockSize);
                        if(GptHeader)
                        {
                            st=BlockIoProtocol->ReadBlocks(BlockIoProtocol,BlockIoProtocol->Media->MediaId,StartLBA,BlockIoProtocol->Media->BlockSize,GptHeader);
                            if(st==EFI_SUCCESS)
                            {
                                if(GptHeader->Header.Signature!=EFI_PTAB_HEADER_ID)
                                    StdOut->OutputString(StdOut,L"Improper GPT Header Signature!");
                                else
                                {
                                    UINT32 PartitionEntrySize=GptHeader->SizeOfPartitionEntry*GptHeader->NumberOfPartitionEntries;
                                    VOID* PartitionEntries=AllocatePool(PartitionEntrySize);
                                    Print(L"Disk GUID: {%g}  Partition Array LBA: %u  Number of Partitions: %u\n",&GptHeader->DiskGUID,GptHeader->PartitionEntryLBA,GptHeader->NumberOfPartitionEntries);
                                    if(PartitionEntries)
                                    {
                                        st=BlockIoProtocol->ReadBlocks(BlockIoProtocol,BlockIoProtocol->Media->MediaId,GptHeader->PartitionEntryLBA,PartitionEntrySize,PartitionEntries);
                                        if(st==EFI_SUCCESS)
                                        {
                                            for(UINT32 j=0;j<GptHeader->NumberOfPartitionEntries;j++)
                                            {
                                                EFI_PARTITION_ENTRY *PartitionEntry=(EFI_PARTITION_ENTRY*)((UINTN)PartitionEntries+j*GptHeader->SizeOfPartitionEntry);
                                                if(EfiCompareGuid(&PartitionEntry->PartitionTypeGUID,&gEfiPartTypeUnusedGuid))
                                                {
                                                    DisplaySize(MultU64x32(PartitionEntry->StartingLBA,BlockIoProtocol->Media->BlockSize),ScaledStart,sizeof(ScaledStart));
                                                    DisplaySize(MultU64x32(PartitionEntry->EndingLBA-PartitionEntry->StartingLBA+1,BlockIoProtocol->Media->BlockSize),ScaledSize,sizeof(ScaledSize));
                                                    Print(L"GPT-Defined Partition %u: Start Position: %s Partition Size: %s\n",j,ScaledStart,ScaledSize);
                                                    Print(L"Partition Type GUID:    {%g}\n",&PartitionEntry->PartitionTypeGUID);
                                                    Print(L"Unique Partition GUID:  {%g}\n",&PartitionEntry->UniquePartitionGUID);
                                                }
                                            }
                                        }
                                        FreePool(PartitionEntries);
                                    }
                                }
                            }
                            else
                                Print(L"Failed to read GPT Header! Status=0x%p\n",st);
                            FreePool(GptHeader);
                        }
                    }
                }
            }
        }
    }
    return st;
}

由于从扇区数换算到字节数的这个过程里,数字都挺大,因此要注意32位扩展到64位的情况,不能简单的用乘法符号。32位数乘32位数可以用__emulu这个编译器内置宏,而64位数乘32位数可以用EDK II库里的MultU64x32函数。
这里我还写了个DisplaySize函数,用于把字节数转换为一个描述大小的含有单位的字符串,但方便起见我直接向右移位,因此单位越大精确度越差。代码如下:

void DisplaySize(IN UINT64 Size,OUT CHAR16 *Buffer,IN UINTN Limit)
{
    if(Size<LimitKiB)
        UnicodeSPrint(Buffer,Limit,L"%u Bytes",Size);
    else if(Size>=LimitKiB && Size<LimitMiB)
        UnicodeSPrint(Buffer,Limit,L"%u KiB",Size>>10);
    else if(Size>=LimitMiB && Size<LimitGiB)
        UnicodeSPrint(Buffer,Limit,L"%u MiB",Size>>20);
    else
        UnicodeSPrint(Buffer,Limit,L"%u GiB",Size>>30);
}

第四步:编译测试

本文的代码已在GitHub上开源了,所以编译没啥好说的,直接用我给的脚本就完事了。

测试1:VMware虚拟机

这个虚拟机里安装了两个硬盘,一个用MBR分区的,另一个用GPT分区的,两个都装了Ubuntu,运行效果如下:
Ubuntu 64-bit-2021-03-11-09-00-50.png
可以发现里面有三块盘,其中两块是虚拟机的虚拟硬盘,另一块是放了EFI程序的U盘。
着重看一下第二块盘的分区表内容,可以看出里面有两个分区,分区类型分别是{C12A7328-F81F-11D2-BA4B00A0C93EC93B}{0FC63DAF-8483-4772-8E793D69D8477DE4}。也就是说,一个是EFI系统分区,另一个是Linux文件系统数据分区。

测试2:VMware虚拟机

这个虚拟机里安装了一个硬盘,以GPT分区,装的是Windows 7 x64,运行效果如下:
Windows 7 x64-2021-03-11-09-08-18.png
有意思的地方就来了,Windows似乎不鸟MBR上的分区大小域,直接就设置了个0xFFFFFFFF值,于是被我的程序识别为Over 2TiB。而Linux却很注意这个点。
里面有三个分区,类型分别是:
EFI系统分区,即{C12A7328-F81F-11D2-BA4B00A0C93EC93B}
微软保留分区,即{E3C9E316-0B5C-4DB8-817DF92DF00215AE}
Windows基本数据分区,即{EBD0A0A2-B9E5-4433-87C068B6B72699C7}

测试3:钓鱼派

钓鱼派上搭载了Intel Atom E3845处理器,和2GiB的DDR3L内存,我在上面插了一张Micro SD卡,以GPT分区,里面装了Windows 10 x64,运行效果如下:
PuTTY_Minnowboard-Turbot-UART_2021-03-11_09-25-44.PNG
由于钓鱼派上有UART接口,可以把控制台输出到串口上,因此可以用PuTTY获取控制台输出。
这张SD卡上有四个分区,类型分别是:
Windows恢复环境分区,即{DE94BBA4-06D1-4D40-A16ABFD50179D6AC}
EFI系统分区,即{C12A7328-F81F-11D2-BA4B00A0C93EC93B}
微软保留分区,即{E3C9E316-0B5C-4DB8-817DF92DF00215AE}
Windows基本数据分区,即{EBD0A0A2-B9E5-4433-87C068B6B72699C7}

测试4:巫毒派

巫毒派上搭载了AMD Ryzen Embedded V1605B处理器,我在上面插了两条4GiB的DDR4内存,和一个128G的M.2 SATA固态硬盘,以GPT分区,里面装了Windows 10 x64,运行效果如下:
Screenshot 2021-03-11 09-45-48.png
这块SSD上有五个分区,类型都一样,相比之下就是多了一个Windows基本数据分区。

结语

代码已在GitHub的组织号上开源了:https://github.com/MickeyMeowMeowHouse/UefiDiskAccess
编译好的文件也放到GitHub上了:https://github.com/MickeyMeowMeowHouse/UefiDiskAccess/releases
或许你们会发现,代理包括了一段获取启动来源设备路径的代码。这段代码的控制台输出告诉你,启动来源设备是它所在逻辑分区,而不是它所在的磁盘。至于为什么我就不解释了。

回复

使用道具 举报

1111

主题

1651

回帖

7万

积分

用户组: 管理员

一只技术宅

UID
1
精华
244
威望
743 点
宅币
24239 个
贡献
46222 次
宅之契约
0 份
在线时间
2297 小时
注册时间
2014-1-26
发表于 2021-3-12 00:49:10 | 显示全部楼层
梦回2011年,我当时还在玩 NASM 用 INT 0x13 的功能读取硬盘的 MBR、DBR、VBR,判断分区并加载分区引导扇区。然后分区引导扇区再加载自己分区里的引导加载器来加载自己的引导列表(兼容 XP 的 NTLDR),和自己的裸机系统。

现在用 UEFI 加载分区的时候,每个分区已经有 GUID、超过 2TB 的位置和大小的表达能力,以及更为细致的描述信息了。放在当时我对 GPT 完全是摸不着头脑的。

时代的变化真快。虽说 UEFI 从很早很早就有了。
回复 赞! 靠!

使用道具 举报

QQ|Archiver|小黑屋|技术宅的结界 ( 滇ICP备16008837号 )|网站地图

GMT+8, 2024-4-23 16:55 , Processed in 0.047390 second(s), 31 queries , Gzip On.

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表