【rCore】用rust从零开始写一个操作系统 开源操作系统训练营:ch1 应用程序与基本执行环境

需要实现什么?

裸机运行rust程序,即在没有std标准库与系统调用的情况下实现println!宏。

拥有什么?

core核心库, QEMU 软件 qemu-system-riscv64 模拟 RISC-V 64 计算机,RustSBI。
在这里插入图片描述

思路

qemu逻辑运行代码。

qemu-system-riscv64 \
            -machine virt \
            -nographic \
            -bios $(BOOTLOADER) \
            -device loader,file=$(KERNEL_BIN),addr=$(KERNEL_ENTRY_PA)

我们需要实现os应用程序KERNEL_BIN和应用入口地址KERNEL_ENTRY_PA两项。

编写os程序入口函数->将os程序的入口地址链接到addr->使用sbi调用实现print

实现过程

跳过为qemu-riscv64实现应用程序,以下是qemu-system-riscv64的实现过程。

QEMU有两种运行模式:
User mode 模式,即用户态模拟,如 qemu-riscv64 程序, 能够模拟不同处理器的用户态指令的执行,并可以直接解析ELF可执行文件, 加载运行那些为不同处理器编译的用户级Linux应用程序。
System mode 模式,即系统态模式,如 qemu-system-riscv64 程序, 能够模拟一个完整的基于不同CPU的硬件系统,包括处理器、内存及其他外部设备,支持运行完整的操作系统。

提供入口函数

简易的入口函数
// os/src/main.rs
#[no_mangle]
extern "C" fn _start() {
    loop{};
}

extern “C” fn _start() 定义一个程序的入口点。由于没有提供 main 函数的调用,此处定义一个 _start 函数,它会被链接器识别为程序的入口点。如果没有入口,程序会被编译为空程序。

extern “C” 修饰符告诉Rust编译器,这个函数的链接约定应该与C语言相同。因为C语言的链接约定在许多平台上都是已知的,并且可以直接被链接器和引导加载器理解。

最终入口函数
global_asm!(include_str!("entry.asm"));
/// the rust entry-point of os
#[no_mangle]
pub fn rust_main() -> ! {
    extern "C" {
        fn stext(); // begin addr of text segment
        fn etext(); // end addr of text segment
        fn srodata(); // start addr of Read-Only data segment
        fn erodata(); // end addr of Read-Only data ssegment
        fn sdata(); // start addr of data segment
        fn edata(); // end addr of data segment
        fn sbss(); // start addr of BSS segment
        fn ebss(); // end addr of BSS segment
        fn boot_stack_lower_bound(); // stack lower bound
        fn boot_stack_top(); // stack top
    }
    clear_bss();
    ...
    use crate::board::QEMUExit;
    crate::board::QEMU_EXIT_HANDLE.exit_success(); // CI autotest success
                                                   //crate::board::QEMU_EXIT_HANDLE.exit_failure(); // CI autoest failed
}
/// clear BSS segment
pub 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) });
}

global_asm!(include_str!(“entry.asm”));内联汇编代码。入口函数rust_main,#[no_mangle]让rust编译器在编译过程中不会修改函数的名称。extern “C” 修饰符告诉Rust编译器,这个函数的链接约定应该与C语言相同。clear_bss清零 .bss 段,链接脚本 linker.ld 中给出的全局符号 sbss 和 ebss 让我们能轻松确定 .bss 段的位置。

# 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:

asm定义程序入口_start,地址将依据链接脚本被放在 BASE_ADDRESS (0x80200000)处。,之后创建堆栈,调用rust_main函数

实现sbi调用

ecall调用从系统态进入sbi态。

#[inline(always)]
fn sbi_call(which: usize, arg0: usize, arg1: usize, arg2: usize) -> usize {
    let mut ret;
    unsafe {
    	// asm! 宏允许我们在 Rust 代码中嵌入汇编代码。
        asm!(
            "li x16, 0",
            "ecall",
            inlateout("x10") arg0 => ret,
            in("x11") arg1,
            in("x12") arg2,
            in("x17") which,
        );
    }
    ret
}

来看看这段的汇编代码

函数开头:
    000000000001116c <_ZN2os7syscall17h2a6fcb2e627ab196E>:
        这是函数 _ZN2os7syscall17h2a6fcb2e627ab196E 的地址标签。这个标签是编译器生成的,通常包含了一些关于函数的信息(如命名空间、函数名、哈希值等)。
函数体
    1116c: 41 11 addi sp, sp, -0x10
        这条指令将栈指针(sp)减少 16 字节(0x10),为局部变量和可能的临时数据腾出空间。
    1116e: 2e 86 mv a2, a1
        将参数寄存器 a1(在 RISC-V 中通常用于传递函数的第二个参数)的值复制到 a2 寄存器。这里 a2 稍后用于索引参数数组。
    11170: aa 88 mv a7, a0
        将参数寄存器 a0(在 RISC-V 中通常用于传递函数的第一个参数)的值复制到 a7 寄存器。这里 a7 用来保存系统调用编号 id。
    11172: 46 e4 sd a7, 0x8(sp)
        将 a7 寄存器(包含系统调用编号 id)的值存储到栈上,偏移量为 8 字节(0x8(sp))。这不是必需的,但在某些情况下,为了保持栈帧的一致性或便于调试,编译器可能会选择这样做。
    11174: 08 62 ld a0, 0x0(a2)
        从 a2 寄存器指向的地址(即参数数组的第一个元素)加载 8 字节数据到 a0 寄存器。这对应于 Rust 代码中的 args[0]。
    11176: 0c 66 ld a1, 0x8(a2)
        从 a2 寄存器指向的地址加上 8 字节(即参数数组的第二个元素)加载 8 字节数据到 a1 寄存器。这对应于 Rust 代码中的 args[1]。
    11178: 10 6a ld a2, 0x10(a2)
        从 a2 寄存器指向的地址加上 16 字节(即参数数组的第三个元素)加载 8 字节数据到 a2 寄存器。这对应于 Rust 代码中的 args[2]。
    1117a: 73 00 00 00 ecall
        执行 ecall 指令,触发系统调用。此时,a0、a1、a2 寄存器分别包含 args[0]、args[1]、args[2] 的值,而系统调用编号 id 应该在某个地方(可能是之前存储在栈上的 a7,但在这个例子中它没有被直接使用)。
        ecall 指令将权限提升到内核模式并将程序跳转到指定的地址。在这个例子中,这个地址是内核的系统调用处理程序。
        应用程序访问操作系统提供的系统调用的指令是 ecall ,操作系统访问 RustSBI提供的SBI调用的指令也是 ecall , 虽然指令一样,但它们所在的特权级是不一样的。 简单地说,应用程序位于最弱的用户特权级(User Mode), 操作系统位于内核特权级(Supervisor Mode), RustSBI位于机器特权级(Machine Mode)。
    1117e: 2a e0 sd a0, 0x0(sp)
        将 a0 寄存器的值(即系统调用的返回值)存储到栈上的某个位置。这通常是为了在后续指令中能够访问这个返回值。
    11180: 02 65 ld a0, 0x0(sp)
        从栈上的位置加载 8 字节数据到 a0 寄存器。这实际上是将之前存储的系统调用返回值重新加载回 a0 寄存器,因为 RISC-V 架构通常约定使用 a0 寄存器来返回函数的结果。
函数结尾
    11182: 41 01 addi sp, sp, 0x10
        将栈指针(sp)增加 16 字节,释放之前为局部变量和临时数据分配的空间。
    11184: 82 80 ret
        返回

实现退出机制

直接复制了,不重要

//ref:: https://github.com/andre-richter/qemu-exit
use core::arch::asm;

const EXIT_SUCCESS: u32 = 0x5555; // Equals `exit(0)`. qemu successful exit

const EXIT_FAILURE_FLAG: u32 = 0x3333;
const EXIT_FAILURE: u32 = exit_code_encode(1); // Equals `exit(1)`. qemu failed exit
const EXIT_RESET: u32 = 0x7777; // qemu reset

pub trait QEMUExit {
    /// Exit with specified return code.
    ///
    /// Note: For `X86`, code is binary-OR'ed with `0x1` inside QEMU.
    fn exit(&self, code: u32) -> !;

    /// Exit QEMU using `EXIT_SUCCESS`, aka `0`, if possible.
    ///
    /// Note: Not possible for `X86`.
    fn exit_success(&self) -> !;

    /// Exit QEMU using `EXIT_FAILURE`, aka `1`.
    fn exit_failure(&self) -> !;
}

/// RISCV64 configuration
pub struct RISCV64 {
    /// Address of the sifive_test mapped device.
    addr: u64,
}

/// Encode the exit code using EXIT_FAILURE_FLAG.
const fn exit_code_encode(code: u32) -> u32 {
    (code << 16) | EXIT_FAILURE_FLAG
}

impl RISCV64 {
    /// Create an instance.
    pub const fn new(addr: u64) -> Self {
        RISCV64 { addr }
    }
}

impl QEMUExit for RISCV64 {
    /// Exit qemu with specified exit code.
    fn exit(&self, code: u32) -> ! {
        // If code is not a special value, we need to encode it with EXIT_FAILURE_FLAG.
        let code_new = match code {
            EXIT_SUCCESS | EXIT_FAILURE | EXIT_RESET => code,
            _ => exit_code_encode(code),
        };

        unsafe {
            asm!(
                "sw {0}, 0({1})",
                in(reg)code_new, in(reg)self.addr
            );

            // For the case that the QEMU exit attempt did not work, transition into an infinite
            // loop. Calling `panic!()` here is unfeasible, since there is a good chance
            // this function here is the last expression in the `panic!()` handler
            // itself. This prevents a possible infinite loop.
            loop {
                asm!("wfi", options(nomem, nostack));
            }
        }
    }

    fn exit_success(&self) -> ! {
        self.exit(EXIT_SUCCESS);
    }

    fn exit_failure(&self) -> ! {
        self.exit(EXIT_FAILURE);
    }
}

const VIRT_TEST: u64 = 0x100000;

pub const QEMU_EXIT_HANDLE: RISCV64 = RISCV64::new(VIRT_TEST);

const SYSCALL_EXIT: usize = 93;

pub fn sys_exit(xstate: i32) -> isize {
    syscall(SYSCALL_EXIT, [xstate as usize, 0, 0])
}

#[no_mangle]
extern "C" fn _start() {
    sys_exit(9);
}

编译执行后可以打印程序的返回值(只是说明返回值的作用,这里暂时还不能运行,按照官方文档的顺序来写可以实现)

$ cargo build --target riscv64gc-unknown-none-elf
  Compiling os v0.1.0 (/media/chyyuu/ca8c7ba6-51b7-41fc-8430-e29e31e5328f/thecode/rust/os_kernel_lab/os)
    Finished dev [unoptimized + debuginfo] target(s) in 0.26s

[打印程序的返回值]
$ qemu-riscv64 target/riscv64gc-unknown-none-elf/debug/os; echo $?
9

提供语义项 panic_handler

简易的 panic_handler

panic_handler是一个特殊的函数,它用于处理panic(程序中的严重错误,如不可恢复的逻辑错误或资源不足),rust程序中必须实现此函数。返回类型 ! ,使用loop无限循环永不返回。

// os/src/lang_items.rs
use core::panic::PanicInfo;

#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop {}
}
加入shutdown关机
//! The panic handler

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

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

实现println宏

const SYSCALL_WRITE: usize = 64;

pub fn sys_write(fd: usize, buffer: &[u8]) -> isize {
  syscall(SYSCALL_WRITE, [fd, buffer.as_ptr() as usize, buffer.len()])
}

struct Stdout;

impl Write for Stdout {
    fn write_str(&mut self, s: &str) -> fmt::Result {
        sys_write(1, s.as_bytes());
        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)+)?) => {
        print(format_args!(concat!($fmt, "\n") $(, $($arg)+)?));
    }
}

修改程序入口地址并运行

qemu-system-riscv64 \
            -machine virt \
            -nographic \
            -bios $(BOOTLOADER) \
            -device loader,file=$(KERNEL_BIN),addr=$(KERNEL_ENTRY_PA)
            
BOOTLOADER := ../bootloader/$(SBI)-$(BOARD).bin
KERNEL_ENTRY_PA := 0x80200000
KERNEL_BIN := target/riscv64gc-unknown-none-elf/release/os.bin

当我们执行包含上述启动参数的 qemu-system-riscv64 软件,就意味给这台虚拟的 RISC-V64 计算机加电了。 此时,CPU 的其它通用寄存器清零,而 PC 会指向 0x1000 的位置,这里有固化在硬件中的一小段引导代码, 它会很快跳转到 0x80000000 的 RustSBI 处。 RustSBI完成硬件初始化后,会跳转到 $(KERNEL_BIN) 所在内存位置 0x80200000 处, 执行操作系统的第一条指令。

通过修改linker.ld文件将os程序的入口地址也设置为0x80200000。

// 修改 Cargo 的配置文件来使用我们自己的链接脚本 os/src/linker.ld
// os/.cargo/config
[build]
target = "riscv64gc-unknown-none-elf"

[target.riscv64gc-unknown-none-elf]
rustflags = [
    "-Clink-arg=-Tsrc/linker.ld", "-Cforce-frame-pointers=yes"
]

// os/src/link.ld
 OUTPUT_ARCH(riscv)
 ENTRY(_start)
 BASE_ADDRESS = 0x80200000;
 ...
  • 15
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值