清华大学操作系统rCore实验-第一章-应用程序与基本执行环境

清华大学操作系统实验—rCore—应用程序与基本执行环境

    • 零、前言
    • 一、创建新项目neos
    • 二、配置执行环境
      • 1、切换riscv目标平台
      • 2、移除标准库std依赖
        • (1)切换Rust核心库-core
        • (2)注释println!宏,暂时绕过
        • (3)实现简陋的异常处理函数
        • (4)移除main函数
        • (5)分析被移除标准库的程序
    • 三、内核第一条指令
      • 1、编写内核第一条指令
      • 2、调整内核的内存布局
      • 3、手动加载内核可执行文件
      • 4、使用gdb验证启动流程
    • 四、分配并使用启动栈
    • 五、基于SBI服务完成输出和关机
    • 六、总结


零、前言

环境配置方面已经在上一节说过了,见清华大学操作系统rCore实验-第零章-Lab环境搭建。本节开始,我们新创建一个项目,并一步一个脚印写出rcore操作系统。


一、创建新项目neos

我们使用cargo创建项目neos,输入cargo new neos --bin,可以通过tree neos看看这个项目的结构:
清华大学操作系统rCore实验-第一章-应用程序与基本执行环境_第1张图片
可以进入该文件目录,输入cargo run直接运行,也可以cat /neos/src/main.rs查看初始的源码:
清华大学操作系统rCore实验-第一章-应用程序与基本执行环境_第2张图片
因为使用qemu时需要一个引导加载程序(bootloader),这里我们使用预编译好的rustsbi-qemu.bin,这个文件需要另外下载安装。
输入git clone https://gitee.com/rcore-os/rCore-Tutorial-v3.git
然后用mv命令,将其中的bootloader移入我们的neos项目中。
清华大学操作系统rCore实验-第一章-应用程序与基本执行环境_第3张图片


二、配置执行环境

1、切换riscv目标平台

我们输入rustc --version --verbose,查看该项目默认运行的目标平台:
清华大学操作系统rCore实验-第一章-应用程序与基本执行环境_第4张图片
可以看到新项目的执行默认基于Linux,CPU架构是x86_64,CPU厂商是unknown(不清楚),运行时库是GNU libc(封装 Linux 系统调用,提供 POSIX 接口为主的函数库)。

rCore基于RISC-V64内核,我们需要将rCore的CPU架构从x86_64转换成RISC-V。
我们可以输入rustc --print target-list | grep riscv,查看Rust 编译器支持哪些基于 RISC-V 的目标平台:
清华大学操作系统rCore实验-第一章-应用程序与基本执行环境_第5张图片
我们选择riscv64gc-unknown-none-elf作为新项目的目标平台,其中riscv64gc是CPU架构,unknown是CPU厂商,none为空内核,elf为不带有运行时库并可以生成ELF格式的文件。这说明我们完全只基于riscv64gc编写操作系统,其余一切都是精简的空壳子,是一个裸机平台。

我们输入cargo run --target riscv64gc-unknown-none-elf,将该项目以riscv64gc-unknown-none-elf为目标平台运行:
清华大学操作系统rCore实验-第一章-应用程序与基本执行环境_第6张图片
可以看到出现了几个error,Rust没有针对该裸机平台的标准库-std,但是Rust有一个核心库-core,它是标准库-std的阉割版,虽然功能不丰富,但是不需要任何操作系统支持,并且也具备一部分的核心机制。

为了方便后续工作,我们需要使rustc编译器缺省生成RISC-V代码。
先输入rustup target add riscv64gc-unknown-none-elf
在这里插入图片描述
然后在/neos目录下新建/.cargo,在这个目录下创建config文件,并在里面输入配置内容:
清华大学操作系统rCore实验-第一章-应用程序与基本执行环境_第7张图片
现在cargo默认会使用riscv64gc-unknown-none-elf作为目标平台而不是原先的默认x86_64-unknown-linux-gnu,我们run或者build的时候就不需要添加--target riscv64gc-unknown-none-elf了。

2、移除标准库std依赖

(1)切换Rust核心库-core

我们重新cargo build
清华大学操作系统rCore实验-第一章-应用程序与基本执行环境_第8张图片
现在,我们针对这几个error挨个解决。

println! 宏是由标准库-std提供的,且会使用到一个名为write的系统调用,而标准库-std本身就需要操作系统的支持。
现在项目转换到了一个什么都没有的裸机平台,我们就需要告诉 Rust 编译器不使用Rust
标准库-std转而使用上面提到的核心库-core(core库不需要操作系统的支持)。
main.rs的开头加上一行#![no_std]即可:
清华大学操作系统rCore实验-第一章-应用程序与基本执行环境_第9张图片
这个时候可以看到,第一个error解决了。
清华大学操作系统rCore实验-第一章-应用程序与基本执行环境_第10张图片

(2)注释println!宏,暂时绕过

至于接下来这个error,现在我们的代码功能还不足以自己实现println! 宏。由于程序使用了系统调用,但不能在核心库 core 中找到它,所以我们目前先通过将 println! 宏注释掉的简单粗暴方式,来暂时绕过这个问题。
清华大学操作系统rCore实验-第一章-应用程序与基本执行环境_第11张图片

(3)实现简陋的异常处理函数

我们继续cargo build,就剩这一个error了:
清华大学操作系统rCore实验-第一章-应用程序与基本执行环境_第12张图片
panic!宏是一个多种编程语言都会有的异常处理函数,大致功能是打印出错位置和原因并kill掉当前应用。
#[panic_handler]是一种编译指导属性,用于标记核心库-core中的panic!宏要对接的函数(该函数实现对致命错误的具体处理)。该编译指导属性所标记的函数需要具有fn(&PanicInfo) -> ! 函数签名,函数可通过PanicInfo数据结构获取致命错误的相关信息。这样Rust编译器就可以把核心库-core中的panic!宏定义与#[panic_handler]指向的panic函数实现合并在一起,使得no_std程序具有类似std库的应对致命错误的功能。

核心库core中只有一个panic!宏的空壳,没有提供panic!宏的精简实现,故我们需要自己先实现一个简陋的panic处理函数,这样才能让我们的neos编译通过。

我们创建一个新的子模块文件lang_items.rs实现panic函数,并通过#[panic_handler]属性通知编译器用panic函数来对接panic!宏。为了将该模块添加到项目中,我们还需要在main.rs 的#![no_std]的下方加上mod lang_items
清华大学操作系统rCore实验-第一章-应用程序与基本执行环境_第13张图片
清华大学操作系统rCore实验-第一章-应用程序与基本执行环境_第14张图片
之后我们会从PanicInfo解析出错位置并打印出来,然后kill应用程序,但目前只会在原地 loop。

(4)移除main函数

重新编译,新出来了一个错误:
清华大学操作系统rCore实验-第一章-应用程序与基本执行环境_第15张图片
提醒我们缺少一个名为start语义项start语义项代表了标准库-std在执行应用程序之前需要进行的一些初始化工作,由于我们禁用了标准库,编译器也就找不到这项功能的实现。

解决方式很简单粗暴,我们在main.rs的开头加入设置#![no_main]告诉编译器我们没有一般意义上的main函数,并将原来的main函数删除。在失去了main函数的情况下,编译器也就不需要完成所谓的初始化工作了:
清华大学操作系统rCore实验-第一章-应用程序与基本执行环境_第16张图片
这个时候再度编译项目,
在这里插入图片描述
至此,我们成功伤筋动骨式地移除了标准库的依赖,并完成了构建裸机平台上新项目neos的第一步工作–通过编译器检查并生成执行码,虽然是一个空程序。

(5)分析被移除标准库的程序

file target/riscv64gc-unknown-none-elf/debug/neos //查看文件格式
rust-readobj -h target/riscv64gc-unknown-none-elf/debug/neos //查看文件头信息
rust-objdump -S target/riscv64gc-unknown-none-elf/debug/neos //反汇编导出汇编程序

上面三条命令帮助我们分析程序,不过经过前面的操作,我们也能知道,这就是一个什么功能都没有的空程序。


三、内核第一条指令

1、编写内核第一条指令

首先,我们需要编写进入内核后的第一条指令,这样更方便我们验证我们的内核镜像是否正确对接到 Qemu 上,为此,我们先新创建一个汇编文件entry.asm,并写入如下内容:
清华大学操作系统rCore实验-第一章-应用程序与基本执行环境_第17张图片
.section .text.entry表明我们希望将其后面的代码全部放到一个名为.text.entry的代码段中。
.global _start说明是_start一个全局符号,可以被其他目标文件使用;
_start符号指向紧跟在其后面的内容,其地址为指令li x1, 100所在的地址;
li x1, 100表示给寄存器x1赋值100

一般情况下,所有的代码都被放到一个名为.text的代码段中,这里我们命名为.text.entry的目的在于确保该段被放置在相比任何其他代码段更低的地址上。作为内核的入口点,这段指令可以被最先执行。

然后将这段代码导入main.rs文件中:
清华大学操作系统rCore实验-第一章-应用程序与基本执行环境_第18张图片

2、调整内核的内存布局

由于链接器默认的内存布局并不能符合我们的要求,为了实现与Qemu正确对接,我们可以通过编写自己的链接脚本(Linker Script) 调整链接器的行为,使得最终生成的可执行文件的内存布局符合Qemu的预期。

编写如下链接脚本linker.ld

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

SECTIONS
{
    . = BASE_ADDRESS;
    skernel = .;

    stext = .;
    .text : {
        *(.text.entry)
        *(.text .text.*)
    }

    . = ALIGN(4K);
    etext = .;
    srodata = .;
    .rodata : {
        *(.rodata .rodata.*)
        *(.srodata .srodata.*)
    }

    . = ALIGN(4K);
    erodata = .;
    sdata = .;
    .data : {
        *(.data .data.*)
        *(.sdata .sdata.*)
    }

    . = ALIGN(4K);
    edata = .;
    .bss : {
        *(.bss.stack)
        sbss = .;
        *(.bss .bss.*)
        *(.sbss .sbss.*)
    }

    . = ALIGN(4K);
    ebss = .;
    ekernel = .;

    /DISCARD/ : {
        *(.eh_frame)
    }
}

然后修改之前的配置文件config来使用我们自己的链接脚本neos/src/linker.ld而非使用默认的内存布局:
清华大学操作系统rCore实验-第一章-应用程序与基本执行环境_第19张图片

3、手动加载内核可执行文件

此后我们便可以生成内核可执行文件,切换到neos目录下并进行以下操作:
清华大学操作系统rCore实验-第一章-应用程序与基本执行环境_第20张图片
可以查看刚刚生成文件的格式:
在这里插入图片描述
然后丢弃内核可执行文件中的元数据得到内核镜像:
在这里插入图片描述
可以使用stat命令比较内核可执行文件和内核镜像的大小:
清华大学操作系统rCore实验-第一章-应用程序与基本执行环境_第21张图片


4、使用gdb验证启动流程

在neos目录下通过以下命令启动Qemu并加载RustSBI和内核镜像:

qemu-system-riscv64 \
    -machine virt \
    -nographic \
    -bios bootloader/rustsbi-qemu.bin \  
    -device loader,file=target/riscv64gc-unknown-none-elf/release/neos.bin,addr=0x80200000 \
    -s -S

打开另一个终端,启动一个 GDB 客户端连接到 Qemu :

riscv64-unknown-elf-gdb \
    -ex 'file /home/kali/neos/target/riscv64gc-unknown-none-elf/release/neos' \
    -ex 'set arch riscv:rv64' \
    -ex 'target remote localhost:1234'

四、分配并使用启动栈

我们在 entry.asm 中分配启动栈空间,并在控制权被转交给Rust入口之前将栈指针sp设置为栈顶的位置。
清华大学操作系统rCore实验-第一章-应用程序与基本执行环境_第22张图片
call rust_main表明我们通过伪指令call调用Rust编写的内核入口点rust_main将控制权转交给Rust代码,该入口点在 main.rs 中实现:
清华大学操作系统rCore实验-第一章-应用程序与基本执行环境_第23张图片
这里需要注意的是需要通过宏将rust_main标记为#![no_mangle]以避免编译器对它的名字进行混淆,不然在链接的时候,entry.asm将找不到main.rs提供的外部符号rust_main从而导致链接失败。

在内核初始化中,需要先完成对 .bss 段的清零:
清华大学操作系统rCore实验-第一章-应用程序与基本执行环境_第24张图片

五、基于SBI服务完成输出和关机

这里我们可以进行基于RustSBI提供的服务完成在屏幕上打印Hello world!和关机操作了。
首先,我们在Cargo.toml中引入sbi_rt依赖:
清华大学操作系统rCore实验-第一章-应用程序与基本执行环境_第25张图片
创建sbi.rs文件,调用sbi_rt提供的接口实现输出字符的功能:
清华大学操作系统rCore实验-第一章-应用程序与基本执行环境_第26张图片
main.rs中加入mod sbi将该子模块加入项目;
清华大学操作系统rCore实验-第一章-应用程序与基本执行环境_第27张图片
同样,我们再来实现关机功能
清华大学操作系统rCore实验-第一章-应用程序与基本执行环境_第28张图片
由于输出字符功能中的console_putchar的功能受限,如果想打印一行 Hello world! 的话需要进行多次调用,因此我们尝试自己编写基于console_putcharprintln!宏:
首先在main.rs中引入一个新文件console.rs
清华大学操作系统rCore实验-第一章-应用程序与基本执行环境_第29张图片
然后编写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)+)?));
    }
}

接下来,我们需要对错误处理函数panic进行完善


六、总结

你可能感兴趣的:(rust,汇编,risc-v,操作系统,qemu,rCore)