在上一篇文章中我们介绍了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;
第二个Result
的Error
为我们自定义的Error
,Error
分为2种UEFI执行中的错误以及应用程序自定义的错误,因此我们可以使用一个枚举来完成
// 用于表示异常的种类,分为UEFI执行错误和UEFI应用程序错误
#[derive(Debug)]
enum Repr {
/// UEFI服务错误
Uefi(Efi),
/// UEFI应用错误
Custom(Custom),
}
在Repr
中包含2个结构体Efi
和Custom
结构定义如下
#[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,
}
Custom
和Efi
主要的不同在于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实现Display
trait
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)
的(有一种思路就是将UefiResult
和Result
和并,这样可以使照常使用,未经过测试可行性)
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
函数只是一个辅助操作,因为我们要提供new
和try_new
2个函数定义如下
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,实现较为简单,这里不做过多阐述
/// 将此文件句柄的游标的位置设置为指定的绝对位置。
/// 允许使用`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))
}
}
}
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))
}
}
}
}
}
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_exact主要读取指定的字节数并填充到给定的切片中,以下是read_exact流程图
/// 读取指定字节数
/// 将读取到的数据填充至`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
的流程图
read_to_end
中的难点便是扩容操作,扩容操作总共执行2个步骤
GlobalAllocator
申请新的内存空间其次我们需要记录每次读取的字节数,以便准确表达缓冲区大小,在扩容时我们只会关注与缓冲区的容量而非大小,但是在用户使用时只关注缓冲区的大小(即从文件中读取的字节数)
因此我们需要一个新的数据结构来完成这个操作,它将完成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值表示是否需要初始化操作,然后我们需要提供两个初始化函数,zero
和nop
,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
是用来指定扩容的大小
相应的我们的Read
trait中需要增加初始化的函数(包含之前编写的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_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环境
随着我们要编译的内容越来越多,我们需要使用更加自动化的方式来完成,基本思路是依靠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文件时会更加方便