清华大学操作系统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

相关文章

通过Docker容器部署Python环境的全流程

《通过Docker容器部署Python环境的全流程》在现代化开发流程中,Docker因其轻量化、环境隔离和跨平台一致性的特性,已成为部署Python应用的标准工具,本文将详细演示如何通过Docker容... 目录引言一、docker与python的协同优势二、核心步骤详解三、进阶配置技巧四、生产环境最佳实践

C++统计函数执行时间的最佳实践

《C++统计函数执行时间的最佳实践》在软件开发过程中,性能分析是优化程序的重要环节,了解函数的执行时间分布对于识别性能瓶颈至关重要,本文将分享一个C++函数执行时间统计工具,希望对大家有所帮助... 目录前言工具特性核心设计1. 数据结构设计2. 单例模式管理器3. RAII自动计时使用方法基本用法高级用法

SpringBoot 多环境开发实战(从配置、管理与控制)

《SpringBoot多环境开发实战(从配置、管理与控制)》本文详解SpringBoot多环境配置,涵盖单文件YAML、多文件模式、MavenProfile分组及激活策略,通过优先级控制灵活切换环境... 目录一、多环境开发基础(单文件 YAML 版)(一)配置原理与优势(二)实操示例二、多环境开发多文件版

使用docker搭建嵌入式Linux开发环境

《使用docker搭建嵌入式Linux开发环境》本文主要介绍了使用docker搭建嵌入式Linux开发环境,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面... 目录1、前言2、安装docker3、编写容器管理脚本4、创建容器1、前言在日常开发全志、rk等不同

Java实现远程执行Shell指令

《Java实现远程执行Shell指令》文章介绍使用JSch在SpringBoot项目中实现远程Shell操作,涵盖环境配置、依赖引入及工具类编写,详解分号和双与号执行多指令的区别... 目录软硬件环境说明编写执行Shell指令的工具类总结jsch(Java Secure Channel)是SSH2的一个纯J

Python ORM神器之SQLAlchemy基本使用完全指南

《PythonORM神器之SQLAlchemy基本使用完全指南》SQLAlchemy是Python主流ORM框架,通过对象化方式简化数据库操作,支持多数据库,提供引擎、会话、模型等核心组件,实现事务... 目录一、什么是SQLAlchemy?二、安装SQLAlchemy三、核心概念1. Engine(引擎)

python 线程池顺序执行的方法实现

《python线程池顺序执行的方法实现》在Python中,线程池默认是并发执行任务的,但若需要实现任务的顺序执行,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋... 目录方案一:强制单线程(伪顺序执行)方案二:按提交顺序获取结果方案三:任务间依赖控制方案四:队列顺序消

Python异步编程之await与asyncio基本用法详解

《Python异步编程之await与asyncio基本用法详解》在Python中,await和asyncio是异步编程的核心工具,用于高效处理I/O密集型任务(如网络请求、文件读写、数据库操作等),接... 目录一、核心概念二、使用场景三、基本用法1. 定义协程2. 运行协程3. 并发执行多个任务四、关键

Go语言连接MySQL数据库执行基本的增删改查

《Go语言连接MySQL数据库执行基本的增删改查》在后端开发中,MySQL是最常用的关系型数据库之一,本文主要为大家详细介绍了如何使用Go连接MySQL数据库并执行基本的增删改查吧... 目录Go语言连接mysql数据库准备工作安装 MySQL 驱动代码实现运行结果注意事项Go语言执行基本的增删改查准备工作

Java 与 LibreOffice 集成开发指南(环境搭建及代码示例)

《Java与LibreOffice集成开发指南(环境搭建及代码示例)》本文介绍Java与LibreOffice的集成方法,涵盖环境配置、API调用、文档转换、UNO桥接及REST接口等技术,提供... 目录1. 引言2. 环境搭建2.1 安装 LibreOffice2.2 配置 Java 开发环境2.3 配