在上一篇文章中我们简单介绍了UEFI的基本概念在本章中我们介绍uefi-rs库的内存管理和文件系统使用
文章目录
基本结构
uefi-rs中基本的结构已经画成脑图的形式

uefi-rs中主要分为以下内容
- 信息类: 固件的信息,UEFI信息,uefi配置表
- 服务类: 在
uefi-rs中主要包含运行时服务,启动服务,退出启动服务等 - Protocol(协议): 在
uefi-rs中支持以下协议,所有的Protocol需要使用BootServer.local_protocol::<ProtocolName>();来获取(ProtocolName表示Protocol名称,例如BootServer.local_protocol::<Input>())(以上都是伪代码)- 标准输入
Input - 标准输出
Output - 图形输出协议
GOP(Grahpics Output Protocol) - 串行IO设备访问
Serial - 调试服务
DebugSupport - 镜像加载
LoadImage - 文件系统
SimpleFileSystem - 多处理器服务
MPService
- 标准输入
- 其他服务: 内存分配等
UEFI的HelloWorld!
我们创建好项目后再Cargo.toml中添加如下内容
[dependencies.uefi]
version="0.4.0"
features = ['exts']
[dependencies.uefi-services]
version = "0.2.0"
features = ["qemu"]
[dependencies.log]
version = "0.4.8"
然后我们在main.rs中添加几个feature
// main.rs
#![no_std]
#![no_main]
#![feature(asm)]
#![feature(abi_efiapi)] // uefi-rs使用的是efi调用约定
#![feature(never_type)]
extern crate uefi;
然后定义UEFI的入口函数
#[entry]
fn efi_main(image: Handle, st: SystemTable<Boot>) -> Status{}
其中#[entry]在uefi-marcos中定义主要功能是将我们定义的函数转化为pub extern "efiapi" fn eif_main这样的形式
程序进入main函数后的第一件事情就是uefi服务进入DXE(Driver Execution Environment)阶段
#[entry]
fn efi_main(image: Handle, st: SystemTable<Boot>) -> Status{
if let Err(e) = uefi_services::init(&st).log_warning(){
info!("{:?}",e);
// 如果服务初始化失败后则不能继续执行
return e.status();
}
info!("Hello World!");
Status::SUCCESS
}
我们编写完毕代码后再项目的根目录(与src目录同级)创建.cargo文件夹,随后创建config文件并填写一下内容(rust已经支持了x86_64-unknown-uefi编译目标)
# build settings
[build]
target = "x86_64-unknown-uefi"
这样我们在运行时不需要指定--target参数,否则每次运行时需要添加,例如
cargo xbuild --target x86_64-unknown-uefi
Cargo代理
如果连接到cargo.io比较慢可以添加国内代理
//in .cargo/config
[source.crates-io]
registry = "https://github.com/rust-lang/crates.io-index"
replace-with = 'ustc'
[source.ustc]
registry = "http://mirrors.ustc.edu.cn/crates.io-index"
这样当我们运行cargo xbuild后会在target/debug目录中找到xxx.efi文件(xxx表示起的项目名称)
使用QEMU启动
当编译完毕后我们需要创建一个目录esp/EFI/Boot然后将我们编译的xxx.efi命名成BootX64.efi
目录结构如下
project
├── .cargo
├── src
├── target
├── Cargo.toml
├── Cargo.lock
└──esp
└── EFI
└── Boot
└──BootX64.efi
esp目录是我们需要挂载的文件目录
因为我们使用的是虚拟机,我们需要使用OVMF(开放虚拟机固件Open Virtual Machine Firmware)OVMF的制作请参考使用Rust开发操作系统(UEFI基本介绍)的OVMF制作章节,然后为QEMU指定OVMF_CODE.fd路径,OVMF_VARS.fd的路径,以及我们创建的esp文件夹路径
例如(为了方便阅读对命令进行了换行)
qemu-system-x86_64
-drive if=pflash,format=raw,file=<OVMF_CODE.fd的路径>,readonly=on
-drive if=pflash,format=raw,file=<OVMF_VARS.fd的路径>,readonly=on
-drive format=raw,file=fat:rw:<esp文件路径>
以下是参考命令(windows)
qemu-system-x86_64
-drive if=pflash,format=raw,file=C:\Users\VenmoSnake\Desktop\ReboxOS\OVMF_CODE.fd,readonly=on
-drive if=pflash,format=raw,file=C:\Users\VenmoSnake\Desktop\OS\OVMF_VARS.fd,readonly=on
-drive format=raw,file=fat:rw:C:\Users\VenmoSnake\Desktop\OS\esp
这样我们可以在QEMU上运行我们编写的efi
基本的数据结构
Result
pub type Result<Output = (), ErrData = ()> = core::result::Result<Completion<Output>, Error<ErrData>>;
在uefi-rs中Result的定义有些许不同而且使用方式跟通常用法也不同,因此我们要具体说明一下
我们可以看到一个名叫Completion的数据结构其定义如下
#[must_use]
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Completion<T> {
status: Status,
result: T,
}
Completion用于表示UEFI服务操作完成后的结果,但在此过程中可能会遇到一些非致命问题,其中的Status表示执行的结果状态,定义如下
newtype_enum! {
#[must_use]
pub enum Status: usize => {
/// 操作执行成果
SUCCESS = 0,
/// 该字符串包含无法呈显示的字符,并被跳过。 UEFI使用的是UCS-2编码
WARN_UNKNOWN_GLYPH = 1,
/// handle已关闭,但未删除文件。
WARN_DELETE_FAILURE = 2,
/// handle已关闭,但文件内容没有正确刷新。
WARN_WRITE_FAILURE = 3,
/// 指定的缓冲区太小,数据被截断。
WARN_BUFFER_TOO_SMALL = 4,
/// The data has not been updated within the timeframe set by local policy.
/// 数据尚没有在指定的时间范围内更新。(时间范围在本地策略中设置)
WARN_STALE_DATA = 5,
/// 结果缓冲区包含符合UEFI的文件系统。
WARN_FILE_SYSTEM = 6,
/// 系统重置后将处理该操作。
WARN_RESET_REQUIRED = 7,
/// 镜像读取失败
LOAD_ERROR = ERROR_BIT | 1,
/// 参数不正确
INVALID_PARAMETER = ERROR_BIT | 2,
/// 不支持的操作
UNSUPPORTED = ERROR_BIT | 3,
/// 缓冲区的大小不符合要求。
BAD_BUFFER_SIZE = ERROR_BIT | 4,
/// 缓冲区的大小不足以容纳请求的数据。或者表示在参数中应该返回所需的缓冲区大小。
BUFFER_TOO_SMALL = ERROR_BIT | 5,
/// 没有数据返回。
NOT_READY = ERROR_BIT | 6,
/// 物理设备在尝试操作时出错。
DEVICE_ERROR = ERROR_BIT | 7,
/// 该设备不能执行写操作。
WRITE_PROTECTED = ERROR_BIT | 8,
/// 该资源已用完。
OUT_OF_RESOURCES = ERROR_BIT | 9,
/// 在文件系统上检测到不一致。
VOLUME_CORRUPTED = ERROR_BIT | 10,
/// 文件系统上没有足够的空间。
VOLUME_FULL = ERROR_BIT | 11,
/// 设备不包含任何执行操作的介质.
NO_MEDIA = ERROR_BIT | 12,
/// 自上次访问以来,设备中的介质已更改.
MEDIA_CHANGED = ERROR_BIT | 13,
/// 找不到该项。
NOT_FOUND = ERROR_BIT | 14,
/// 访问被拒绝.
ACCESS_DENIED = ERROR_BIT | 15,
/// 找不到服务器或未响应请求.
NO_RESPONSE = ERROR_BIT | 16,
/// 设备的映射不存在.
NO_MAPPING = ERROR_BIT | 17,
/// 超时.
TIMEOUT = ERROR_BIT | 18,
/// Protocol尚未启动.
NOT_STARTED = ERROR_BIT | 19,
/// Protocol已经启动.
ALREADY_STARTED = ERROR_BIT | 20,
/// 该操作被中止。
ABORTED = ERROR_BIT | 21,
/// 网络通信期间发生了ICMP错误。
ICMP_ERROR = ERROR_BIT | 22,
/// 网络通信期间发生了TFTP 错误
TFTP_ERROR = ERROR_BIT | 23,
/// 网络通信期间发生了protocol 错误
PROTOCOL_ERROR = ERROR_BIT | 24,
/// 该函数遇到的内部版本与调用者请求的版本不兼容。
INCOMPATIBLE_VERSION = ERROR_BIT | 25,
/// 由于违反安全性,未执行该功能。
SECURITY_VIOLATION = ERROR_BIT | 26,
/// 检测到 CRC 错误 (CRC循环冗余校验)
CRC_ERROR = ERROR_BIT | 27,
/// Beginning or end of media was reached
END_OF_MEDIA = ERROR_BIT | 28,
/// 文件结束 EOF
END_OF_FILE = ERROR_BIT | 31,
/// 指定的语言无效。
INVALID_LANGUAGE = ERROR_BIT | 32,
/// 数据的安全状态未知或受到破坏,必须更新或替换数据以便恢复。
COMPROMISED_DATA = ERROR_BIT | 33,
/// 地址冲突地址分配
IP_ADDRESS_CONFLICT = ERROR_BIT | 34,
/// 网络通信期间发生HTTP错误。
HTTP_ERROR = ERROR_BIT | 35,
}
}
Completion提供了4种便利的函数分别是:
impl<T> From<T> for Completion<T> {
fn from(result: T) -> Self {
Completion::new(Status::SUCCESS, result)
}
}
impl From<Status> for Completion<()> {
fn from(status: Status) -> Self {
Completion::new(status, ())
}
}
pub fn status(&self) -> Status {
self.status
}
pub fn split(self) -> (Status, T) {
(self.status, self.result)
}
fn from(status: Status) -> Self用于封装指定的状态,用于快速返回执行状态(只需要执行状态不要求其结果的情况),例如
Completion::from(Status::SUCCESS)
fn from(result: T) -> Self用于封装内容通常用于操作成功需要返回执行内容,例如
Completion::from("执行成功")
pub fn status(&self) -> Status用于返回状态,用于判断执行操作的结果状态
pub fn split(self) -> (Status, T)将状态和结果分离
改造Result
了解到这些基本结构后我们可以吧一些常用的函数签名定义出来,uefi-rs不支持自定义异常,uefi::Result表示UEFI执行的结果,Result分为uefi服务执行结果与uefi应用(我们自己编写的程序)的执行结果,因此我们的函数如果要使用uefi的服务则可以抽象为以下形式
// 该函数用于打印当前内存布局
fn alloc_memory(bt: &BootServices)-> uefi::Result<()>{
let size = bt.memory_map_size();
let buffer = bt.allocate_pool(MemoryType::BOOT_SERVICES_DATA,size).log_warning()?;
let buffer = unsafe{core::slice::from_raw_parts_mut(buffer,size)};
let (map,mut iter) = bt.memory_map(buffer).log_warning()?;
while let Some(desc) = iter.next(){
info!("{:?}",desc);
}
Ok(Completion::from(()))
}
这样我们的代码简化不少否则会充斥大量的if let.match等代码,如果函数需要返回处理的结果则我们的函数可以设计为
// 该函数用于分配指定大小的内存并转换为切片的形式
fn alloc_slice(bt: &BootServices,size:usize)-> uefi::Result<Option<&mut [u8]>>{
if size <= 0 {
Ok(Completion::from(None))
}
let ptr = bt.allocate_pool(MemoryType::LOADER_DATA,size).log_warning()?;
let slice:&mut [u8] = unsafe{ core::slice::from_raw_parts_mut(ptr,size)};
Ok(Completion::from(Some(slice)))
}
我们需要注意的是虽然Completion提供了fn from(status: Status) -> Self这样的函数,该函数的使用时机是使用UEFI服务发生错误时返回的状态(uefi服务层面),如果出现了应用层面的错误(应用层面)则不能使用uefi::Result来做返回结果,需要进行封装例如uefi::Result<core::result::Result<Data>>相应的返回成功结果为Ok(Completion::from(core::result::Result::Ok(data))),错误结果为Ok(Completion::from(core::result::Result::Err(err))),当然我们可以使用函数来简化
首先我们定义2种Result,分别表示UEFI和应用的
// 表示UEFI执行的结果
pub type UefiResult<T> = uefi::Result<T>;
// 表示应用程序的结果
pub type Result<T> = core::result::Result<T, Error>;
然后我们定义应用程序使用的Error
#[derive(Debug)]
pub enum Error {
Io,
}
然后我们提供ok和err函数来简化
pub fn ok<T>(t: T) -> UefiResult<Result<T>> {
Ok(Completion::from(core::result::Result::Ok(t)))
}
pub fn err<T>(e: Error) -> UefiResult<Result<T>> {
Ok(Completion::from(core::result::Result::Err(e)))
}
然后我们的函数就会变成如下形式
fn alloc_slice(bt: &BootServices, size: usize) -> UefiResult<Result<&mut [u8]>> {
if size <= 0 {
return err::<&mut [u8]>(Error::Io);
}
let ptr = bt.allocate_pool(MemoryType::LOADER_DATA, size).log_warning()?;
let slice: &mut [u8] = unsafe { core::slice::from_raw_parts_mut(ptr, size) };
ok(slice)
}
这样就简化了编写的内容
内存管理
uefi中提供了基础的内存管理方式主要分为两种
- AllocatePool: UEFI驱动程序使用
allocate_pool()和free_pool()引导服务来分配和释放保证8字节边界对齐的小缓冲区 - AllocatePages: UEFI驱动程序使用
allocate_pages()和free_pages()引导服务来分配和释放较大的缓冲区,这些缓冲区保证在4 KB边界上对齐。这些服务允许在任何可用地址,特定地址或特定地址下方分配缓冲区
内存分配的注意事项
UEFI驱动程序不应该理想化系统内存的布局。虽然allocate_pages允许在特定地址出分配缓冲区,但是强烈建议不要从特定地址分配或在特定地址以下分配。 allocate_pool()服务不允许调用者指定首选地址,因此该服务可以安全使用,并且不会影响UEFI驱动程序在不同平台上的兼容性
allocate_pages()服务有用在特定地址或指定地址范围分配内存的功能。主要通过AllocateType指定分配类型,AllocateType::AnyPages可安全使用,并提高了不同平台上UEFI驱动程序的兼容性。 AllocateType::MaxAddress(usize)和AllocateType::Address(usize)等类型可能会降低平台兼容性,因此不建议使用它们
尽管
Alloc service可以进行特定的内存分配,但不要在UEFI驱动程序再特定的地址分配内存。在特定地址上分配内存可能会导致错误,包括某些平台上的灾难性故障。 UEFI驱动程序中的内存分配应动态进行
在访问分配的内存之前,请始终检查函数返回码以验证内存分配请求是否成功
UEFI内存管理只在Boot阶段有效当调用
exit_boot_services后不在可用,部分分配的内存也会被释放
内存分配的关键点
- 为防止内存泄漏,每个分配操作必须具有相应的释放操作。必须注意某些UEFI服务会为调用方分配缓冲区,并让调用方释放这些内存
- 如果系统内存大于4 GB,则可以分配4 GB以上的缓冲区。因此,所有UEFI驱动程序必须注意指针可能包含4 GB以上的地址值,并且必须小心不要剥离高位地址
- 存储的结构和值必须在已分配内存中进行自然对齐,以最大程度地与所有CPU架构兼容
基本数据结构
MemoryType
MemoryType主要表示内存的范围, UEFI允许固件和操作系统在0x70000000…0xFFFFFFFF范围内的新内存类型。 单词我们在编译时不知道完整的内存类型集合,并且将此C枚举建模为Rust枚举是不安全的。
因此uefi-rs使用了newtype_enum宏
newtype_enum! {
pub enum MemoryType: u32 => {
/// 不使用此枚举变量.
RESERVED = 0,
/// 已加载的UEFI应用程序的代码部分.
LOADER_CODE = 1,
/// 加载的UEFI应用程序的数据部分以及它分配的任何内存
LOADER_DATA = 2,
/// 引导驱动程序的代码.
/// 加载操作系统后可以重复使用
BOOT_SERVICES_CODE = 3,
/// 用于存储启动驱动程序数据的内存.
/// 加载操作系统后可以重复使用
BOOT_SERVICES_DATA = 4,
/// 运行时驱动程序的代码.
RUNTIME_SERVICES_CODE = 5,
/// 运行时服务的代码.
RUNTIME_SERVICES_DATA = 6,
/// 可自由使用的内存.
CONVENTIONAL = 7,
/// 无法使用的内存(内存错误).
UNUSABLE = 8,
/// 存放ACPI表的内存(ACPI高级配置和电源管理接口).
/// 解析后可以回收
ACPI_RECLAIM = 9,
/// 固件保留的地址.
ACPI_NON_VOLATILE = 10,
/// 用于内存映射I / O的区域.
MMIO = 11,
/// 用于内存映射的端口I / O的地址空间.
MMIO_PORT_SPACE = 12,
/// 属于处理器的地址空间
PAL_CODE = 13,
/// 可用且非易失的存储区.
PERSISTENT_MEMORY = 14,
}
}
AllocateType
AllocateType 主要用于alloc_pages来分配内存
#[derive(Debug, Copy, Clone)]
pub enum AllocateType {
/// 分配任意的页面.
AnyPages,
/// 在给定地址以下的任何地址处分配页面.
MaxAddress(usize),
/// 在指定地址分配页面.
Address(usize),
}
MemoryAttribute
MemoryAttribute用于描述内存范围功能的标志
bitflags! {
pub struct MemoryAttribute: u64 {
/// 支持标记为不可缓存.
const UNCACHEABLE = 0x1;
/// 支持写合并.
const WRITE_COMBINE = 0x2;
/// 支持直写.
const WRITE_THROUGH = 0x4;
/// 支持回写.
const WRITE_BACK = 0x8;
/// 支持标记为不可缓存,已导出,并支持“获取并添加”信号量机制。
const UNCACHABLE_EXPORTED = 0x10;
/// 支持写保护.
const WRITE_PROTECT = 0x1000;
/// 支持读保护.
const READ_PROTECT = 0x2000;
/// 支持禁用代码执行.
const EXECUTE_PROTECT = 0x4000;
/// 永久内存.
const NON_VOLATILE = 0x8000;
/// 该存储器区域比其他存储器更可靠.
const MORE_RELIABLE = 0x10000;
/// 该内存范围可以设置为只读
const READ_ONLY = 0x20000;
/// 调用运行时服务时,操作系统必须映射此内存.
const RUNTIME = 0x8000_0000_0000_0000;
}
}
AllocatePool基本使用
分配1个页大小的切片
fn alloc_page(bt: &BootServices,) -> UefiResult<Result<&mut [u8]>>{
let ptr = bt.allocate_pool(MemoryType::LOADER_CODE,4096).log_warning()?;
ok( unsafe{ core::slice::from_raw_parts_mut(ptr,4096)})
}
释放内存的切片
fn alloc_and_free_page(bt: &BootServices,) -> UefiResult<Result<()>>{
let ptr = bt.allocate_pool(MemoryType::LOADER_CODE,4096).log_warning()?;
let slice = unsafe{ core::slice::from_raw_parts_mut(ptr,4096)};
bt.free_pool(slice.as_mut_ptr());
ok(())
}
allocate_pool还可以作为内存分配器使用例如在uefi-rs/src/alloc.rs中
use core::alloc::{GlobalAlloc, Layout};
#[global_allocator]
static ALLOCATOR: Allocator = Allocator;
pub struct Allocator;
unsafe impl GlobalAlloc for Allocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let mem_ty = MemoryType::LOADER_DATA;
let size = layout.size();
let align = layout.align();
// TODO: add support for other alignments.
if align > 8 {
// Unsupported alignment for allocation, UEFI can only allocate 8-byte aligned addresses
ptr::null_mut()
} else {
boot_services()
.as_ref()
.allocate_pool(mem_ty, size)
.warning_as_error()
.unwrap_or(ptr::null_mut())
}
}
unsafe fn dealloc(&self, ptr: *mut u8, _layout: Layout) {
boot_services()
.as_ref()
.free_pool(ptr)
.warning_as_error()
.unwrap();
}
}
使用global_allocator宏将Allocator声明为全局内存分配器,Allocator 必须要实现GlobalAlloc trait
这样我们可以可以使用alloc中的Vec了
#![no_std]
#![no_main]
#![feature(asm)]
#![feature(slice_patterns)]
#![feature(abi_efiapi)]
#![feature(never_type)]
#![feature(fn_traits)]
#[macro_use]
extern crate alloc;
#[macro_use]
extern crate log;
extern crate uefi;
#[entry]
fn efi_main(image: Handle, st: SystemTable<Boot>) -> Status {
if let Err(e) = uefi_services::init(&st).log_warning() {
info!("{:?}", e);
return e.status();
}
// 创建Vec
let mut values = vec![-5, 16, 23, 4, 0];
values.sort();
assert_eq!(values[..], [-5, 0, 4, 16, 23], "Failed to sort vector");
// 分配按照0x100对齐的结构体
#[repr(align(0x100))]
struct Block([u8; 0x100]);
let value = vec![Block([1; 0x100])];
assert_eq!(value.as_ptr() as usize % 0x100, 0, "Wrong alignment");
}
AllocatePages基本使用
OSLoader程序应分配类型为LOADER_DATA的内存。该函数会返回u64,即使在32位平台上也返回“ u64”,因为某些硬件配置(例如Intel PAE)在32位处理器上启用了64位物理寻址。
分配1个页大小的切片
fn alloc_one_page(bt: &BootServices,) -> UefiResult<Result<&mut [u8]>>{
let page = bt.allocate_pages(AllocateType::AnyPages,MemoryType::LOADER_DATA,1).log_warning()?;
ok(unsafe{ core::slice::from_raw_parts_mut(page as *mut u8,4096)})
}
该函数在任意位置申请1个页大小的LOADER_DATA类型的内存,然后使用slice提供的from_raw_parts_mut将该内存转为切片
释放内存的切片
fn alloc_and_free_page(bt: &BootServices,) -> UefiResult<Result<()>>{
let page = bt.allocate_pages(AllocateType::AnyPages,MemoryType::CONVENTIONAL,1).log_warning()?;
let slice = unsafe{ core::slice::from_raw_parts_mut(page as *mut u8,4096)}
bt.free_pages(slice.as_ptr() as u64,slice.len()).log_warning()?;
ok(())
}
Protocol
Protocol的介绍如下
protocol是server和client之间的一种约定,双方根据这种约定互通信息。这里的server和client是一种广义的称呼,提供服务的称为server,使用服务的称为client。 TCP是一种protocol, client(应用程序)通过一组函数来压包和解包,压包和解包是server提供的服务,COM也是一种protocol,client通过CoCreateInstance(…)和GUID获得指向COM对象的指针,然后使用该指针获得COM对象提供的服务, GUID标示了这个COM对象
在uefi-rs中获取一个Protocol方法如下以SimpleFileSystem为例
#[entry]
fn efi_main(image: Handle, st: SystemTable<Boot>) -> Status{
if let Err(e) = uefi_services::init(&st).log_warning(){
info!("{:?}",e);
// 如果服务初始化失败后则不能继续执行
return e.status();
}
// 获取BootServices
let bt = st.boot_services();
// 通过GUID来获取SimpleFileSystem (这里把结果写出来了方便了解该函数的返回值)
let fs: &UnsafeCell<SimpleFileSystem> = bt.locate_protocol::<SimpleFileSystem>().log_warning().unwrap();
//
let f: &mut SimpleFileSystem = unsafe{ &mut *fs.get() };
Status::SUCCESS
}
这样便可以使用SimpleFileSystem Protocol
我们编写最简单的内核加载器只需要SimpleFileSystem因此我们需要只讲述SimpleFileSystemProtocol的使用(用于读取内核文件)
SimpleFileSystem Protocol
SimpleFileSystem 提供了一个最基础的文件系统,该文件系统的路径分隔符是反斜杠\(与windows一致),文件系统的基本使用步骤如下
- 通过
bt.locate_protocol::<SimpleFileSystem>()来获取SimpleFileSystem 实例 - 使用
open_volume访问根目录(我们在QEMU中指定esp为根目录) - 创建一个缓冲区用于存放读取的数据,然后使用
read_entry开始读取 - 使用
open打开文件(在我们的例子中以esp根目录) - 处理文件
基本数据结构
在读取文件的过程中需要接触到以下数据结构
- Directory: 通过字面意识得知
Directory用来表示文件夹定义在uefi-rs/src/proto/media/file/dir.rs中结,定为pub struct Directory(RegularFile),可以知道Directory是对RegularFile的一次封装,主要用于读取文件夹中的内容 - RegularFile:
RegularFile是对文件的抽象,文件夹也是一种文件,普通文件也属于一种文件,uefi-rs种使用FileType来区分是普通文件还是文件夹,RegularFile定义在uefi-rs/src/proto/media/file/regular.rs,签名为pub struct RegularFile(FileHandle),RegularFile也是对FileHandle的一次封装,主要用于读取数据文件 - FileHandle:
FileHandleVolume上某些连续数据块的不透明句柄(对用户不透明),定义在uefi-rs/src/proto/media/file/mod.rs中,签名为pub struct FileHandle(*mut FileImpl),FileHandle也是对FileHandle的一次封装FileImpl(禁止套娃2333),主要用途是使用into_type获取FileType - FileInfo: FileInfo为文件的详细信息包括文件名(使用的是
UCS-2编码),文件大小,创建时间,访问时间,文件属性等等信息,通过Directory的read_entry来获取 - FileType: FileType用来区分普通文件和文件夹,定义如下
pub enum FileType {
/// The file was a regular (data) file.
Regular(RegularFile),
/// The file was a directory.
Dir(Directory),
}
- FileMode: 用于表示文件的操作形式,定义如下
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
#[repr(u64)]
pub enum FileMode {
/// The file can be read from
Read = 1,
/// The file can be read from and written to
ReadWrite = 2 | 1,
/// The file can be read, written, and will be created if it does not exist
CreateReadWrite = (1 << 63) | 2 | 1,
}
以下我们通过几个例子来说明SimpleFileSystem 的使用
遍历文件夹内容
这个例子用于遍历EFI/Boot文件夹的所有内容
// 该函数用于分配一个页大小的内存
pub fn alloc_one_page(bt: &BootServices) -> Result<&mut [u8; 4096]> {
let page = bt.allocate_pages(AllocateType::AnyPages, MemoryType::LOADER_DATA, 1).log_warning()?;
let data = unsafe { &mut *(page as *mut [u8; 4096]) };
Ok(Completion::new(Status::SUCCESS, data))
}
// 用于分配指定大小的内存 按页对齐
pub fn alloc_size(bt: &BootServices, mem_ty: MemoryType, want: usize) -> Result<&mut [u8]> {
let ptr = bt.allocate_pool(mem_ty,want).log_warning()?;
let data = unsafe { core::slice::from_raw_parts_mut(ptr as *mut u8, page_num) };
Ok(Completion::new(Status::SUCCESS, data))
}
fn walk(bt:&BootServices) -> UefiResult<Result<()>>{
//1. 通过locate_protocol获取SimpleFileSystem实例
let f = unsafe{&mut *bt.locate_protocol::<SimpleFileSystem>().log_warning()?.get()};
// 2. open_volume打开根目录
let mut volume = f.open_volume().log_warning()?;
// 3. 申请缓冲区 读取的文件夹数据会存于此
let en_buff = alloc_one_page(bt).log_warning()?;
// 4. 读取 根目录文件夹中的内容并放在缓冲区中
let dir = match volume.read_entry(en_buff).log_warning(){
Ok(info)=> info.unwrap(),
Err(e) => {
// 如果状态码为BUFFER_TOO_SMALL则表示声明的缓冲区过小
if e.status() == Status::BUFFER_TOO_SMALL{
// data将会返回所需要的缓冲区大小
let size = e.data().unwrap();
// 释放之前申请的缓冲区
let ptr = en_buff.as_ptr() as u64;;
bt.free_pages(ptr,1);
// 重新申请新的缓冲区
let buffer = alloc_size(bt,MemoryType::LOADER_DATA,size).log_warning()?;
// 再次读取 如果这次读取失败只会是 坏的数据卷,硬件设备错误 没有该media 中的一个
volume.read_entry(buffer).log_warning().unwrap().unwrap()
}else{
// 坏的数据卷,硬件设备错误 ,没有该media等错误无法处理只能panic
panic!("{:?}",e);
}
}
};
// 5. 以只读模式打开 EFI\Boot 目录
let efi = volume.open(r"EFI\Boot", FileMode::Read, dir.attribute()).log_warning()?.into_type().log_warning()?;
// 6. 获取具体的文件类型
let ty = efi.into_type().log_warning()?;
if let FileType::Dir(mut sub) = ty {
// 7. 读取文件夹中的内容
while let Ok(f_info) = sub.read_entry(en_buff) {
// 8. 获取读取的结果
let (status, f) = f_info.split();
if status == Status::SUCCESS {
let f = f.unwrap();
// 9. 如果读取成功 将当前文件的文件名转为u16切片 uefi使用的是UCS-2编码
// UCS-2是一种定长的编码方式,UCS-2仅仅简单的使用一个16位码元来表示码位,也就是说在0到0xFFFF的码位范围内,它和UTF-16基本一致,因此我们可以使用String::from_utf16_lossy来转为UTF-16编码
let u16_slice = files.file_name().to_u16_slice();
// 10. 将UCS-2转为UTF-16编码
let name = String::from_utf16_lossy(files.file_name().to_u16_slice());
info!("{}",name);
}
}
}else{
info!("is not folder!")
}
}
读取文件内容
fn read_file(bt: &BootServices, filename: &str) -> UefiResult<Result<Vec<u8>>> {
//1. 通过locate_protocol获取SimpleFileSystem实例
let f = unsafe{&mut *bt.locate_protocol::<SimpleFileSystem>().log_warning()?.get()};
// 2. open_volume打开根目录
let mut volume = f.open_volume().log_warning()?;
// 3. 申请缓冲区 读取的文件夹数据会存于此
let en_buff = alloc_one_page(bt).log_warning()?;
// 4. 读取 根目录文件夹中的内容并放在缓冲区中
let dir = match volume.read_entry(en_buff).log_warning(){
Ok(info)=> info.unwrap(),
Err(e) => {
// 如果状态码为BUFFER_TOO_SMALL则表示声明的缓冲区过小
if e.status() == Status::BUFFER_TOO_SMALL{
// data将会返回所需要的缓冲区大小
let size = e.data().unwrap();
// 释放之前申请的缓冲区
let ptr = en_buff.as_ptr() as u64;;
bt.free_pages(ptr,1);
// 重新申请新的缓冲区
let buffer = alloc_size(bt,MemoryType::LOADER_DATA,size).log_warning()?;
// 再次读取 如果这次读取失败只会是 坏的数据卷,硬件设备错误 没有该media 中的一个
volume.read_entry(buffer).log_warning().unwrap().unwrap()
}else{
// 坏的数据卷,硬件设备错误 ,没有该media等错误无法处理只能panic
panic!("{:?}",e);
}
}
};
// 5. 打开指定文件
let file = volume.open(filename, FileMode::Read, dir.attribute()).log_warning()?;
match file.into_type().log_warning()? {
FileType::Regular(mut k_file) => {
// 获取文件大小
let file_size = dir.file_size() as usize;
// 分配内存
if let Ok(buffer) = alloc_size(bt,MemoryType::LOADER_DATA, file_size as usize).log_warning() {
// 读取文件数据到缓冲区,该read方法会尽可能多的读取
match k_file.read(buffer) {
// 如果读取成功直接返回读取的数据
Ok(s) => { return ok(Vec::from(buffer)); }
Err(e) => {
// 如果分配的缓冲区过小则重新分配
if e.status() == Status::BUFFER_TOO_SMALL {
let size = e.data().unwrap();
let ptr = buffer.as_ptr() as u64;
let pages = file_size / 4096;
bt.free_pages(ptr, pages);
let buffer = mem::alloc_size(bt, MemoryType::LOADER_DATA, size).log_warning()?;
k_file.read(buffer).log_warning().unwrap();
return ok(Vec::from(buffer));
} else {
panic!("{:?}", e)
}
}
}
}
}
FileType::Dir(_) => {
// 如果检测到的是文件夹说明不是普通文件,这里也可以不用panic可以自定义异常
panic!("{} is not regular file!", filename)
}
}
// 如果到了这一步则说明没找到该文件
err::<Vec<u8>>(Error::NoSuchFile)
}
这样我们完成了关于UEFI的内存管理和文件系统的基本使用
下一步要做什么
在下一篇文章中我们将要开发OSLoader所需要的基本功能,主要的工作是封装对FS和内存的操作使用起来更加简单
本文介绍了使用Rust开发UEFI操作系统的内存管理和文件系统使用。主要内容包括:改造Result结构以适应UEFI服务,讲解了AllocatePool和AllocatePages的内存分配方法,以及如何使用SimpleFileSystem Protocol读取文件。通过示例展示了遍历文件夹和读取文件内容的方法,为后续的OSLoader开发打下基础。
1834

被折叠的 条评论
为什么被折叠?



