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

本文主要是介绍清华大学操作系统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看看这个项目的结构:
在这里插入图片描述
可以进入该文件目录,输入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

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和内核镜像:

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设置为栈顶的位置。
在这里插入图片描述
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的代码:

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进行完善


六、总结

这篇关于清华大学操作系统rCore实验-第一章-应用程序与基本执行环境的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/642656

相关文章

SQL BETWEEN 语句的基本用法详解

《SQLBETWEEN语句的基本用法详解》SQLBETWEEN语句是一个用于在SQL查询中指定查询条件的重要工具,它允许用户指定一个范围,用于筛选符合特定条件的记录,本文将详细介绍BETWEEN语... 目录概述BETWEEN 语句的基本用法BETWEEN 语句的示例示例 1:查询年龄在 20 到 30 岁

mysql中insert into的基本用法和一些示例

《mysql中insertinto的基本用法和一些示例》INSERTINTO用于向MySQL表插入新行,支持单行/多行及部分列插入,下面给大家介绍mysql中insertinto的基本用法和一些示例... 目录基本语法插入单行数据插入多行数据插入部分列的数据插入默认值注意事项在mysql中,INSERT I

python常见环境管理工具超全解析

《python常见环境管理工具超全解析》在Python开发中,管理多个项目及其依赖项通常是一个挑战,下面:本文主要介绍python常见环境管理工具的相关资料,文中通过代码介绍的非常详细,需要的朋友... 目录1. conda2. pip3. uvuv 工具自动创建和管理环境的特点4. setup.py5.

mapstruct中的@Mapper注解的基本用法

《mapstruct中的@Mapper注解的基本用法》在MapStruct中,@Mapper注解是核心注解之一,用于标记一个接口或抽象类为MapStruct的映射器(Mapper),本文给大家介绍ma... 目录1. 基本用法2. 常用属性3. 高级用法4. 注意事项5. 总结6. 编译异常处理在MapSt

Python中使用uv创建环境及原理举例详解

《Python中使用uv创建环境及原理举例详解》uv是Astral团队开发的高性能Python工具,整合包管理、虚拟环境、Python版本控制等功能,:本文主要介绍Python中使用uv创建环境及... 目录一、uv工具简介核心特点:二、安装uv1. 通过pip安装2. 通过脚本安装验证安装:配置镜像源(可

MyBatis ResultMap 的基本用法示例详解

《MyBatisResultMap的基本用法示例详解》在MyBatis中,resultMap用于定义数据库查询结果到Java对象属性的映射关系,本文给大家介绍MyBatisResultMap的基本... 目录MyBATis 中的 resultMap1. resultMap 的基本语法2. 简单的 resul

Java 枚举的基本使用方法及实际使用场景

《Java枚举的基本使用方法及实际使用场景》枚举是Java中一种特殊的类,用于定义一组固定的常量,枚举类型提供了更好的类型安全性和可读性,适用于需要定义一组有限且固定的值的场景,本文给大家介绍Jav... 目录一、什么是枚举?二、枚举的基本使用方法定义枚举三、实际使用场景代替常量状态机四、更多用法1.实现接

git stash命令基本用法详解

《gitstash命令基本用法详解》gitstash是Git中一个非常有用的命令,它可以临时保存当前工作区的修改,让你可以切换到其他分支或者处理其他任务,而不需要提交这些还未完成的修改,这篇文章主要... 目录一、基本用法1. 保存当前修改(包括暂存区和工作区的内容)2. 查看保存了哪些 stash3. 恢

python获取cmd环境变量值的实现代码

《python获取cmd环境变量值的实现代码》:本文主要介绍在Python中获取命令行(cmd)环境变量的值,可以使用标准库中的os模块,需要的朋友可以参考下... 前言全局说明在执行py过程中,总要使用到系统环境变量一、说明1.1 环境:Windows 11 家庭版 24H2 26100.4061

pytest+allure环境搭建+自动化实践过程

《pytest+allure环境搭建+自动化实践过程》:本文主要介绍pytest+allure环境搭建+自动化实践过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐... 目录一、pytest下载安装1.1、安装pytest1.2、检测是否安装成功二、allure下载安装2.