3

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

 2 years ago
source link: http://yiiyee.cn/blog/2021/08/25/uefi%e5%bc%80%e5%8f%91%e6%8e%a2%e7%b4%a298-%e7%a1%ac%e7%9b%98%e8%ae%bf%e9%97%aediskdump/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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地址。

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

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

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下

212 total views, 1 views today


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK