构建用户态执行环境

注解

前三小节的用户态程序案例代码在 此处 获取。

用户态最小化执行环境

执行环境初始化

首先我们要给 Rust 编译器编译器提供入口函数 _start() , 在 main.rs 中添加如下内容:

// os/src/main.rs
#[no_mangle]
extern "C" fn _start() {
    loop{};
}

对上述代码重新编译,再用分析工具分析:

$ cargo build
   Compiling os v0.1.0 (/home/shinbokuow/workspace/v3/rCore-Tutorial-v3/os)
    Finished dev [unoptimized + debuginfo] target(s) in 0.06s

[反汇编导出汇编程序]
$ rust-objdump -S target/riscv64gc-unknown-none-elf/debug/os
   target/riscv64gc-unknown-none-elf/debug/os:       file format elf64-littleriscv

   Disassembly of section .text:

   0000000000011120 <_start>:
   ;     loop {}
     11120: 09 a0            j       2 <_start+0x2>
     11122: 01 a0            j       0 <_start+0x2>

反汇编出的两条指令就是一个死循环, 这说明编译器生成的已经是一个合理的程序了。 用 qemu-riscv64 target/riscv64gc-unknown-none-elf/debug/os 命令可以执行这个程序。

程序正常退出

我们把 _start() 函数中的循环语句注释掉,重新编译并分析,看到其汇编代码是:

$ rust-objdump -S target/riscv64gc-unknown-none-elf/debug/os

 target/riscv64gc-unknown-none-elf/debug/os: file format elf64-littleriscv


 Disassembly of section .text:

 0000000000011120 <_start>:
 ; }
   11120: 82 80              ret

看起来是合法的执行程序。但如果我们执行它,会引发问题:

$ qemu-riscv64 target/riscv64gc-unknown-none-elf/debug/os
  段错误 (核心已转储)

这个简单的程序导致 qemu-riscv64 崩溃了!为什么会这样?

注解

QEMU有两种运行模式:

User mode 模式,即用户态模拟,如 qemu-riscv64 程序, 能够模拟不同处理器的用户态指令的执行,并可以直接解析ELF可执行文件, 加载运行那些为不同处理器编译的用户级Linux应用程序。

System mode 模式,即系统态模式,如 qemu-system-riscv64 程序, 能够模拟一个完整的基于不同CPU的硬件系统,包括处理器、内存及其他外部设备,支持运行完整的操作系统。

目前的执行环境还缺了一个退出机制,我们需要操作系统提供的 exit 系统调用来退出程序。这里先给出代码:

// os/src/main.rs

const SYSCALL_EXIT: usize = 93;

fn syscall(id: usize, args: [usize; 3]) -> isize {
    let mut ret;
    unsafe {
        core::arch::asm!(
            "ecall",
            inlateout("x10") args[0] => ret,
            in("x11") args[1],
            in("x12") args[2],
            in("x17") id,
        );
    }
    ret
}

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

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

main.rs 增加的内容不多,但还是有点与一般的应用程序有所不同,因为它引入了汇编和系统调用。 第二章的第二节 实现应用程序 会详细介绍上述代码的含义。 这里读者只需要知道 _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

可以看到,返回的结果确实是 9 。这样,我们勉强完成了一个简陋的用户态最小化执行环境。

有显示支持的用户态执行环境

没有 println 输出信息,终究觉得缺了点啥。

Rust 的 core 库内建了以一系列帮助实现显示字符的基本 Trait 和数据结构,函数等,我们可以对其中的关键部分进行扩展,就可以实现定制的 println! 功能。

实现输出字符串的相关函数

注意

如果你觉得理解 Rust 宏有困难,把它当成黑盒就好!

首先封装一下对 SYSCALL_WRITE 系统调用。

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

然后实现基于 Write Trait 的数据结构,并完成 Write Trait 所需要的 write_str 函数,并用 print 函数进行包装。

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

最后,实现基于 print 函数,实现Rust语言 格式化宏 ( formatting macros )。

#[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)+)?));
    }
}

接下来,我们调整一下应用程序,让它发出显示字符串和退出的请求:

#[no_mangle]
extern "C" fn _start() {
    println!("Hello, world!");
    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.61s

$ qemu-riscv64 target/riscv64gc-unknown-none-elf/debug/os; echo $?
  Hello, world!
  9