第七章 硬盘和文件系统

作者:Maxwell Li
日期:2017/12/09
未经作者允许,禁止转载本文任何内容。如需转载请留言。


[TOC]

7.1 GPT 硬盘

7.1.1 基于 MBR 分区的传统硬盘

MBR(Master Boot Record)是硬盘的首个扇区(0号扇区)。0号扇区用于存放启动代码和主分区表。0 ~ 439字节为启动代码。446 ~ 509为主分区表,主分区表区域最多容纳4个分区表项。最后2字节是结束标志。

7.1.2 GPT 硬盘详解

MBR 分区和 GPT 分区对比

要素 MBR 分区 GPT 分区
块地址位数 32位 64位
分区数量 4个主分区 目前 UEFI 版本支持128个分区
可扩展性 无 有版本号,可扩展
分区表安全性 无备份 有备份分区表
分区表完整性 无完整性检查 分区表有 CRC32 校验

GPT 硬盘仍然划分为扇区,所有扇区统一编址,从0开始编号,扇区地址称为 LBA。

  • 0号扇区:保护性 MBR。
  • 1号扇区:GPT 表头。
  • 2 ~ 33号扇区:GPT 硬盘分区表。
  • 末尾33个扇区:备份分区表和 GPT 头。

保护性 MBR

保护性 MBR 主要作用是兼容 MBR 硬盘时代的工具,防止这些工具不识别 GPT 格式而破坏硬盘。对各个域有不同的要求:

  1. 启动代码域(0 ~ 439字节):对 UEFI 系统无效,将被忽略。
  2. 分区表项的第一项:必须包含整个硬盘,并且 BootIndicator 必须为0,OSType 标志为 0xEE。
  3. 其他分区表项:必须为0。
  4. 标志位(510 ~ 511字节):必须为 0xAA55。
  5. 其它字节:必须为0。

GPT 头

1号扇区称为 GPT 头,存储了硬盘的结构信息。

7-1.png

硬盘最后一个扇区用于备份 GPT 头,当计算机系统读取硬盘 GPT 头时,会计算表头的 CRC32 校验码,如果计算出的校验码与存储的不一致,则会尝试从备份表头恢复 GPT 头。

GPT 分区表

分区表项结构体:

typedef struct {
  EFI_GUID PartitionTypeGUID;    // 分区类型 GUID,0表示此项空
  EFI_GUID UniquePartitionGUID;  // 分区标志 GUID
  EFI_LBA StartingLBA;           // 分区的首扇区
  EFI_LBA EndingLBA;             // 分区的尾扇区
  UINT64 Attributes;             // 分区属性
  CHAR16 PartitionName[36];      // 分区名字字符串,必须以 0x0000 结尾
} EFI_PARTITION_ENTRY

分区属性 Attributes

位 名称 属性
0 必须分区 若此标志位为1,表明该分区对系统至关重要。本分区被破坏或删除将导致系统不能正常工作
1 无 BlockIo 分区 若此标志位为1,UEFI 不为该分区生成块设备
2 传统 BIOS 引导 仅供传统 BIOS 系统使用,UEFI 系统忽略该位
3 ~ 37 保留区1 必须为0
48 ~ 63 保留区2 保留给 PartitionTypeGUID 的拥有者使用

7.2 设备路径

设备路径中的节点称为设备节点。设备路径由设备节点组成的列表构成,列表由“结束设备节点”结束。

设备节点以 EFI_DEVICE_PATH_PROTOCOL 开始,它是设备节点的第一个域。从面向对象的角度来说,它是所有设备节点的基类。结构体如下:

typedef struct _EFI_DEVICE_PATH_PROTOCOL {
  UINT8 Type;       // 类型
  UINT8 SubType;    // 次类型
  UINT8 Length[2];  // 节点字节数
} EFI_DEVICE_PATH_PROTOCOL

UEFI 支持的设备类型和次类型

设备路径类型 类型 Type 值 设备次类型
硬件设备路径 0x01 1、PCI 设备;2、PCCARD;3、内存映射设备;4、vendor 自定义设备;5、控制器设备
ACPI 设备路径 0x02 1、ACPI 设备;2、扩展 ACPI 设备;3、_ADR 设备
Messaging 设备路径 0x03 1、ATAPI;2、SCSI;3、光纤;4、1394;5、USB;12、IPv4;13、IPv6;15、USB;18、SATA;20、802.1q
介质设备路径 0x04 1、硬盘;2、光驱;3、vendor 自定义设备;4、文件路径;5、介质 Protocol;6、固件文件路径;7、固件卷
BIOS 启动设备路径 0x05 用于启动不支持 UEFI 的操作系统
结束节点设备路径 0x7F

硬盘设备(HARDDRIVE_DEVICE_PATH)节点

域 偏移 域长度 域类型 域的含义
Type 0 1 UINT8 介质设备路径,值为 0x4
SubType 1 1 UINT8 硬盘,值为1
Length 2 2 UINT16 长度,大小为42字节
PartitionNumber 4 4 UINT32 该分区在分区表中的位置,从1开始编号。
0表示整个硬盘设备。
对 MBR 硬盘来说,有效值是1、2、3、4中的一个;
对 GPT 硬盘来说,有效值的范围是[1,分区个数]。
PartitionStart 8 8 UINT64 该分区第一个扇区的 LBA 地址
PartitionSize 16 8 UINT64 该分区的扇区数
Signature 24 16 UINT8[16] 该分区的标识符。
如果为0,本域必须为0;
如果为1,本域前4字节有效,后12字节为0;
如果为2,本域必须是一个有效的 GUID
MBRType 40 1 UINT8 1表示 MBR 硬盘,2表示 GPT 硬盘
SignatureType 41 1 UINT8 分区标识符的类。
0表示无分区标识符;
1表示 MBR 分区(32位)标识符;
2表示标识符为 GUID(16位)

UEFI 提供 EFI_DEVICE_PATH_TO_EXIT_PROTOCOL 将设备路径转换为字符串。函数原型如下:

typedef CHAR16*(EFIAPI *EFI_DEVICE_PATH_TO_EXIT_PROTOCOL)(
  IN CONST EFI_DEVICE_PATH_TO_EXIT_PROTOCOL *DevicePath,
  IN BOOLEAN DisplayOnly,
  IN BOOLEAN AllowShortcuts
  );
typedef struct {
  EFI_DEVICE_PATH_TO_EXIT_NODE ConvertDeviceNodeToText;  // 将设备节点转换为字符串
  EFI_DEVICE_PATH_TO_EXIT_PATH ConvertDevicePateToText;  // 将设备路径转换为字符串
} EFI_DEVICE_PATH_TO_EXIT_PROTOCOL;
  • 若 DisplayOnly 为 TRUE,则生成的字符串为短格式,不能用于被分析后生成设备路径;
  • 若 DisplayOnly 为 FALSE,则生成的字符串为长格式,可以被分析后反向生成设备路径。

7.3 硬盘相关的 Protocol

每个硬盘设备(硬盘设备包括分区设备)控制器都安装有一个 BlockIo 实例、一个 BlockIo2 实例。 BlockIo 提供访问设备的阻塞函数;BlockIo2 提供访问设备的异步函数。

并且都安装有一个 DiskIo 实例、一个 DiskIo2 实例。 DiskIo 提供访问硬盘的阻塞函数;DiskIo2 提供访问硬盘的异步函数。

BlockIo 与 DiskIo 的区别:

  • BlockIo 只能按块(扇区)读写设备。
  • DiskIo 可以从任意偏移处(以字节为单位)读写磁盘,并可以读取任意字节数。

7.3.1 BlockIo 解析

BlockIo 是 EFI_BLOCK_IO_PROTOCOL 的缩写,包含两个属性:Revision、Media 以及四个成员函数:Reset、ReadBlocks、WriteBlocks、FlushBlocks。结构体如下:

typedef struct _EFI_BLOCK_IO_PROTOCOL {
  UINT64 Revision;              // Protocol 版本号
  EFI_BLOCK_IO_MEDIA *Media;    // 设备信息
  EFI_BLOCK_RESET Reset;        // 重置设备
  EFI_BLOCK_READ ReadBlocks;    // 读扇区
  EFI_BLOCK_WRITE WriteBlocks;  // 写扇区
  EFI_BLOCK_FLUSH FlushBlocks;  // 将缓冲中的数据写入扇区
} EFI_BLOCK_IO_PROTOCOL;

设备信息 Media

Media 指向该设备的 EFI_BLOCK_IO_MEDIA 结构体,结构体如下:

typedef struct {
  UINT32 MediaId;
  BOOLEAN RemovableMedia;
  BOOLEAN MediaPresent;      // 设备中是否有介质
  BOOLEAN LogicalPartition;  // 该介质是分区(TRUE);或整个设备(FALSE)
  BOOLEAN ReadOnly;
  BOOLEAN WriteCaching;      // 是否用 cache 方式写硬盘
  UINT32 BlockSize;
  UINT32 IoAlign;            // 读写硬盘时缓冲区地址对齐字节数,0或2^n。0和1表示可以是任意地址
  EFI_LBA LastBlock;         // 设备最后一个块的 LBA 地址
  // 以下三项只出现在 EFI_BLOCK_IO_PROTOCOL_REVISION2 及以上版本
  // 当该 Protocol 安装在分区设备上时,数值全为0
  EFI_LBA LowestAlignedLba;
  UINT32 LogicalBlocksPerPhysicalBlock;
  UINT32 OptimalTransferLengthGranularity;
} EFI_BLOCK_IO_MEDIA;

ReadBlocks 函数

ReadBlocks 只能按块读取设备,函数原型如下:

typedef EFI_STATUS(EFIAPI *EFI_BLOCK_READ) (
  IN EFI_BLOCK_IO_PROTOCOL *This,
  IN UINT32 MediaId,    // 设备中的介质号
  IN EFI_LBA LBA,       // 读取设备(或分区)起始块的 LBA 地址
  IN UINTN BufferSize,  // 读取的字节数,必须是块大小的整数倍
  OUT VOID *Buffer      // 读取的数据存到此缓冲区,调用者负责管理此内存
  );

WriteBlocks 函数

WriteBlocks 用于按块写设备,函数原型如下:

typedef EFI_STATUS(EFIAPI *EFI_BLOCK_WRITE) (
  IN EFI_BLOCK_IO_PROTOCOL *This,
  IN UINT32 MediaId,    // 设备中的介质号
  IN EFI_LBA LBA,       // 写入设备(或分区)起始块的 LBA 地址
  IN UINTN BufferSize,  // 写入的字节数,必须是块大小的整数倍
  IN VOID *Buffer       // 从缓冲区写数据到设备,调用者负责管理此内存
  );

FlushBlock 函数

FlushBlocks 用于将设备缓存中修改过的数据全部更新到介质中。

7.3.2 BlockIo2 解析

为了提高性能,UEFI 提供异步读写块设备的 Protocol:BlockIo2(EFI_BLOCK_IO2_PROTOCOL),结构体如下:

typedef struct _EFI_BLOCK_IO2_PROTOCOL {
  EFI_BLOCK_IO_MEDIA *Media;         // 设备信息
  EFI_BLOCK_RESET_EX Reset;          // 重置设备(阻塞函数)
  EFI_BLOCK_READ_EX ReadBlocksEx;    // 异步读设备
  EFI_BLOCK_WRITE_EX WriteBlocksEx;  // 异步写设备
  EFI_BLOCK_FLUSH_EX FlushBlocksEx;  // 异步 Flush 设备
} EFI_BLOCK_IO2_PROTOCOL;

ReadBlocksEx 函数

通常异步操作需要事件的支持,还需要上下文用于传递数据。在 UEFI 规范中,异步操作的事件和上下文封装起来称为令牌(Token),不同的异步操作有不同类型的令牌。一个拥有令牌的异步操作也称为一个事务。ReadBlocksEx 的令牌为 EFI_BLOCK_IO2_TOKEN。

ReadBlocksEx 函数向设备发出读指令后立刻返回。设备完成操作后会触发 Token 中的事件。

  • 如果函数返回错误,则表示指令没有发送到设备,事件不会触发。
  • 如果 Token 为 NULL 或 Token->Event 为 NULL,则该函数将退化为阻塞函数,功能与 ReadBlocks 相同。

Token 由调用者负责创建和删除。函数原型如下:

typedef EFI_STATUS(EFIAPI *EFI_BLOCK_READ_EX) (
  IN EFI_BLOCK_IO2_PROTOCOL *This,
  IN UINT32 MediaId,                  // 设备中的介质号
  IN EFI_LBA LBA,                     // 读取设备(或分区)起始块的 LBA 地址
  IN OUT EFI_BLOCK_IO2_TOKEN *Token,  // 此事务对应的 Token,调用者负责生成和关闭
  IN  UINTN BufferSize,               // 读取的字节数,必须是块大小的整数倍
  OUT VOID *Buffer                    // 读取的数据存到此缓冲区,调用者负责管理此内存
  ); 

有两种方式等待异步读硬盘完成:

  • 使用 WaitForEvent 等待异步读结束后再执行后续任务。流程如下:
    1. 生成 EFI_BLOCK_IO2_TOKEN 对象,主要是生成 Token 中的事件对象。
    2. 通过 ReadBlocksEx 函数向设备发出读指令。
    3. 执行其他任务。
    4. 通过 WaitForEvent 服务等待 Token 中的事件,然后处理读回的数据。
  • 在事件的 Notification 函数中完成后续任务。
    1. 生成 EFI_BLOCK_IO2_TOKEN 对象,主要是生成 Token 中的事件对象,定义事件的 Notification 函数,该函数的主要作用是处理读到的数据。
    2. 通过 ReadBlocksEx 函数向设备发出读指令。
    3. 执行其他任务。
    4. 通过 WaitForEvent 服务等待 Token 中的事件。WaitForEvent 返回时,Notification 函数已经由系统执行完毕。

WriteBlocksEx 函数

WriteBlocksEx 用于异步写数据到硬盘扇区,与 ReadBlocksEx 相似。函数原型如下:

typedef EFI_STATUS(EFIAPI *EFI_BLOCK_WRITE_EX) (
  IN EFI_BLOCK_IO2_PROTOCOL *This,
  IN UINT32 MediaId,                  // 设备中的介质号
  IN EFI_LBA LBA,                     // 写入设备(或分区)起始块的 LBA 地址
  IN OUT EFI_BLOCK_IO2_TOKEN *Token,  // 此事务对应的 Token,调用者负责生成和关闭
  IN UINTN BufferSize,                // 写入的字节数,必须是块大小的整数倍
  IN VOID *Buffer                     // 从缓冲区写数据到设备,调用者负责管理此内存
  ); 

7.3.3 DiskIo 解析

利用 DiskIo(EFI_DISK_IO_PROTOCOL) 可以从磁盘任意地址读写任意长度的数据。其结构体如下:

typedef struct _EFI_DISK_IO_PROTOCOL {
  UINT64 Revision;           // 版本号
  EFI_DISK_READ ReadDisk;    // 读磁盘
  EFI_DISK_WRITE WriteDisk;  // 写磁盘
} EFI_DISK_IO_PROTOCOL;

ReadDisk 函数

ReadDisk 函数原型:

typedef EFI_STATUS(EFIAPI *EFI_DISK_READ) (
  IN EFI_DISK_IO_PROTOCOL *This,
  IN UINT32 MediaId,    // 介质号
  IN UINT64 Offset,     // 从偏移 Offset(以字节为单位)处开始读数据
  IN UINTN BufferSize,  // 读取数据的长度
  OUT VOID *Buffer      // 数据读取到此缓冲区,调用者负责管理此内存
  );

WriteDisk 函数

WriteDisk 函数原型:

typedef EFI_STATUS(EFIAPI *EFI_DISK_WRITE) (
  IN EFI_DISK_IO_PROTOCOL *This,
  IN UINT32 MediaId,
  IN UINT64 Offset,
  IN UINTN BufferSize,
  IN VOID *Buffer
  );

7.3.4 DiskIo2 解析

DiskIo2(EFI_DISK_IO2_PROTOCOL)建立在 BlockIo2 基础上,是异步的磁盘操作协议。结构体如下:

typedef struct _EFI_DISK_IO2_PROTOCOL {
  UINT64 Revision;
  EFI_DISK_CANCEL_EX Cancel;      // 取消磁盘设备上处于等待状态的事件
  EFI_DISK_READ_EX ReadDiskEx;    // 异步读磁盘
  EFI_DISK_WRITE_EX WriteDiskEx;  // 异步写磁盘
  EFI_DISK_FLUSH_EX FlushDiskEx;  // 异步 Flush 磁盘
} EFI_DISK_IO2_PROTOCOL;

ReadDiskEx 函数

ReadDiskEx 是 ReadDisk 的异步版,用于异步读硬盘。函数原型如下:

typedef EFI_STATUS(EFIAPI *EFI_DISK_READ_EX) (
  IN EFI_DISK_IO2_PROTOCOL *This,
  IN UINT32 MediaId,                 // 介质号
  IN UINT64 Offset,                  // 从 Offset(以字节为单位)处开始读数据
  IN OUT EFI_DISK_IO2_TOKEN *Token,
  IN UINTN BufferSize,               // 读取数据的长度
  OUT VOID *Buffer                   // 数据读取到此缓冲区,调用者负责管理此内存
  );  
  • 如果 Token 为 NULL 或 Token 中的事件无效,则该函数退化为阻塞操作,相当于 ReadDisk 函数。
  • 如果 Token 中的事件有效,则该函数想设备发出读指令后立即返回,系统在该事务完成后自动触发 Token 中的事件。

WriteDiskEx 函数

WriteDiskEx 函数原型:

typedef EFI_STATUS(EFIAPI *EFI_DISK_WRITE_EX) (
  IN EFI_DISK_IO2_PROTOCOL *This,
  IN UINT32 MediaId,
  IN UINT64 Offset,
  IN OUT EFI_DISK_IO2_TOKEN *Token,
  IN UINTN BufferSize,
  IN VOID *Buffer
  );

7.3.5 PassThrough 解析

UEFI 标准中定义了两种 PassThrough:一种用于 ATA(Advanced Technology Attachment)硬盘,另一种用于 SCSI 硬盘和 ATAPI(AT Attachment Program Interface)硬盘。ATA 设备的 PassThrough Protocol 数据结构如下:

struct _EFI_ATA_PASS_THRU_PROTOCOL {
  EFI_ATA_PASS_THRU_MODE *Mode;                         // 设备模式
  EFI_ATA_PASS_THRU_PASSTHRU PassThru;                  // 向 ATA 设备发送 ATA 命令
  EFI_ATA_PASS_THRU_GET_NEXT_PORT GetNextPort;          // 列出 ATA 设备的端口号
  EFI_ATA_PASS_THRU_GET_NEXT_DEVICE GetNextDevice;      // 列出指定端口上的 Port Multiplier
  EFI_ATA_PASS_THRU_BUILD_DEVICE_PATH BuildDevicePath;  // 设备生成 DevicePath 节点
  EFI_ATA_PASS_THRU_GET_DEVICE GetDevice;               // 获得 DevicePath 对应的端口和 Port Multiplier
  EFI_ATA_PASS_THRU_RESET_PORT ResetPort;               // 重置指定端口及其端口上的设备
  EFI_ATA_PASS_THRU_RESET_DEVICE ResetDevice;           // 重置指定的设备
};

PassThru 的作用是向设备发送命令包,设备由 Port 和 PortMultiplierPort 决定。

  • 若指定参数 Event,则 PassThru 采用异步方式。
  • 若参数 Event 设为 NULL,则 PassThru 采用阻塞模式。

BlockIo 建立在 PassThrough 基础之上,用于读写硬盘扇区;DiskIo 建立在 BlockIo 基础之上,提供了读写硬盘任意地址处数据的能力。

7.4 文件系统

每个 UEFI 系统至少有一个 ESP(EFI System Partition)分区,用于存放启动文件。操作系统加载器以文件的形式存放在 ESP 分区,UEFI 通过文件系统来完成读写操作。UEFI 内置了 EFI_SIMPLE_FILE_SYSTEM_PROTOCOL(FileSyetemIo)来操作 FAT 文件系统,FileSystemIo 建立在 DiskIo 基础上,结构体如下:

typedef struct _EFI_SIMPLE_FILE_SYSTEM_PROTOCOL {
  UINT64 Revision;
  EFI_SIMPLE_FILE_SYSTEM_PROTOCOL_OPEN_VOLUME OpenVolume; // 打开卷并获得根目录句柄
} EFI_SIMPLE_FILE_SYSTEM_PROTOCOL;
// 打开卷,获得该卷上的根目录句柄,即根目录文件操作接口 EFI_FILE_PROTOCOL
typedef EFI_STATUS(EFIAPI *EFI_SIMPLE_FILE_SYSTEM_PROTOCOL_OPEN_VOLUME)(
  IN EFI_SIMPLE_FILE_SYSTEM_PROTOCOL *This,
  OUT EFI_FILE_PROTOCOL **Root  // 返回根目录句柄
  );

7.5 文件操作

EFI_FILE_HANDLE 是指向 EFI_FILE_PROTOCOL(FileIo)的指针。这里文件视为一种特殊的设备,EFI_FILE_PROTOCOL 被安装到文件设备上,其指针被当作文件句柄。EFI_FILE_PROTOCOL 用于操作设备上的文件或目录,结构体如下:

typedef struct _EFI_FILE_PROTOCOL {
  UINT64                Revision;
  EFI_FILE_OPEN         Open;
  EFI_FILE_CLOSE        Close;
  EFI_FILE_DELETE       Delete;
  EFI_FILE_READ         Read;
  EFI_FILE_WRITE        Write;
  EFI_FILE_GET_POSITION GetPosition;
  EFI_FILE_SET_POSITION SetPosition;
  EFI_FILE_GET_INFO     GetInfo;  // 获得文件属性
  EFI_FILE_SET_INFO     SetInfo;  // 设置文件属性
  EFI_FILE_FLUSH        Flush;    // 将文件中修改过的内容全部更新到设备
  EFI_FILE_OPEN_EX      OpenEx;
  EFI_FILE_READ_EX      ReadEx;
  EFI_FILE_WRITE_EX     WriteEx;
  EFI_FILE_FLUSH_EX     FlushEx;
} EFI_FILE_PROTOCOL;

7.5.1 打开文件

EFI_FILE_OPEN 函数用于打开文件或目录,函数原型如下:

typedef EFI_STATUS(EFIAPI *EFI_FILE_OPEN)(
  IN EFI_FILE_PROTOCOL  *This,        // EFI_FILE_PROTOCOL 实例,通常是一个目录的句柄
  OUT EFI_FILE_PROTOCOL **NewHandle,  // 返回打开文件的句柄
  IN CHAR16             *FileName,    // 文件名,以 NULL 结尾的 Unicode 字符串
  IN UINT64             OpenMode,     // 打开模式,表明打开文件用于读、写、创建
  IN UINT64             Attributes    // 文件属性,仅在产生文件时有效
  );
  • 如果 FileName 指定的文件路径是相对路径(非 \ 开头),该相对路径起始于 This 对应的目录。
  • 如果 FileName 指定的文件路径是绝对路径(以 \ 开头),该文件路径起始于 This 所在文件系统的根目录。

文件打开模式(OpenMode)

模式 用途
EFI_FILE_MODE_READ 文件用于读
EFI_FILE_MODE_WRITE 文件用于写
EFI_FILE_MODE_CHEATE 若文件不存在,则创建

创建文件时,可指定文件属性。

文件属性

属性 功能
EFI_FILE_READ_ONLY 只读文件
EFI_FILE_HIDDEN 隐藏文件
EFI_FILE_SYSTEM 系统文件
EFI_FILE_RESERVED 保留
EFI_FILE_DIRECTORY 目录
EFI_FILE_ARCHIVE 归档文件
EFI_FILE_VALID_ATTR 有效属性位

打开成功后,系统为文件(或目录)生成文件对象 FAT_IFILE,并返回文件对象中的 EFI_FILE_PROTOCOL 指针作为文件句柄。

7.5.2 读文件

EFI_FILE_READ 函数用于读文件或目录,函数原型如下:

typedef EFI_STATUS(EFIAPI *EFI_FILE_READ)(
  IN EFI_FILE_PROTOCOL *This,        // 文件句柄
  IN OUT UINTN         *BufferSize,  // 输入:要读出的的数据长度;输出:实际读出的数据长度
  OUT VOID             *Buffer       // 读缓冲区
  );
  • This 指向文件。Read 函数将从文件当前位置处读 BufferSize 个字节到 Buffer 缓冲区,如果文件当前位置到文件末尾小于 BufferSize,则读至文件末尾。返回后 BufferSize 存放实际读取的字节数。
  • This 指向目录。Read 函数将读取该目录下的文件及目录(不包含子目录下的文件和目录)的目录项 EFI_FILE_INFO。
    • 如果给定的缓冲区小于目录项大小,返回 EFI_BUFFER_TOO_SMALL,同时 BufferSize 返回需要的缓冲区大小。
    • 如果当前位置到达该目录下目录项的末尾,返回 EFI_SUCCESS,BufferSize 返回0。

EFI_FILE_INFO 结构体如下:

typedef struct {
  UINT64    Size;             // EFI_FILE_INFO 结构的大小,包括 FileName 字符串(及串尾的0)的长度
  UINT64    FileSize;         // 文件的长度
  UINT64    PhysicalSize;     // 文件在文件系统卷上占用的实际字节数
  EFI_TIME  CreateTime;
  EFI_TIME  LastAccessTime;
  EFI_TIME  ModificationTime;
  UINT64    Attribute;
  CHAR16    FileName[1];
} EFI_FILE_INFO;

7.5.3 写文件

EFI_FILE_WRITE 用于写数据到文件,函数原型如下:

typedef EFI_STATUS (EFIAPI *EFI_FILE_WRITE)(
  IN EFI_FILE_PROTOCOL *This,        // 文件句柄
  IN OUT UINTN         *BufferSize,  // 输入:要写入的数据长度;输出:实际写入的数据长度
  IN VOID              *Buffer       // 待写入数据
  ); 

Write 只能写数据到文件,不能写数据到目录。通常 Write 函数会写 BufferSize 指定的字节数到文件中(实际写的字节数等于指定要写的字节数),仅在遇到错误时,只写入部分数据,BufferSize 返回实际写的字节数。

7.5.4 关闭文件(句柄)

文件操作完毕后,要关闭文件句柄。Close 函数只有一个参数,即要关闭的文件句柄。

7.5.5 其他文件操作

读写文件位置

EFI_FILE_PROTOCOL 提供了 GetPosition 和 SetPosition 用于获取和设置文件的当前位置,函数原型如下:

/**
  @retval EFI_SUCCESS      成功获取当前位置
  @retval EFI_UNSUPPORTED  This 指向目录句柄,无法获得目录的当前位置
  @retval EFI_DEVICE_ERROR This 指向的文件已删除或其他设备错误
**/
typedef EFI_STATUS(EFIAPI *EFI_FILE_GET_POSITION)(
  IN EFI_FILE_PROTOCOL *This,     // 文件句柄
  OUT UINT64           *Position  // 返回文件的当前位置(从文件开头算起)
  );
/**
  @retval EFI_SUCCESS      成功设置文件位置
  @retval EFI_UNSUPPORTED  This 指向目录句柄,无法设置目录的当前位置
  @retval EFI_DEVICE_ERROR This 指向的文件已删除或其他设备错误
**/
typedef EFI_STATUS(EFIAPI *EFI_FILE_SET_POSITION)(
  IN EFI_FILE_PROTOCOL *This,    // 文件句柄
  IN UINT64            Position  // 要设定的文件位置
  );

读写文件信息

EFI_FILE_PROTOCOL 提供了 GetInfo 和 SetInfo 用于读取和设置文件信息,函数原型如下:

typedef EFI_STATUS(EFIAPI *EFI_FILE_GET_INFO)(
  IN EFI_FILE_PROTOCOL *This,             // 文件(或目录)句柄
  IN EFI_GUID          *InformationType,  // 要获取信息的类型标识符
  IN OUT UINTN         *BufferSize,       // 输入:缓冲区大小;输出:返回数据的长度
  OUT VOID             *Buffer            // 读缓冲区,数据类型由 InformationType 决定
  );
typedef EFI_STATUS (EFIAPI *EFI_FILE_SET_INFO)(
  IN EFI_FILE_PROTOCOL *This,             // 文件(或目录)句柄
  IN EFI_GUID          *InformationType,  // 要设置信息的类型标识符
  IN UINTN             BufferSize,        // InformationType 指定的数据的字节数
  IN VOID              *Buffer            // InformationType 指定的数据
  );

InformationType 有三种类型:

  • EFI_FILE_INFO(标识符 EFI_FILE_INFO_ID),结构体见 Read 函数。
  • EFI_FILE_SYSTEM_VOLUME_LABEL(标识符 EFI_FILE_SYSTEM_VOLUME_LABEL_ID)包含了用于表示卷标的字符串。
  • EFI_FILE_SYSTEM_INFO(标识符 EFI_FILE_SYSTEM_INFO_ID)包含了文件系统的相关信息,其中只有 VolumeLabel 可写,其他成员只读。结构体如下:
typedef struct {
  UINT64  Size;            // EFI_FILE_SYSTEM_INFO 结构的大小,包括 VolumeLabel(及串尾的0)的长度
  BOOLEAN ReadOnly;        // 只读卷
  UINT64  VolumeSize;      // 卷大小
  UINT64  FreeSpace;       // 文件系统剩余空间
  UINT32  BlockSize;       // 文件以此大小的块增加长度
  CHAR16  VolumeLabel[1];  // 卷名字符串(以 NULL 结尾)
} EFI_FILE_SYSTEM_INFO;

7.5.6 异步文件操作

异步文件操作需要提供一个 EFI_FILE_IO_TOKEN 类型的令牌,数据结构如下:

typedef struct {
  EFI_EVENT   Event;       // 此事务对应的事件
  EFI_STATUS  Status;      // Event 触发后,事务的状态
  UINTN       BufferSize;  // 作为输入:表示请求操作的字节数;作为输出:表示实际操作的字节数
  VOID        *Buffer;
} EFI_FILE_IO_TOKEN;

异步操作文件的步骤:

  1. 初始化 Token,包括 Token 中的 Event,如果时 ReadEx 或 WriteEx,还需要初始化 Buffer 和 BufferSize。
  2. 发出异步命令。
  3. 等待 Token 中的事件触发。
  4. 检查 Token 中的状态。
  5. 清理资源。

异步打开

OpenEx 用于异步打开文件或目录,当 Token 无效时,退化为阻塞操作。函数原型如下:

typedef EFI_STATUS(EFIAPI *EFI_FILE_OPEN_EX)(
  IN EFI_FILE_PROTOCOL      *This,        // EFI_FILE_PROTOCOL 实例,通常是一个目录的句柄
  OUT EFI_FILE_PROTOCOL     **NewHandle,  // 返回打开文件的句柄
  IN CHAR16                 *FileName,    // 文件名,以 NULL 结尾的 Unicode 字符串
  IN UINT64                 OpenMode,     // 打开模式,表明打开文件用于读、写、创建
  IN UINT64                 Attributes,   // 文件属性,仅在产生文件时有效
  IN OUT EFI_FILE_IO_TOKEN  *Token        // 每个事务都需要一个有效的 Token
  );

异步读/写文件

ReadEx 用于异步读文件,WriteEx 用于异步写文件,函数原型如下:

typedef EFI_STATUS(EFIAPI *EFI_FILE_READ_EX) (
  IN EFI_FILE_PROTOCOL      *This,
  IN OUT EFI_FILE_IO_TOKEN  *Token
);
typedef EFI_STATUS (EFIAPI *EFI_FILE_WRITE_EX) (
  IN EFI_FILE_PROTOCOL      *This,
  IN OUT EFI_FILE_IO_TOKEN  *Token 
);

有两种方式可以完成异步文件读写:

  • 使用 WaitForEvent 等待异步读结束后再执行后续任务;
  • 在事件的 Notification 函数中完成后续任务。

7.5.7 EFI_SHELL_PROTOCOL 中的文件操作

EFI_SHELL_PROTOCOL 中的文件操作接口对 EFI_FILE_PROTOCOL 进行了封装和扩充,除了提供基本的文件访问函数之外,还提供了相对目录的操作方式,以及 stdin、stdout、stderr 及文件查找函数。只有 Shell 下的应用程序才能使用 EFI_SHELL_PROTOCOL 中的文件操作函数。结构体如下:

typedef struct _EFI_SHELL_PROTOCOL {
  EFI_SHELL_EXECUTE                         Execute;
  EFI_SHELL_GET_ENV                         GetEnv;
  EFI_SHELL_SET_ENV                         SetEnv;
  EFI_SHELL_GET_ALIAS                       GetAlias;
  EFI_SHELL_SET_ALIAS                       SetAlias;
  EFI_SHELL_GET_HELP_TEXT                   GetHelpText;
  EFI_SHELL_GET_DEVICE_PATH_FROM_MAP        GetDevicePathFromMap;
  EFI_SHELL_GET_MAP_FROM_DEVICE_PATH        GetMapFromDevicePath;
  EFI_SHELL_GET_DEVICE_PATH_FROM_FILE_PATH  GetDevicePathFromFilePath;
  EFI_SHELL_GET_FILE_PATH_FROM_DEVICE_PATH  GetFilePathFromDevicePath;
  EFI_SHELL_SET_MAP                         SetMap;
  EFI_SHELL_GET_CUR_DIR                     GetCurDir;
  EFI_SHELL_SET_CUR_DIR                     SetCurDir;
  EFI_SHELL_OPEN_FILE_LIST                  OpenFileList;
  EFI_SHELL_FREE_FILE_LIST                  FreeFileList;
  EFI_SHELL_REMOVE_DUP_IN_FILE_LIST         RemoveDupInFileList;
  EFI_SHELL_BATCH_IS_ACTIVE                 BatchIsActive;
  EFI_SHELL_IS_ROOT_SHELL                   IsRootShell;
  EFI_SHELL_ENABLE_PAGE_BREAK               EnablePageBreak;
  EFI_SHELL_DISABLE_PAGE_BREAK              DisablePageBreak;
  EFI_SHELL_GET_PAGE_BREAK                  GetPageBreak;
  EFI_SHELL_GET_DEVICE_NAME                 GetDeviceName;
  EFI_SHELL_GET_FILE_INFO                   GetFileInfo;
  EFI_SHELL_SET_FILE_INFO                   SetFileInfo;
  EFI_SHELL_OPEN_FILE_BY_NAME               OpenFileByName;
  EFI_SHELL_CLOSE_FILE                      CloseFile;
  EFI_SHELL_CREATE_FILE                     CreateFile;
  EFI_SHELL_READ_FILE                       ReadFile;
  EFI_SHELL_WRITE_FILE                      WriteFile;
  EFI_SHELL_DELETE_FILE                     DeleteFile;
  EFI_SHELL_DELETE_FILE_BY_NAME             DeleteFileByName;
  EFI_SHELL_GET_FILE_POSITION               GetFilePosition;
  EFI_SHELL_SET_FILE_POSITION               SetFilePosition;
  EFI_SHELL_FLUSH_FILE                      FlushFile;
  EFI_SHELL_FIND_FILES                      FindFiles;
  EFI_SHELL_FIND_FILES_IN_DIR               FindFilesInDir;
  EFI_SHELL_GET_FILE_SIZE                   GetFileSize;
  EFI_SHELL_OPEN_ROOT                       OpenRoot;
  EFI_SHELL_OPEN_ROOT_BY_HANDLE             OpenRootByHandle;
  EFI_EVENT                                 ExecutionBreak;
  UINT32                                    MajorVersion;
  UINT32                                    MinorVersion;
  // Added for Shell 2.1
  EFI_SHELL_REGISTER_GUID_NAME              RegisterGuidName;
  EFI_SHELL_GET_GUID_NAME                   GetGuidName;
  EFI_SHELL_GET_GUID_FROM_NAME              GetGuidFromName;
  EFI_SHELL_GET_ENV_EX                      GetEnvEx;
} EFI_SHELL_PROTOCOL;

EFI_SHELL_PROTOCOL 中文件操作相关函数的第一个参数不再是 This 指针,通常是 Shell 文件句柄。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容