使用Rust开发操作系统(UEFI内存管理和文件系统使用)

本文介绍了使用Rust开发UEFI操作系统的内存管理和文件系统使用。主要内容包括:改造Result结构以适应UEFI服务,讲解了AllocatePool和AllocatePages的内存分配方法,以及如何使用SimpleFileSystem Protocol读取文件。通过示例展示了遍历文件夹和读取文件内容的方法,为后续的OSLoader开发打下基础。
AI助手已提取文章相关产品:

在上一篇文章中我们简单介绍了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,
}

然后我们提供okerr函数来简化

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一致),文件系统的基本使用步骤如下

  1. 通过bt.locate_protocol::<SimpleFileSystem>()来获取SimpleFileSystem 实例
  2. 使用open_volume访问根目录(我们在QEMU中指定esp为根目录)
  3. 创建一个缓冲区用于存放读取的数据,然后使用read_entry开始读取
  4. 使用open打开文件(在我们的例子中以esp根目录)
  5. 处理文件
基本数据结构

在读取文件的过程中需要接触到以下数据结构

  • Directory: 通过字面意识得知Directory用来表示文件夹定义在uefi-rs/src/proto/media/file/dir.rs中结,定为pub struct Directory(RegularFile),可以知道Directory是对RegularFile的一次封装,主要用于读取文件夹中的内容
  • RegularFileRegularFile是对文件的抽象,文件夹也是一种文件,普通文件也属于一种文件,uefi-rs种使用FileType来区分是普通文件还是文件夹,RegularFile定义在uefi-rs/src/proto/media/file/regular.rs,签名为pub struct RegularFile(FileHandle),RegularFile也是对FileHandle的一次封装,主要用于读取数据文件
  • FileHandleFileHandle Volume上某些连续数据块的不透明句柄(对用户不透明),定义在uefi-rs/src/proto/media/file/mod.rs中,签名为pub struct FileHandle(*mut FileImpl) ,FileHandle也是对FileHandle的一次封装FileImpl(禁止套娃2333),主要用途是使用into_type获取FileType
  • FileInfo: FileInfo为文件的详细信息包括文件名(使用的是UCS-2编码),文件大小,创建时间,访问时间,文件属性等等信息,通过Directoryread_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和内存的操作使用起来更加简单

您可能感兴趣的与本文相关内容

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值