入口

​ 我们要在裸机上进行编程,所以在main.rs中要加入#![no_std],表示我们不适用标准库,其中!表示编译器内置相关的特性,需要加载文件的一开始。在编写main之前,先介绍这里的链接文件,我们不适用默认的链接模式,因为这时我们编程是要考虑操作系统不存在的情况。

OUTPUT_ARCH(riscv)
ENTRY_ADDRESS(_start)
BASE_ADDRESS = 0x80200000;

​ 这里设置一个变量BASE_ADDRESS, 将我们的kernel加载的位置设置为0x80200000。下面是我们空间分布:

​ 这里每一个segment都是4k对齐的,通过导出设置在linker中的变量,我们可以知道各个segment的大小和偏移。由于我们在裸机上启动程序,并没有运行时为我们设置堆栈,随意我们需要自己设置。

	.section .text.entry
    .global _start
_start:
	la sp, boot_stack_top
	call rust_main
	
	.section .bss.stack
	.global boot_stack_lower_bound
boot_stack_lower_bound:
	.space 4096*16
	.global boot_stack_top
boot_stack_top:

​ 设置完sp后,就可以跳转到main中。在main.rs中,我们引入这段代码:

#![no_std]
#![no_main]
//...

use core::arch::global_asm;
global_asm!(include_str!("entry.asm"));

#[no_mangle]
pub fn rust_main() -> !{
    extern "C" {
        fn stext();
        fn etext();
        fn srodata();
        fn erodata();
        fn sdata();
        fn edata();
        fn boot_stack_lower_bound();
        fn boot_stack_top()
    }
    //...
}

​ 通过core::arch::global_asm引入这段汇编代码。在rust_main之前有个#[no_mangle]是为了防止编译器对函数名进行一个再命令,之后通过调用的形式,将我们需要的地址引入。

输出

​ 这里我们通过opensbi进行输出,sbi是通过对ecall进行封装,这样我们就可以通过函数请求opensbi进行字符输出。

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

​ 于是通过sbi_call进一步我们封装具体的功能。

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

​ 我们需要对print宏进行编写,这样我们才能通过print进行输出。

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();
}

​ 这里的Write作为一个Trait只有一个方法write_str,编译器会帮我们解析格式化字符,我们只要进行输出就可以了。

​ 在rust中,我们调用的println实际是一个宏,再去除了标准库后,我们需要自己编写println

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

​ 这里我们匹配的意思是,前面一个字面量作为格式化字符,后面是一串的token,进行隔开,之后将需要格式化输出的内容放入print中。

panic

​ 我们还需要编写panic相关的信息。

use core::panic::PanicInfo;
use crate::sbi::shutdown;

#[panic_handler]
fn panic(info: &PanicInfo) -> !{
    if let Some(location: &Location) = info.location(){
        println!(
        	"Panicked at {}:{} {}",
            location.file(),
            location.line(),
            info.message().unwrap()
        )
    } else {
        println!("Panicked: {}", info.message().unwrap());
    }
    shutdown()
}

​ 发生panic时我们这里进行输出,通过#[panic_handler]说明这时panic发生时进行处理的办法,之后通过sbi进行关机

revisit main

#![feature(panic_info_message)]
//...

#[macro_use]
mod console;
mod land_item;
mod sbi;

#[no_managle]
pub fn rust_main() -> !{
    //...
    
    clear_bss();
    println!("Hello OS");
    println!(".text [{:#x}, {:#x})", stext as usize, etext as usize);
    //...
    
    shutdown()
}

fn clear_bss(){
    extern "C" {
        fn sbss();
        fn ebss();
    }
    
    (sbss as usize ..ebss as usize).foreach|x:size| {
        unsafe{ (x as *mut u8).write_volatile(0)}
    };
}

#![feature(panic_info_message)]说明如果我们发生panic可以输出信息。之后添加#[macro_use]导入宏,之后清空bss段,之后输出我们的段信息,最后关机。