为什么我想要学习如何制作一个操作系统?
最开始的想法是我想拥有从软件到硬件可以一个人包揽的能力,这就意味着,我不仅要会处理器的硬件设计知识,同样需要操作系统及内核的设计知识,这样才能真正把活给做全了。
为什么选择了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工程清单)
- src/
源码解析
第一章实现的系统非常简陋,只有非常粗糙的几行代码,实现了一个简单的函数库,将应用与硬件隔离,可以在不动用硬件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还是别的处理器架构,他们之间是如何进行协同工作,又是怎么提供基础的应用服务的,我大概不会再写第二章的源码解析,因为代码量巨大,等写出来估计都能成书了,同样的,第三章,第四章也是。可能会针对某个特定的问题进行讨论。这一篇就到这里