UEFI开发探索98 – 硬盘访问Diskdump

请保留-> 【原文:  https://blog.csdn.net/luobing4365 和 http://yiiyee.cn/blog/author/luobing/】

之前在CSDN上建了一个专栏,名字为汇编语言探索,准备聊一些用汇编写的小项目。大概写了十几篇关于Foxdisk运行原理的文章,后面还会不定期的写写,特别是最近看到一个俄罗斯程序员用FASM写的KolibriOS,贼有意思了,找时间读一读。

Foxdisk本来是我为自己写的一个多操作系统引导的软件,类似于Grub。核心点在于,在Foxdisk内可以将硬盘分成若干区域,每个区域装一个操作系统,各操作系统间可以通过共享的分区分享数据。而提供的图形界面,比单调的Grub让我舒服些。

这是一个很好的想法,能满足我使用多个操作系统的开发习惯。Foxdisk开发了3代,前两代是纯粹用汇编语言写的,3.0则使用了C语言嵌汇编。再然后,我迷上了UEFI,对于在Legacy BIOS下开发的Foxdisk,就这么抛弃了。

Foxdisk 3.0中,内置了对硬盘分区的功能(相当于Fdisk或spfdisk之类的工具)。实际上,Foxdisk大部分的工作,都是在和硬盘打交道。

进入UEFI后,一直不怎么想去研究硬盘访问。总觉得UEFI本身已经将硬盘访问封装得很好了,没什么必要再去开发分区、格式化之类的软件了。

刚好在近期的项目中,需要在Option ROM中查看某几个扇区的数据。为了比对Option ROM的代码,需要开发一个UEFI小程序,实现类似功能。

1 UEFI的存储介质访问栈

UEFI规范中提供了大量的存储介质Protocol(Media Protocol),甚至包括内存虚拟盘(RAM Disk)都支持了。

Legacy BIOS下是通过Int 0x13进行硬盘访问的,由寄存器AH标识各种功能号,供程序员调用。习惯了这种思维,再看UEFI的访问方式,总是有点别扭。

首先需要找出类似功能的访问接口,UEFI提供了从硬盘协议层的Protocol,一直到文件系统的Protocol,非常完善,其架构如图1所示。

图1 UEFI的存储介质访问栈

PassThrough以接近介质访问协议的方式提供接口,包括ATA协议、SCSI协议等,因此能提供相当复杂的操作。

在此之上,UEFI提供了Block I/O和Block I/O 2,可以按扇区(块)读写设备。Block |/O是阻塞操作,Block I/O为异步操作。这一层的操作,类似于Legacy BIOS中的Int 0x13的按扇区访问操作。

Disk I/O和Disk I/O 2可以从任意偏移处读写磁盘,并且可以读写任意字节数,相比于Block I/O和Block I/O 2只能按扇区读写,更为灵活。类似的,Disk I/O是阻塞操作,而Disk I/O 2是异步操作。

再往上所构建的Protocol,是可以操作FAT文件系统的Simple File System和访问文件的File。之前的博客中,使用File Protocol构建了访问文件的读写函数(FileRW.c)。所构建的函数,与C语言库中的fread()、fwrite()等函数类似。

为了实现篇首所说的访问指定扇区的小程序,最合适的是Block I/O,下面介绍编写过程。

2 编写Diskdump程序

2.1 Block I/O简介

与Block I/O Protocol相关的函数及GUID,定义在MdePkg\Include\Protocol\BlockIo.h中。其结构体为:

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;
};

它提供了四个接口函数:Reset、ReadBlocks、WriteBlocks和FlushBlocks,以及版本号Revision和介质属性Media。

1)设备信息Media

Media指向设备的结构体EFI_BLOCK_IO_MEDIA,它包含了设备的相关属性信息。其内容如下:

/**
  Block IO read only mode data and updated only via members of BlockIO
**/
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;

一般来说,BlockSize是0x200,也即512字节一个扇区(块)。

UEFI完全摒弃了以前CHS的地址模式,直接用LBA地址模式来标志扇区地址。LBA地址从0计算,最后一个地址为LastBlock。

2)读扇区函数ReadBlocks

ReadBlocks用于读取块设备,也即按扇区进行读取,其函数原型如下所示。

/**
  Read BufferSize bytes from Lba into Buffer.

  @param  This       Indicates a pointer to the calling context.
  @param  MediaId    Id of the media, changes every time the media is replaced.
  @param  Lba        The starting Logical Block Address to read from
  @param  BufferSize Size of Buffer, must be a multiple of device block size.
  @param  Buffer     A pointer to the destination buffer for the data. The caller is
                     responsible for either having implicit or explicit ownership of the buffer.

  @retval EFI_SUCCESS           The data was read correctly from the device.
  @retval EFI_DEVICE_ERROR      The device reported an error while performing the read.
  @retval EFI_NO_MEDIA          There is no media in the device.
  @retval EFI_MEDIA_CHANGED     The MediaId does not matched the current device.
  @retval EFI_BAD_BUFFER_SIZE   The Buffer was not a multiple of the block size of the device.
  @retval EFI_INVALID_PARAMETER The read request contains LBAs that are not valid, 
                                or the buffer is not on proper alignment.

**/
typedef
EFI_STATUS
(EFIAPI *EFI_BLOCK_READ)(
  IN EFI_BLOCK_IO_PROTOCOL          *This,      // EFI_BLOCK_IO_PROTOCOL实例
  IN UINT32                         MediaId,    //*Media中的MediaID
  IN EFI_LBA                        Lba,        //读取设备(或分区)的LBA地址
  IN UINTN                          BufferSize, //读取的字节数,为BlockSize整数倍
  OUT VOID                          *Buffer     //存储数据的缓冲区
  ); 

3)写扇区函数WriteBlocks

WriteBlocks用于按快写设备(或扇区),其入口参数与ReadBlocks相同,如下所示:

/**
  Write BufferSize bytes from Lba into Buffer.

  @param  This       Indicates a pointer to the calling context.
  @param  MediaId    The media ID that the write request is for.
  @param  Lba        The starting logical block address to be written. The caller is
                     responsible for writing to only legitimate locations.
  @param  BufferSize Size of Buffer, must be a multiple of device block size.
  @param  Buffer     A pointer to the source buffer for the data.

  @retval EFI_SUCCESS           The data was written correctly to the device.
  @retval EFI_WRITE_PROTECTED   The device can not be written to.
  @retval EFI_DEVICE_ERROR      The device reported an error while performing the write.
  @retval EFI_NO_MEDIA          There is no media in the device.
  @retval EFI_MEDIA_CHNAGED     The MediaId does not matched the current device.
  @retval EFI_BAD_BUFFER_SIZE   The Buffer was not a multiple of the block size of the device.
  @retval EFI_INVALID_PARAMETER The write request contains LBAs that are not valid, 
                                or the buffer is not on proper alignment.

**/
typedef
EFI_STATUS
(EFIAPI *EFI_BLOCK_WRITE)(
 IN EFI_BLOCK_IO_PROTOCOL          *This,      // EFI_BLOCK_IO_PROTOCOL实例
  IN UINT32                         MediaId,    //*Media中的MediaID
  IN EFI_LBA                        Lba,         //读取设备(或分区)的LBA地址
  IN UINTN                          BufferSize,   //要写的字节数,为BlockSize整数倍
  OUT VOID                          *Buffer     //数据的缓冲区,写往设备(或分区)
  );

3)更新介质FlushBlocks

写往设备的数据,在实际写入设备前,函数就会返回EFI_SUCCESS,数据实际上存储在缓存中.此函数将设备缓冲中修改过的数据,全部更新到介质中。其函数原型为:

/**
  Flush the Block Device.

  @param  This              Indicates a pointer to the calling context.

  @retval EFI_SUCCESS       All outstanding data was written to the device
  @retval EFI_DEVICE_ERROR  The device reported an error while writting back the data
  @retval EFI_NO_MEDIA      There is no media in the device.

**/
typedef
EFI_STATUS
(EFIAPI *EFI_BLOCK_FLUSH)(
  IN EFI_BLOCK_IO_PROTOCOL  *This    // EFI_BLOCK_IO_PROTOCOL实例
  );

2.2 Diskdump编程

了解了Block I/O的函数接口后,可以进入实质的编程阶段。

所实现的Diskdump程序,主要实现两个功能:
1) 获取当前系统下有多少个BlockIo实例,显示每个BlockIo设备的信息;
2) 指定BlockIo设备以及其LBA地址,得到扇区数据并显示在屏幕上。

编程步骤如下:

1)项目中增加对Block I/O的支持

添加头文件的包含:

#include <Protocol/BlockIo.h>

并在INF文件的[Protocols]部分,增加对应的GUID声明:

[Protocols]
  gEfiSimpleTextInputExProtocolGuid            
  gEfiSimplePointerProtocolGuid
  gEfiGraphicsOutputProtocolGuid
  gEfiSimpleFileSystemProtocolGuid
  gEfiDevicePathProtocolGuid
  
  gEfiBlockIoProtocolGuid   # add for BlockIO robin 20210824

2)实现对Block I/O实例的获取

实现方法和其他Protocol实例的获取方法一样,如下:

EFI_BLOCK_IO_PROTOCOL* gBlockIoArray[256];
UINTN nBlockIO = 0;
EFI_STATUS LocateBlockIO(void)
{
	EFI_STATUS                         Status;
	EFI_HANDLE                         *BlockIOHandleBuffer = NULL;
	UINTN                              HandleIndex = 0;
	UINTN                              HandleCount = 0;
	//get the handles which supports 
	Status = gBS->LocateHandleBuffer(
		ByProtocol,
		&gEfiBlockIoProtocolGuid,
		NULL,
		&HandleCount,
		&BlockIOHandleBuffer
		);
	if (EFI_ERROR(Status))	return Status;		//unsupport
	nBlockIO = HandleCount;		//保存BlockIO数目
	if(HandleCount>250)
		HandleCount = 250; //只支持250个存储设备,应该不大可能有这么多
	for (HandleIndex = 0; HandleIndex < HandleCount; HandleIndex++)
	{
		Status = gBS->HandleProtocol(
			BlockIOHandleBuffer[HandleIndex],
			&gEfiBlockIoProtocolGuid,
			(VOID**)&(gBlockIoArray[HandleIndex]));
		if (EFI_ERROR(Status))	break;
		else
		{
			Status = EFI_SUCCESS;
		}
	}
	if(BlockIOHandleBuffer!=NULL)
		FreePool(BlockIOHandleBuffer);
	return Status;
}

3)实现功能

具体的实现,在main()函数中。根据不同的命令行参数,来执行相应的动作。实现代码如下:

if(Argc == 1)  //列出所有BlockIO设备
{
    Print(L"BlockIO counts: %d\n",nBlockIO);
    for(i=0; i<nBlockIO; i++)
    {
      Print(L" Number %02d:\n",i);
      dumpBlockIOMedia(gBlockIoArray[i]);
      WaitKey();
    }
}
else if(Argc == 2)
{
  
    if((strcmp("-h",Argv[1])==0) ||(strcmp("-H",Argv[1])==0) ||  (strcmp("-?",Argv[1])==0))
    {
      Print(L"Syntax: Diskdump x y\n");
      Print(L"  x: number of BlockIO\n");
      Print(L"  y: LBA Address\n");
    }
}
else if(Argc == 3)
{
    EFI_STATUS Status;
    sscanf(Argv[1],"%d",&number);
    sscanf(Argv[2],"%lld",&rAddress);
    Print(L"Get data from: BlockIo[%x],LBA-%ld\n",number,rAddress);
    if(number > (UINT16)nBlockIO)
    {
      Print(L"Error: Out of range!\n");
    }
    else
    {
      Status = gBlockIoArray[number]->ReadBlocks(
                        gBlockIoArray[number],
                        gBlockIoArray[number]->Media->MediaId,
                        rAddress,512,Buffer);
      if(EFI_ERROR(Status))
        Print(L"%r\n",Status);
      else
      {
        Print(L"--------0--1--2--3--4--5--6--7--8--9--A--B--C--D--E--F-\n");
        for(i=0; i<32; i++)
        {
          if(i==16)
          {
            WaitKey();
            Print(L"--------0--1--2--3--4--5--6--7--8--9--A--B--C--D--E--F-\n");
          }
          Print(L"0x%03x:  ",i*16);
          gST->ConOut->SetAttribute(gST->ConOut,EFI_BACKGROUND_RED|EFI_WHITE);
          for(j=0; j<16; j++)
          {      
            Print(L"%02x",Buffer[i*16+j]);
            if(j<15)Print(L" ");
          }
          gST->ConOut->SetAttribute(gST->ConOut,EFI_BACKGROUND_BLACK|EFI_LIGHTGRAY);
          Print(L"\n");
        }
      }
   }
}

分为三种情况:
1) 无参数时,打印所有Block I/O设备的属性(*Media);
2) 一个参数时,只接受“-h”、“-H”和“-?”三个输入,会打印基本的语法说明;
3) 两个参数时,认为第一个参数时Block I/O的序号(从0开始),第二个参数是LBA地址。

两个参数时,会将获取到的扇区数据打印出来。

3 测试

在模拟器上没法测试,只能在实际机器上进行测试。使用如下命令编译:

C:\vUDK2018\edk2>build -p RobinPkg\RobinPkg.dsc -m RobinPkg\Applications\Diskdump\Diskdump.inf -a X64

我所测试的机器,带有一个M.2硬盘,进入UEFI Shell会发现4个设备,前两个是硬盘和U盘设备,后两个是它们的分区。

获取第1个设备的LBA 0数据,结果如图2所示。

图2 获取BlockIO 0,LBA 0的数据

出现了熟悉的MBR信息,很容易看出,硬盘上只分了一个分区。

UEFI Shell的一屏没法把512字节的数据全部打印出来,图2是由两张图拼接而成的。

如下为本篇项目的代码,可以编译试试。

Gitee地址:https://gitee.com/luobing4365/uefi-explorer
项目代码位于:/ FF RobinPkg/RobinPkg/Applications/Diskdump下

2,528 total views, 2 views today

发表评论

电子邮件地址不会被公开。