请保留-> 【原文: 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所示。
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所示。
出现了熟悉的MBR信息,很容易看出,硬盘上只分了一个分区。
UEFI Shell的一屏没法把512字节的数据全部打印出来,图2是由两张图拼接而成的。
如下为本篇项目的代码,可以编译试试。
Gitee地址:https://gitee.com/luobing4365/uefi-explorer
项目代码位于:/ FF RobinPkg/RobinPkg/Applications/Diskdump下
2,509 total views, 2 views today