作者: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 格式而破坏硬盘。对各个域有不同的要求:
- 启动代码域(0 ~ 439字节):对 UEFI 系统无效,将被忽略。
- 分区表项的第一项:必须包含整个硬盘,并且 BootIndicator 必须为0,OSType 标志为 0xEE。
- 其他分区表项:必须为0。
- 标志位(510 ~ 511字节):必须为 0xAA55。
- 其它字节:必须为0。
GPT 头
1号扇区称为 GPT 头,存储了硬盘的结构信息。
硬盘最后一个扇区用于备份 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 等待异步读结束后再执行后续任务。流程如下:
- 生成 EFI_BLOCK_IO2_TOKEN 对象,主要是生成 Token 中的事件对象。
- 通过 ReadBlocksEx 函数向设备发出读指令。
- 执行其他任务。
- 通过 WaitForEvent 服务等待 Token 中的事件,然后处理读回的数据。
- 在事件的 Notification 函数中完成后续任务。
- 生成 EFI_BLOCK_IO2_TOKEN 对象,主要是生成 Token 中的事件对象,定义事件的 Notification 函数,该函数的主要作用是处理读到的数据。
- 通过 ReadBlocksEx 函数向设备发出读指令。
- 执行其他任务。
- 通过 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;
异步操作文件的步骤:
- 初始化 Token,包括 Token 中的 Event,如果时 ReadEx 或 WriteEx,还需要初始化 Buffer 和 BufferSize。
- 发出异步命令。
- 等待 Token 中的事件触发。
- 检查 Token 中的状态。
- 清理资源。
异步打开
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 文件句柄。