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

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


零、前言

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


一、创建新项目neos

我们使用cargo创建项目neos ,输入cargo new neos --bin,可以通过tree neos看看这个项目的结构:

可以进入该文件目录,输入cargo run直接运行,也可以cat /neos/src/main.rs查看初始的源码:

因为使用qemu时需要一个引导加载程序(bootloader),这里我们使用预编译好的rustsbi-qemu.bin,这个文件需要另外下载安装。

输入git clone https://gitee.com/rcore-os/rCore-Tutorial-v3.git

然后用mv命令,将其中的bootloader移入我们的neos项目中。


二、配置执行环境

1、切换riscv目标平台

我们输入rustc --version --verbose,查看该项目默认运行的目标平台:

可以看到新项目的执行默认基于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 的目标平台:

我们选择riscv64gc-unknown-none-elf作为新项目的目标平台,其中riscv64gc 是CPU架构,unknown 是CPU厂商,none 为空内核,elf 为不带有运行时库并可以生成ELF格式的文件。这说明我们完全只基于riscv64gc编写操作系统,其余一切都是精简的空壳子,是一个裸机平台。

我们输入cargo run --target riscv64gc-unknown-none-elf,将该项目以riscv64gc-unknown-none-elf为目标平台运行:

可以看到出现了几个error ,Rust没有针对该裸机平台的标准库-std ,但是Rust有一个核心库-core ,它是标准库-std的阉割版,虽然功能不丰富,但是不需要任何操作系统支持,并且也具备一部分的核心机制。

为了方便后续工作,我们需要使rustc编译器缺省生成RISC-V代码。

先输入rustup target add riscv64gc-unknown-none-elf

然后在/neos目录下新建/.cargo,在这个目录下创建config文件,并在里面输入配置内容:

现在cargo默认会使用riscv64gc-unknown-none-elf作为目标平台而不是原先的默认x86_64-unknown-linux-gnu,我们run 或者build 的时候就不需要添加--target riscv64gc-unknown-none-elf了。

2、移除标准库std依赖

(1)切换Rust核心库-core

我们重新cargo build

现在,我们针对这几个error挨个解决。

println! 宏是由标准库-std 提供的,且会使用到一个名为write 的系统调用,而标准库-std 本身就需要操作系统的支持。

现在项目转换到了一个什么都没有的裸机平台,我们就需要告诉 Rust 编译器不使用Rust
标准库-std 转而使用上面提到的核心库-core (core库不需要操作系统的支持)。

main.rs的开头加上一行#![no_std]即可:

这个时候可以看到,第一个error解决了。

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

至于接下来这个error,现在我们的代码功能还不足以自己实现println! 宏。由于程序使用了系统调用,但不能在核心库 core 中找到它,所以我们目前先通过将 println! 宏注释掉的简单粗暴方式,来暂时绕过 这个问题。

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

我们继续cargo build,就剩这一个error了:

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

之后我们会从PanicInfo解析出错位置并打印出来,然后kill应用程序,但目前只会在原地 loop。

(4)移除main函数

重新编译,新出来了一个错误:

提醒我们缺少一个名为start语义项start语义项 代表了标准库-std 在执行应用程序之前需要进行的一些初始化工作,由于我们禁用了标准库,编译器也就找不到这项功能的实现。

解决方式很简单粗暴,我们在main.rs的开头加入设置#![no_main]告诉编译器我们没有一般意义上的main函数,并将原来的main函数删除 。在失去了main函数的情况下,编译器也就不需要完成所谓的初始化工作了:

这个时候再度编译项目,

至此,我们成功伤筋动骨 式地移除了标准库的依赖,并完成了构建裸机平台上新项目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,并写入如下内容:

.section .text.entry表明我们希望将其后面的代码全部放到一个名为.text.entry的代码段中。
.global _start说明是_start一个全局符号,可以被其他目标文件使用;
_start符号指向紧跟在其后面的内容,其地址为指令li x1, 100所在的地址;
li x1, 100表示给寄存器x1赋值100

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

然后将这段代码导入main.rs文件中:

2、调整内核的内存布局

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

编写如下链接脚本linker.ld

rust 复制代码
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而非使用默认的内存布局:

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

此后我们便可以生成内核可执行文件 ,切换到neos目录下并进行以下操作:

可以查看刚刚生成文件的格式:

然后丢弃内核可执行文件中的元数据得到内核镜像:

可以使用stat命令比较内核可执行文件和内核镜像的大小:


4、使用gdb验证启动流程

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

rust 复制代码
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 :

rust 复制代码
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设置为栈顶的位置。

call rust_main表明我们通过伪指令call调用Rust编写的内核入口点rust_main将控制权转交给Rust代码,该入口点在 main.rs 中实现:

这里需要注意的是需要通过宏将rust_main标记为#![no_mangle]以避免编译器对它的名字进行混淆,不然在链接的时候,entry.asm将找不到main.rs提供的外部符号rust_main从而导致链接失败。

在内核初始化中,需要先完成对 .bss 段的清零:

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

这里我们可以进行基于RustSBI提供的服务完成在屏幕上打印Hello world!和关机操作了。

首先,我们在Cargo.toml中引入sbi_rt依赖:

创建sbi.rs文件,调用sbi_rt提供的接口实现输出字符 的功能:

main.rs中加入mod sbi将该子模块加入项目;

同样,我们再来实现关机功能

由于输出字符功能中的console_putchar的功能受限,如果想打印一行 Hello world! 的话需要进行多次调用,因此我们尝试自己编写基于console_putcharprintln!宏:

首先在main.rs中引入一个新文件console.rs

然后编写console.rs的代码:

rust 复制代码
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进行完善


六、总结

相关推荐
VertexGeek2 小时前
Rust学习(八):异常处理和宏编程:
学习·算法·rust
不爱学习的YY酱18 小时前
【操作系统不挂科】<CPU调度(13)>选择题(带答案与解析)
java·linux·前端·算法·操作系统
前端与小赵1 天前
什么是Sass,有什么特点
前端·rust·sass
钰爱&1 天前
【操作系统】Linux之网络编程(UDP)(头歌作业)
linux·操作系统
清酒伴风(面试准备中......)1 天前
操作系统基础——针对实习面试
笔记·面试·职场和发展·操作系统·实习
一个小坑货1 天前
Rust基础
开发语言·后端·rust
Object~1 天前
【第九课】Rust中泛型和特质
开发语言·后端·rust
Crossoads1 天前
【汇编语言】call 和 ret 指令(一) —— 探讨汇编中的ret和retf指令以及call指令及其多种转移方式
android·开发语言·javascript·汇编·人工智能·数据挖掘·c#
码农飞飞1 天前
详解Rust结构体struct用法
开发语言·数据结构·后端·rust·成员函数·方法·结构体
Dontla2 天前
Rust derive macro(Rust #[derive])Rust派生宏
开发语言·后端·rust