使用Rust开发操作系统(UEFI抽象文件系统)

在上一篇文章中我们介绍了rust在uefi中的基本使用,在本章中我们开始编写UEFI基础设施,主要包括File结构和uefi编译脚本用来简化内核文件读取和qemu启动过程

建立基础异常

在标准库中为我们提供了Result,但是在UEFI开发中将Result分成了2种,UEFI服务执行的Result和用户自定义的Result,在本节中我们仿照标准库的io::Result建立我们自己的Result

设计思路

我们设计的Result除了满足我们程序基本的使用以外还要兼容UEFI服务的Result因此我们可以通过type来重新命令2种Result,其次我们要兼容UEFI服务的错误,但是还是要区分UEFI异常和UEFI应用异常,因此我们需要设计一个枚举Repr来区分两种异常

根据Result我们可以轻松定义出如下结构

// 表示UEFI服务使用的Result
pub type UefiResult = uefi::Result;
// 表示UEFI应用程序使用的Result
pub type Result = core::result::Result;

第二个ResultError为我们自定义的ErrorError分为2种UEFI执行中的错误以及应用程序自定义的错误,因此我们可以使用一个枚举来完成

// 用于表示异常的种类,分为UEFI执行错误和UEFI应用程序错误
#[derive(Debug)]
enum Repr {
    /// UEFI服务错误
    Uefi(Efi),
    /// UEFI应用错误
    Custom(Custom),
}

Repr中包含2个结构体EfiCustom结构定义如下

#[derive(Debug, Clone)]
struct Custom {
	/// UEFI应用执行错误类型
    kind: ErrorKind,
    /// UEFI应用执行错误说明
    err_msg: String,
}

#[derive(Debug, Clone)]
struct Efi {
	/// UEFI服务执行结果状态码
    status: Status,
    /// UEFI状态对应说明,如果为None使用默认的提示信息,
    err_msg: Option,
}

// 用于表示UEFI应用异常类型
#[derive(Clone, Copy, Debug, PartialOrd, PartialEq)]
pub enum ErrorKind {
	/// 文件未找到
    NotFound,
    /// 在读取文件中读取到了EOF
    UnexpectedEof,
    /// 无效的文件
    InvalidFile,
    /// 无效的文件模式
    InvalidFileMode,
    /// UEFI错误,包含错误码
    UefiErrorCode,
    /// 终止
    Interrupted,
}

CustomEfi主要的不同在于kind为我们自定义的错误类型,Status表示UEFI服务执行错误的状态码(Status其实是Rust与C的枚举的映射,但是uefi-rs中不是简单地与C枚举映射因为会产生UB(undefined behaviour),而是采取了newtype_enum宏进行处理)

紧接着我们为2种结构提供as_str()方法,ErrorKind的实现比较简单,以下的部分代码

impl ErrorKind {
    pub fn as_str(&self) -> &'static str {
        match *self {
            ErrorKind::NotFound => "entity not found",
            ErrorKind::UnexpectedEof => "unexpected end of file",
            ....
        }
  }
}

但是实现Efi结构的as_str是有些不同了,我们不能写出这样的代码

impl Efi {
    pub fn as_str(&self) -> &'static str {
        match self.status {
        	Status:: WARN_UNKNOWN_GLYPH => "The string contained characters that could not be rendered and were skipped."
        	.....
        }
     }
}

在上面我们也提到了,如果简单的看源码Status确实像枚举,但是其实是结构体,因此我们要判断为Status中具体的值

Status:: WARN_UNKNOWN_GLYPH为例,经过宏扩展后结果为pub struct WARN_UNKNOWN_GLYPH(pub usize),因此我们要以结构体的思路来处理

考虑到完整性列出了所有Status的结果

const ERROR_BIT: usize = 1 << (core::mem::size_of::() * 8 - 1);


impl Efi {
    pub fn as_str(&self) -> &'static str {
        match self.status.0 {
            0 => "The operation completed successfully.",
            1 => "The string contained characters that could not be rendered and were skipped.",
            2 => "The handle was closed, but the file was not deleted.",
            3 => "The handle was closed, but the data to the file was not flushed properly.",
            4 => "The resulting buffer was too small, and the data was truncated.",
            5 => "The data has not been updated within the timeframe set by local policy.",
            6 => "The resulting buffer contains UEFI-compliant file system.",
            7 => "The operation will be processed across a system reset.",
            ERROR_BIT | 1 => "The image failed to load.",
            ERROR_BIT | 2 => "A parameter was incorrect.",
            ERROR_BIT | 3 => "The operation is not supported.",
            ERROR_BIT | 4 => "The buffer was not the proper size for the request.The buffer is not large enough to hold the requested data.",
            ERROR_BIT | 5 => "The required buffer size is returned in the appropriate parameter.",
            ERROR_BIT | 6 => "There is no data pending upon return.",
            ERROR_BIT | 7 => "The physical device reported an error while attempting the operation.",
            ERROR_BIT | 8 => "The device cannot be written to.",
            ERROR_BIT | 9 => "A resource has run out.",
            ERROR_BIT | 10 => "An inconstency was detected on the file system.",
            ERROR_BIT | 11 => "There is no more space on the file system.",
            ERROR_BIT | 12 => "The device does not contain any medium to perform the operation.",
            ERROR_BIT | 13 => "The medium in the device has changed since the last access.",
            ERROR_BIT | 14 => "The item was not found.",
            ERROR_BIT | 15 => "Access was denied.",
            ERROR_BIT | 16 => "The server was not found or did not respond to the request.",
            ERROR_BIT | 17 => "A mapping to a device does not exist.",
            ERROR_BIT | 18 => "The timeout time expired.",
            ERROR_BIT | 19 => "The protocol has not been started.",
            ERROR_BIT | 20 => "The protocol has already been started.",
            ERROR_BIT | 21 => "The operation was aborted.",
            ERROR_BIT | 22 => "An ICMP error occurred during the network operation.",
            ERROR_BIT | 23 => "A TFTP error occurred during the network operation.",
            ERROR_BIT | 24 => "A protocol error occurred during the network operation. The function encountered an internal version that was",
            ERROR_BIT | 25 => "incompatible with a version requested by the caller.",
            ERROR_BIT | 26 => "The function was not performed due to a security violation.",
            ERROR_BIT | 27 => "A CRC error was detected.",
            ERROR_BIT | 28 => "Beginning or end of media was reached",
            ERROR_BIT | 31 => "The end of the file was reached.",
            ERROR_BIT | 32 => "The language specified was invalid.The security status of the data is unknown or compromised and",
            ERROR_BIT | 33 => "the data must be updated or replaced to restore a valid security status.",
            ERROR_BIT | 34 => "There is an address conflict address allocation",
            ERROR_BIT | 35 => "A HTTP error occurred during the network operation.",
            _ => "Unknown status"
        }
    }
}

ERROR_BIT定义在uefi-rs/src/result/status.rs中该结构并不是pub的,我们因此我们在我们自己的文件中重新定义了该结构(只用作读取不会用作其他用途)

这样我们可以定义出Error的结构

#[derive(Debug)]
pub struct Error {
    repr: Repr,
}

最后我们提供Error配套的方法

impl Error {
	/// 根据给定的错误类型创建UEFI应用异常
    pub fn new(kind: ErrorKind, msg: &str) -> Error {
        Error { repr: Repr::Custom(Custom { kind, err_msg: msg.to_string() }) }
    }
	/// 根据给定的错误类型创建UEFI应用异常 支持String 主要方便使用format!
    pub fn with_string(kind: ErrorKind, msg: String) -> Error {
        Error { repr: Repr::Custom(Custom { kind, err_msg: msg }) }
    }
    /// 根据传递的状态码创建UEFI服务错误
    pub fn from_uefi_status(status: Status, msg: Option<&str>) -> Error {
        Error {
            repr: Repr::Uefi(Efi {
                status,
                err_msg: match msg {
                    Some(msg) => Some(msg.to_string()),
                    None => None,
                },
            })
        }
    }
	/// 提供错误类型的判断
    pub fn kind(&self) -> ErrorKind {
        match self.repr {
            Repr::Uefi(ref efi) => ErrorKind::UefiErrorCode,
            Repr::Custom(ref cu) => cu.kind,
        }
    }
}

为了发生错误时能够显示错误信息我们要为Error实现Displaytrait

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self.repr {
            Repr::Custom(ref cu) => {
                write!(f, "{}: {}", cu.kind.as_str(), cu.err_msg)
            }
            Repr::Uefi(ref efi) => {
                match efi.err_msg {
                    None => write!(f, "got uefi status `{}` info: {}", efi.status.0, efi.as_str()),
                    Some(ref other) => write!(f, "got uefi status `{}` info: {}", efi.status.0, other),
                }
            }
        }
    }
}

经过修改后就不能使用以前的Ok(T)Err(T)的(有一种思路就是将UefiResultResult和并,这样可以使照常使用,未经过测试可行性

pub fn ok(t: T) -> UefiResult> {
    Ok(Completion::from(core::result::Result::Ok(t)))
}

pub fn err(e: Error) -> UefiResult> {
    Ok(Completion::from(core::result::Result::Err(e)))
}

这样我们我们Result设计就完成了

封装文件操作

文件的操作我们可以参考标准库,将文件的操作抽象成3个trait来实现分别是Read,Write,Seek
read主要提供文件读取操作,write主要提供文件写入操作,Seek提供文件指针移动操作

pub trait Seek {
	/// 指定文件/读取写入位置
    fn seek(&mut self, pos: SeekFrom) -> Result<()>;
}

pub trait Read {
	/// 读取尽可能多的文件数据并填充到`but`中,返回读取的字节数
	/// #Error
	/// 如果指定的缓冲区过小则返回`BUFFER_TOO_SMALL`并返回所需要的`size`
    fn read(&mut self, buf: &mut [u8]) -> Result;
}

pub trait Write {
	/// 将`buf`中的数据写入到文件中,返回写入的字节数
	/// 在写入的过程发生错误会返回已写入的字节数
    fn write(&mut self, buf: &[u8]) -> usize;
	/// 将所有写入的数据刷新到设备
    fn flush(&mut self) -> Result<()>;
}

我们定义出了每个trait基本的功能,后续我们会丰富我们的trait功能

在结构方面我们需要将文件操作和文件读写操作分离主要原因是uefi-rs提供的open方法也适用于文件夹操作,因此File主要用于文件夹和文件的操作,FileOperator主要完成文件读写操作,File定义如下

/// 适用于文件和文件夹
pub struct File {
	/// 根目录
    root: Directory,
    /// 缓冲区用于存储已打开目录信息
    buffer: Vec,
}

File的new函数只需要从BootServices中获取SimpleFileSystemProtocol并且读取打开根目录即可

impl File {
    /// new函数和try_new函数的辅助操作
    fn _new(bt: &BootServices) -> UefiResult {
    	// 获取SimpleFileSystemProtocol
        let f = unsafe { &mut *bt.locate_protocol::().log_warning()?.get() };
        // 打开根目录
        let mut volume = f.open_volume().log_warning()?;
        Ok(Completion::from(File {
            root: volume,
            buffer: vec![0_u8; DEFAULT_BUFFER_SIZE],
        }))
    }
 }

这里的_new函数只是一个辅助操作,因为我们要提供newtry_new2个函数定义如下

impl File {
    /// 根据BootServices创建File实例,
    /// # Panic
    /// 当尝试读取根目录失败后,文件系统出错,设备驱动等错误均会导致panic
    pub fn new(bt: &BootServices) -> Self {
        match File::try_new(bt) {
            Ok(f) => f,
            Err(e) => {
                panic!("occur an error during open root folder! {}", e);
            }
        }
    }
    /// new的方法
    pub fn try_new(bt: &BootServices) -> Result {
        match Self::_new(bt).log_warning() {
            Ok(f) => Ok(f),
            Err(e) => Err(Error::from_uefi_status(e.status(), None)),
        }
    }
}

经过try_new函数后我们将底层的uefi服务错误转为uefi应用错误,这样我们不需要处理层层嵌套的Result
当然,我们也可以提供可以指定的缓冲区大小的函数

impl File {
    /// with_buffer_capacity的辅助函数
    fn capacity(bt: &BootServices, size: usize) -> UefiResult {
        let f = unsafe { &mut *bt.locate_protocol::().log_warning()?.get() };
        let volume = f.open_volume().log_warning()?;
        Ok(Completion::from(File {
            root: volume,
            buffer: vec![0_u8; size],
        }))
    }

    /// 指定缓冲区容量大小
    pub fn with_buffer_capacity(bt: &BootServices, size: usize) -> Result {
        match Self::capacity(bt, size).log_warning() {
            Ok(f) => Ok(f),
            Err(e) => Err(Error::from_uefi_status(e.status(), None))
        }
    }
 }

打开指定文件

open函数较为复杂,因此需要3个辅助函数来完成,这三个辅助将要完成以下操作

  • 读取根目录中的文件信息FileInfo
  • 获取文件的属性FileAttribute
  • 根据指定的文件模式和文件属性打开指定路径的文件

首先我们完成第一步: 读取根目录中的文件信息FileInfo

impl File{
	/// 读取根目录信息
    fn read_entry(&mut self) -> Result<&mut FileInfo> {
        return match self.root.read_entry(self.buffer.as_mut_slice()).log_warning() {
            Ok(info) => {
                if let Some(f) = info { Ok(f) } else { Err(Error::new(ErrorKind::NotFound, "the file info header not found!")) }
            }
            Err(e) => Err(Error::from_uefi_status(e.status(), None))
        };
    }
}

第二步:获取文件的属性FileAttribute

impl File{
	/// 读取根目录属性
    fn root_attribute(&mut self) -> Result {
        match self.read_entry() {
            Ok(info) => Ok(info.attribute()),
            Err(e) => Err(e),
        }
    }
}

第三步:

impl File{
	fn _open(&mut self, filename: &str, mode: FileMode, mut attr: FileAttribute) -> UefiResult> {
		// 如果指定的模式为写模式需要更改属性值
        if let FileMode::CreateReadWrite = mode {
            attr = FileAttribute::VALID_ATTR;
        }

        return match self.root.open(filename, mode, attr).log_warning() {
            Ok(handle) => {
            	// 这里只处理文件的情况,如果指定的是文件夹则返回ErrorKind::InvalidFile
                match handle.into_type().log_warning()? {
                    FileType::Dir(_) => {
                        return err(Error::new(ErrorKind::InvalidFile, "except file found folder, if you want create folder please use `mkdir` method if you want read folder please use `walk` method"));
                    }
                    FileType::Regular(file) => {
                        ok(FileOperator { file, current: 0 })
                    }
                }
            }
            Err(e) => {
                err(Error::from_uefi_status(e.status(), None))
            }
        };
    }
}

最后我们将open函数定义出来

    pub fn open(&mut self, filename: &str, mode: &str) -> UefiResult> {
    	// 获取文件属性
        let attr = self.root_attribute().unwrap();
        let f_mode = match mode {
            "r" => FileMode::Read,
            "w" => FileMode::ReadWrite,
            "c" => FileMode::CreateReadWrite,
            other => return err(Error::new(ErrorKind::InvalidFileMode, format!("No Support mode: `{}`", other.clone()).as_str())),
        };
        self._open(filename, f_mode, attr)
    }

值得注意的是root_attribute,open_open函数中的内容并不能写在一个函数中,编译器会提示一次可变借用,一次不可变借用,以及FileInfo not implement copy trait等错误
我们注意到open的返回值是FileOperator它的定义如下

pub struct FileOperator {
	/// 已文件实例
    file: RegularFile,
    /// 文件指针位置
    current: u64,
}

FileOperator将会实现Read,Write,Seek等trait,实现较为简单,这里不做过多阐述

Seek

/// 将此文件句柄的游标的位置设置为指定的绝对位置。
/// 允许使用`End`将游标设置超过文件末尾的位置,它将在下次写入时触发文件增长。
///
/// * `SeekFrom::Start(size)` 将游标移至文件起始位置`size`个字节处
/// * `SeekFrom::End(size)` 将游标移至设置为此对象的大小加上指定的`size`个字节处
/// * `SeekFrom::Current(size)` 将游标移至设置为当前位置加上指定的`size`个字节处
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum SeekFrom {
    Start(u64),
    End(u64),
    Current(u64),
}

impl Seek for FileOperator {
    fn seek(&mut self, pos: SeekFrom) -> Result<()> {
        let result = match pos {
            SeekFrom::Start(p) => self.file.set_position(p).log_warning(),
            SeekFrom::End(p) => self.file.set_position(RegularFile::END_OF_FILE + p).log_warning(),
            SeekFrom::Current(p) => self.file.set_position(self.current + p).log_warning(),
        };
        match result {
            Ok(_) => Ok(()),
            Err(e) => Err(Error::from_uefi_status(e.status(), None))
        }
    }
}

Read

impl Read for FileOperator {
    fn read(&mut self, buf: &mut [u8]) -> Result {
        match self.file.read(buf).log_warning() {
            Ok(size) => {
                self.current += size as u64;
                Ok(size)
            }
            Err(e) => {
                match e.data() {
                    Some(size) => Err(Error::from_uefi_status(e.status(), Some(format!("buffer to small need {}", size).as_str()))),
                    None => Err(Error::from_uefi_status(e.status(), None))
                }
            }
        }
    }
}

Write

impl Write for FileOperator {
    fn write(&mut self, buf: &[u8]) -> usize {
        match self.file.write(buf).log_warning() {
            Ok(_) => buf.len(),
            Err(size) => *size.data()
        }
    }

    fn flush(&mut self) -> Result<()> {
        match self.file.flush().log_warning() {
            Ok(()) => Ok(()),
            Err(e) => Err(Error::from_uefi_status(e.status(), None))
        }
    }
}

这些主要复用SimpleFileSystem提供的方法,我们主要的关注点是设计并提供便利的API,我们首先来设计读取的api

扩充Read trait

read_exact主要读取指定的字节数并填充到给定的切片中,以下是read_exact流程图
使用Rust开发操作系统(UEFI抽象文件系统)_第1张图片

/// 读取指定字节数
/// 将读取到的数据填充至`buf`中
fn read_exact(&mut self, mut buf: &mut [u8]) -> Result<()> {
   // buf是否填充完毕?
    while !buf.is_empty() {
    	// 读取并填充数据
        match self.read(buf){
            Ok(0) => break,
            Ok(n) => {
                let tmp = buf;
                buf = &mut tmp[n..];
            }
            Err(e) => return Err(e),
        }
    }
    // buf是否为空?
    if !buf.is_empty() {
        Err(Error::new(ErrorKind::UnexpectedEof, "failed to fill whole buffer"))
    } else {
        Ok(())
    }
}

read_to_end读取所有字节,直到碰到EOF,将读取到的数据填充至buf中,以下是read_to_end的流程图
使用Rust开发操作系统(UEFI抽象文件系统)_第2张图片
read_to_end中的难点便是扩容操作,扩容操作总共执行2个步骤

  1. 使用GlobalAllocator申请新的内存空间
  2. 将对新申请的内存空间进行初始化(全部置0)

其次我们需要记录每次读取的字节数,以便准确表达缓冲区大小,在扩容时我们只会关注与缓冲区的容量而非大小,但是在用户使用时只关注缓冲区的大小(即从文件中读取的字节数)
因此我们需要一个新的数据结构来完成这个操作,它将完成2个操作

  1. 保存读取的内容并记录每次读取的字节数
  2. 在读取完毕后设置缓冲区大小
    我们可以得到这样的结构
struct Guard<'a> {
    buf: &'a mut Vec,
    len: usize,
}

Guard将会完成以上2个操作,设置缓冲区大小的时机应该是Guard生命周期结束的时候,因此在Guard被Drop前需要设置缓冲区大小,因此我们可以重写Drop trait

impl Drop for Guard<'_> {
    fn drop(&mut self) {
        unsafe {
            self.buf.set_len(self.len);
        }
    }
}

我们现在还有一个扩容问题需要解决,在alloc库中已经为Vec实现了扩容的函数reserve,在扩容后需要重新设置缓冲区的容量
随后将新申请的内存空间初始化,因此我们还需要一个用于初始化的数据结构Initializer,定义如下

#[derive(Debug)]
pub struct Initializer(bool);

其中的bool值表示是否需要初始化操作,然后我们需要提供两个初始化函数,zeronop,zero函数用于表示对内存进行初始化,nop表示不需要对内存进行初始化,因此对于nop函数而言是unsafe操作

impl Initializer {
    /// 表明需要对缓冲区进行初始化操作
    #[inline]
    pub fn zeroing() -> Initializer {
        Initializer(true)
    }
    /// 表明不会对缓冲区进行初始化操作
    #[inline]
    pub unsafe fn nop() -> Initializer {
        Initializer(false)
    }
    /// 表示缓冲区是否应该被初始化
    #[inline]
    pub fn should_initialize(&self) -> bool {
        self.0
    }
}

最后我们实现初始化功能initialize

    /// 如果需要的话会始化缓冲区(根据缓冲区长度将值设为0)
    #[inline]
    pub fn initialize(&self, buf: &mut [u8]) {
        if self.should_initialize() {
            unsafe { ptr::write_bytes(buf.as_mut_ptr(), 0, buf.len()) }
        }
    }

初始化功能较为简单,在这里不在赘述

那么我们的扩容操作可以这样编写,以下是伪代码

let r = Initializer::zeroing();
let mut g = 初始化 Guard;
if 缓冲区容量不足 {
    // 进行扩容
    g.buf.reserve(需要扩充的字节数);
    // 获得扩容后的缓冲区最大容量
    let capacity = g.buf.capacity();
    unsafe {
        // 设置缓冲区容量
        g.buf.set_len(capacity);
        // 初始化缓冲区新扩容的内存 只需要初始化新增的内存
        r.initializer().initialize(&mut g.buf[g.len..]);
    }
}

这里缓冲区容量不足的判断条件便是 Guard中缓冲区的长度等于缓冲区的长度
相应的我们每次读取的时候只需要将数据读取套新增的内存中,因此我们读取操作可以写成这样

let start_len = 初始缓冲区大小
// 将数据读取至扩容部分
let r = FileOperator实例
let ret:Option;
//  只需要读取新增的内存
match r.read(&mut g.buf[g.len..]){
	// 没有读取到数据
    Ok(0) => {
    	// 读取的内存大小 = 读取后的大小 - 读取前的大小 
        ret = Ok(g.len - start_len);
        break;
    }
    // 记录每次读取的字节数
    Ok(n) => g.len += n,
    Err(e) => {
        ret = Err(e);
        break;
    }
}

这样我们可以形成read_to_end_with_reservation函数

fn read_to_end_with_reservation(r: &mut R, buf: &mut Vec, mut reservation_size: F) -> Result 
		where R: Read + ?Sized, F: FnMut(&R) -> usize
{
    let start_len = buf.len();
    let mut g = Guard { len: buf.len(), buf };
    let ret:Result;
    loop {
        // 缓冲区的长度等于缓冲区的长度时需要进行扩容
        if g.len == g.buf.len() {
            // 进行扩容
            g.buf.reserve(reservation_size(r));
            // 获得扩容后的缓冲区最大容量
            let capacity = g.buf.capacity();
            unsafe {
                // 设置缓冲区容量
                g.buf.set_len(capacity);
                // 初始化缓冲区新扩容的内存 只需要初始化新增的内存
                r.initializer().initialize(&mut g.buf[g.len..]);
            }
        }
        // 将数据读取至扩容部分
        match r.read(&mut g.buf[g.len..]){
        	// 没有读取到数据
            Ok(0) => {
            	// // 读取的内存大小 = 读取后的大小 - 读取前的大小 
                ret = Ok(g.len - start_len);
                break;
            }
            // 记录每次读取的字节数
            Ok(n) => g.len += n,
            Err(e) => {
                ret = Err(e);
                break;
            }
        }
    }

    ret
}

read_to_end_with_reservation是read_to_end的辅助函数其中泛型参数F: FnMut(&R)-> usize是用来指定扩容的大小

相应的我们的Readtrait中需要增加初始化的函数(包含之前编写的read_exact函数)

pub trait Read {
    /// 读取尽可能多的文件数据并填充到`but`中,返回读取的字节数
    /// #Error
    /// 如果指定的缓冲区过小则返回`BUFFER_TOO_SMALL`并返回所需要的`size`
    fn read(&mut self, buf: &mut [u8]) -> Result;

    /// 读取指定字节数
    /// 将读取到的数据填充至`buf`中
    fn read_exact(&mut self, mut buf: &mut [u8]) -> Result<()> {
        while !buf.is_empty() {
            match self.read(buf) {
                Ok(0) => break,
                Ok(n) => {
                    let tmp = buf;
                    buf = &mut tmp[n..];
                }
                Err(e) => return Err(e),
            }
        }
        if !buf.is_empty() {
            Err(Error::new(ErrorKind::UnexpectedEof, "failed to fill whole buffer"))
        } else {
            Ok(())
        }
    }

    #[inline]
    unsafe fn initializer(&self) -> Initializer {
        Initializer::zeroing()
    }
}

然后我们可以定义出read_to_end函数了

fn read_to_end(r: &mut R, buf: &mut Vec) -> Result {
    read_to_end_with_reservation(r, buf, |_| 32)
}

之后再Read trait中添加read_to_end函数

pub trait Read {
	....
    fn read_to_end(&mut self, buf: &mut Vec) -> Result {
        read_to_end(self, buf)
    }
    ...
}

扩充 Write trait

Write trait中我们暂时只提供一个write_all函数已经够用了,暂时在uefi环境先不会做过多的复杂操作

pub trait Write {
	fn write_all(&mut self, mut buf: &[u8]) -> Result<()> {
        while !buf.is_empty() {
            match self.write(buf) {
                0 => {
                    return Err(Error::new(ErrorKind::WriteZero, "failed to write whole buffer"));
                }
                n => buf = &buf[n..],
            }
        }
        Ok(())
    }
}

write_all函数的实现比较简单,循环入即可

如果大家读过std::io的代码的话就知道其实这是官方库的修改版本适用于我们当前的uefi环境

简化Qemu启动

随着我们要编译的内容越来越多,我们需要使用更加自动化的方式来完成,基本思路是依靠rust提供的std::use std::process::Command功能,以内核编译举例


fn main() -> std::io::Result<()> {
	// 获取当前项目路径
    let work_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
    // 1. build kernel
    check_status(kernel_build_step(work_dir)?);
}
// 执行 cargo xbuild --release命令
pub fn kernel_build_step(path: &Path) -> std::io::Result {
    Command::new("cargo")
        .current_dir(path.join("kernel"))
        .arg("xbuild")
        .arg("--release").status()
}
fn check_status(status: ExitStatus) {
    if !status.success() {
        println!("status is not succeed: {}", status);
        exit(1);
    }
}

如法炮制我们可以得到uefi编译的函数

// 执行 cargo xbuild --package uefis
pub fn efi_build_step(path: &Path) -> std::io::Result {
    Command::new("cargo")
        .current_dir(path.join("uefis"))
        .arg("xbuild")
        .args(&["--package", "uefis"])
        .arg("--release")
        .status()
}

然后我们需要建立模拟esp分区,我们将其放到targetr/debug文件夹中即可,基本的过程如下

pub fn copy_file(path: &Path) -> std::io::Result<()> {
    //构建内核路径 $WORK_DIR/kernel/target/x86-64/debug/kernel
    let src_kernel_path = path.join(r"kernel\target\x86-64\debug\kernel");
    //构建efi文件路径 $WORK_DIR/uefis/target/x86_64-unknown-uefi/debug/uefis.efi
    let uefi_path = path.join(r"uefis\target\x86_64-unknown-uefi\debug\uefis.efi");
    // 构建uefi启动目录 $WORK_DIR/target/debug/esp/EFI/Boot
    let dest_path = path.join(r"target\debug\esp\EFI\Boot");

    // 创建esp/EFI/Boot目录
    std::fs::create_dir_all(&dest_path)?;

    // 复制efi文件
    let efi_file_path = dest_path.join("BootX64.efi");
    File::create(&efi_file_path)?;
    std::fs::copy(uefi_path, efi_file_path)?;

    // 复制内核文件
    let dest_kernel_path = dest_path.join("kernel");
    File::create(&dest_kernel_path)?;
    std::fs::copy(src_kernel_path, dest_kernel_path)?;

    Ok(())
}

注意path的join函数会直接添加到路径中听不会根据不相同的系统来做对应的处理,因此在linux系统的路径分隔符需要反过来

最后我们将qemu启动的参数写成脚本的形式

pub fn run_qemu(path: &Path) {
	// 拼接OVMF_CODE.fd命令
    let p_code = format!("if=pflash,format=raw,file={},readonly=on", path.join("OVMF_CODE.fd").to_str().unwrap());
    // 拼接OVMF_VARS.fd命令
    let p_vars = format!("if=pflash,format=raw,file={},readonly=on", path.join("OVMF_VARS.fd").to_str().unwrap());
    // 拼接挂在esp分区的命令
    let p_esp = format!("format=raw,file=fat:rw:{}", path.join("target\\debug\\esp").to_str().unwrap());
    // 启动qemu
    let process = Command::new("qemu-system-x86_64.exe").stdout(Stdio::piped())
        .args(&[
            "-drive", p_code.as_str(),
            "-drive", p_vars.as_str(),
            "-drive", p_esp.as_str(),
            "-serial", "stdio",
            "-device", "isa-debug-exit,iobase=0xf4,iosize=0x04",
            "-debugcon", "file:debug.log",
            "-s",
//            "-S"
//            "-global", "isa-debugcon.iobase=0x402"
        ])
        .spawn().unwrap();
	// 循环读取stdout输出的数据(qemu将会把模拟环境的输出重定向到本机的stdout中)
    let mut line = String::new();
    if let Some(out) = process.stdout {
        let mut reader = BufReader::new(out);
        while let Ok(size) = reader.read_line(&mut line) {
            if size == 0 {
                break;
            }
            println!("{}", line);
        }
    }
}

最后我们的启动脚本就编写好了

fn main() -> std::io::Result<()> {
	// 获取当前项目路径
    let work_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
     // 1. build kernel
    check_status(kernel_build_step(work_dir)?);
    // 2. build efi file
    check_status(efi_build_step(work_dir)?);
    // 3. copy file
    copy_file(work_dir)?;
    run_qemu(work_dir);
}

最后我们的项目结构如下

OperatingSystem
├── kernel
├── uefi
├── target
└── src
     └──  main.rs

下一步要做什么?

在下一篇文章中我们要认识并解析elf文件,因为我们的内核编译后便是elf文件,有了本章中提供的便捷api,解析elf文件时会更加方便

你可能感兴趣的:(使用Rust开发操作系统)