【Rust】rCores操作系统第一章总结与个人理解

为什么我想要学习如何制作一个操作系统?

最开始的想法是我想拥有从软件到硬件可以一个人包揽的能力,这就意味着,我不仅要会处理器的硬件设计知识,同样需要操作系统及内核的设计知识,这样才能真正把活给做全了。

为什么选择了Risc-V + Rust

二者从推出时间来看,都是非常新的,都是新时代的现代化的产物,拥有着无限的发展潜力。

why Risc-V?

Risc-V至今推出了12年,是一款全开源的,模块化的处理器指令集架构,自由度很高,且目前越来越受欢迎,按照天梯制度来排位,目前一定是Risc-V,ARM,X86如此排序,而目前Risc-V正试图取代ARM的位置,发展极其迅猛,性能越来越高,我相信终有一天Risc-V会发展到X86的地位,成为新时代的架构之首。

why Rust?

Rust至今同样推出了12年,是不是有一种莫名的巧合在里面?他是一款无gc,内存安全,高自由度的拥有多种编程模式的强大语言,性能极高,堪比C/C++,甚至高于两者,如今已经成功进入了Linux内核,成为30年来除了C后唯一被Linus本人认可的第二款内核开发语言,就问你牛不牛!甚至谷歌在Android 13中正式引入了Rust语言,其经过验证,存在的内存安全漏洞是 0 ,是的,100%的无内存安全漏洞,是真的 0!如此优秀的语言用于操作系统的编写再合适不过了,能兼备安全与高性能,你有什么理由不使用呢?我也相信未来也一定会有Rust的一席之地。

第一章源码的目录结构

  • 工程根目录/
    • src/
      • console.rs(内核对控制台输出的实现)
      • entry.asm(内核最开始启动阶段的启动代码)
      • lang_item.rs(除去std库后的panic处理实现)
      • linker.ld(内存布局链接脚本)
      • main.rs(主函数)
      • sbi.rs(S模式二进制接口)
    • bootloader/
      • rustsbi-qemu.bin(RustSBI bootloader)
    • Cargo.toml(Rust工程清单)

源码解析

第一章实现的系统非常简陋,只有非常粗糙的几行代码,实现了一个简单的函数库,将应用与硬件隔离,可以在不动用硬件API以及跨特权级别的情况下完成任务。

先从main.rs看看系统在启动时做了什么

#![no_std]
#![no_main]
#![feature(panic_info_message)]
mod lang_items;
mod sbi;

#[macro_use]
mod console;

use core::{arch::global_asm};

global_asm!(include_str!("entry.asm")); 

#[no_mangle]
pub fn rust_main() {
    clear_bss();
    println!("Hello, world!");
    panic!("Shutdown machine");
}

fn clear_bss() {
    extern "C" {
        fn sbss();
        fn ebss();
    }
    (sbss as usize..ebss as usize).for_each(|a| {
        unsafe {
            (a as *mut u8).write_volatile(0)
        }
    })
}

第1,2行,我们使用 #![no_std] 以及 #![no_main] 告诉rust编译器移除main函数以及std标准库。

为什么要移除main函数?

其实感觉不移除main函数也是没关系的,只需要在 entry.asm 中汇编更改调用的函数名按道理也可以运行,假如使用main函数,会提示我

error: requires `start` lang_item

于是查了查,似乎是因为rust语言有许多特性是pluggable的,即模块化的,当我们禁用std库后,就需要我们自己实现这些 lang_item , start lang_item 用于表明哪一个函数是程序的入口,但如果加上该lang_item,

#![no_std]
#![feature(panic_info_message, start)]

...

#[no_mangle]
#[start]
pub fn main() {
    ...
}
...

则会要求我们以特定的方式声明函数

error[E0308]: `#[start]` function has wrong type
–> src/main.rs:16:1
|
16 | pub fn main() {
| ^^^^^^^^^^^^^ incorrect number of function parameters
|
= note: expected fn pointer `fn(isize, *const *const u8) -> isize`
found fn pointer `fn()`

不得不说整的还挺麻烦的,还是以文档所说的为主吧,以后再去深究原理。


为什么要移除std库

很简单的理由,std由于大量依赖于操作系统的库实现,故无法在裸机环境上跑起来,所以一定得移除他。

让我们将目光移动到第12行

global_asm!(include_str!("entry.asm")); 

我们使用宏在一开始嵌入入了我们编写的entry.asm,该文件代码如下:

    .section .text.entry
    .globl  _start
_start:
    la sp, boot_stack_top
    call rust_main

    .section .bss.stack
    .globl boot_stack_lower_bound
boot_stack_lower_bound:
    .space 4096 * 16
    .globl boot_stack_top
boot_stack_top:

该代码主要分配了一个栈,并且从汇编跳转到了 rust_main 函数,即进入了rust代码部分。接着我们将目光转向 rust_main 函数。


进入rust_main,首先我们看到他调用了一个 clear_bss() 函数,定义在 rust_main 函数的下面

fn clear_bss() {
    extern "C" {
        fn sbss();
        fn ebss();
    }
    (sbss as usize..ebss as usize).for_each(|a| {
        unsafe {
            (a as *mut u8).write_volatile(0)
        }
    })
}

在这里,我们使用了 extern "C" 来声明了两个C语言符号,其地址指向内存中的sbss与ebss段,于是通过接下来的unsafe操作将两个内存段进行清零操作。


回到 rust_main 函数,接着就是调用了我们移除std库后由用户自己实现的println!与panic!宏,那么就该跳转到下一个文件了。

看看console.rs里做了什么

首先上代码

use crate::sbi::console_putchar;
use core::fmt::{self, Write};

struct Stdout;

impl Write for Stdout {
    fn write_str(&mut self, s: &str) -> fmt::Result {
        for c in s.chars() {
            console_putchar(c as usize);
        }

        Ok(())
    }
}

pub fn print(args: fmt::Arguments) {
    Stdout.write_fmt(args).unwrap();
}

#[macro_export]
macro_rules! print {
    ($fmt: literal $(, $($arg: tt)+)?) => {
        $crate::console::print(format_args!($fmt $(, $($arg)+)?));
    };
}

#[macro_export]
macro_rules! println {
    ($fmt: literal $(, $($arg: tt)+)?) => {
        $crate::console::print(format_args!(concat!($fmt, "\n") $(, $($arg)+)?));
    };
}

首先我们定义了一个Stdout的结构体并将 core::fmt::Write trait添加到了该结构体,用于实现标准输出流,使用 core::fmt crate是为了格式化我们所输入的字符串,让我们能够像在普通编程时一样输入,接着我们定义了print函数,并且将其用宏的方式包装起来,就不再过多的讲解。但我们主要得关注结构体里重写的 write_str 里的 console_putchar 方法,于是来到了sbi.rs

与硬件沟通打交道的sbi.rs

#![allow(unused)]

use core::{arch::asm};

const SBI_SET_TIMER: usize = 0;
const SBI_CONSOLE_PUTCHAR: usize = 1;
const SBI_CONSOLE_GETCHAR: usize = 2;
const SBI_CLEAR_IPI: usize = 3;
const SBI_SEND_IPI: usize = 4;
const SBI_REMOTE_FENCE_I: usize = 5;
const SBI_REMOTE_SFENCE_VMD: usize = 6;
const SBI_REMOTE_SFENCE_VMD_ASID: usize = 7;
const SBI_SHUTDOWN: usize = 8;

#[inline(always)]
fn sbi_call(which: usize, arg0: usize, arg1: usize, arg2: usize) -> usize {
    let mut ret;
    unsafe {
        asm!(
            "ecall",
            inlateout("x10") arg0 => ret,
            in("x11") arg1,
            in("x12") arg2,
            in("x17") which,
        );
    }
    ret
}

pub fn console_putchar(c: usize) {
    sbi_call(SBI_CONSOLE_PUTCHAR, c, 0, 0);
}

pub fn shutdown() -> ! {
    sbi_call(SBI_SHUTDOWN, 0, 0, 0);
    panic!("Shutdown...");
}

在sbi.rs中,最重要的就是 sbi_call 函数了!他接受4个参数,这四个参数分别对应了Risc-V中的寄存器调用规范,通过ecall和rustsbi bootloader交互,执行我们所需要的操作,详情请阅读rustsbi和risc-v的权威文档。


总结

通过rCore的第一章,我们很直观的了解到了rust与无论是Risc-V还是别的处理器架构,他们之间是如何进行协同工作,又是怎么提供基础的应用服务的,我大概不会再写第二章的源码解析,因为代码量巨大,等写出来估计都能成书了,同样的,第三章,第四章也是。可能会针对某个特定的问题进行讨论。这一篇就到这里

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值