在裸机上输出Hello,world! [rCore-lab1]

news2025/1/17 5:55:10

引言

非常简单的“Hello, world”应用程序,实际上有着多层硬件和软件工具和支撑环境隐藏在它背后,才让我们不必付出那么多努力就能够创造出功能强大的应用程序。生成应用程序二进制执行代码所依赖的是以 编译器 为主的开发环境;运行应用程序执行码所依赖的是以 操作系统 为主的执行环境。
这次本文章梳理文档,要在裸机上实现输出Hello,world!
在这里插入图片描述

代码树

./os/src
Rust 4 Files 119 Lines
Assembly 1 Files 11 Lines

├── bootloader(内核依赖的运行在 M 特权级的 SBI 实现,本项目中我们使用 RustSBI)
│ ├── rustsbi-k210.bin(可运行在 k210 真实硬件平台上的预编译二进制版本)
│ └── rustsbi-qemu.bin(可运行在 qemu 虚拟机上的预编译二进制版本)
├── LICENSE
├── os(我们的内核实现放在 os 目录下)
│ ├── Cargo.toml(内核实现的一些配置文件)
│ ├── Makefile
│ └── src(所有内核的源代码放在 os/src 目录下)
│ ├── console.rs(将打印字符的 SBI 接口进一步封装实现更加强大的格式化输出)
│ ├── entry.asm(设置内核执行环境的的一段汇编代码)
│ ├── lang_items.rs(需要我们提供给 Rust 编译器的一些语义项,目前包含内核 panic 时的处理逻辑)
│ ├── linker-k210.ld(控制内核内存布局的链接脚本以使内核运行在 k210 真实硬件平台上)
│ ├── linker-qemu.ld(控制内核内存布局的链接脚本以使内核运行在 qemu 虚拟机上)
│ ├── main.rs(内核主函数)
│ └── sbi.rs(调用底层 SBI 实现提供的 SBI 接口)
├── README.md
├── rust-toolchain(控制整个项目的工具链版本)
└── tools(自动下载的将内核烧写到 k210 开发板上的工具)
├── kflash.py
├── LICENSE
├── package.json
├── README.rst
└── setup.py

我的github

平台与目标三元组

通过 目标三元组 (Target Triplet) 来描述一个目标平台。它一般包括 CPU 架构、CPU 厂商、操作系统和运行时库。
修改为riscv64gc-unknown-none-elf

交叉编译 (Cross Compile)

 os/.cargo/config
 
[build]
target = "riscv64gc-unknown-none-elf"

移除标准库

在 main.rs 的开头加上一行 #![no_std] 来告诉 Rust 编译器不使用 Rust 标准库 std 转而使用核心库 core(core库不需要操作系统的支持)

提供panic_handler功能应对致命错误

在标准库 std 中提供了关于 panic! 宏的具体实现,其大致功能是打印出错位置和原因并杀死当前应用。但我们要实现的操作系统是不能使用还需依赖操作系统的标准库std,而更底层的核心库 core 中只有一个 panic! 宏的空壳,并没有提供 panic! 宏的精简实现。因此我们需要自己先实现一个简陋的 panic 处理

// os/src/lang_items.rs
use core::panic::PanicInfo;

#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop {}
}

移除main函数

语言标准库和三方库作为应用程序的执行环境,需要负责在执行应用程序之前进行一些初始化工作,然后才跳转到应用程序的入口点(也就是跳转到我们编写的 main 函数)开始执行。
start 语义项代表了标准库 std 在执行应用程序之前需要进行的一些初始化工作。由于我们禁用了标准库,编译器也就找不到这项功能的实现了。

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

编写内核第一条指令

.text.entry 区别于其他 .text 的目的在于我们想要确保该段被放置在相比任何其他代码段更低的地址上。这样,作为内核的入口点,这段指令才能被最先执行。

 # os/src/entry.asm
     .section .text.entry
     .globl _start
 _start:
     li x1, 100

在 main.rs 中嵌入这段汇编代码,这样 Rust 编译器才能够注意到它,不然编译器会认为它是一个与项目无关的文件:

// os/src/main.rs
#![no_std]
#![no_main]

mod lang_item;

use core::arch::global_asm;
global_asm!(include_str!("entry.asm"));

调整内核的内存布局

通过 链接脚本 (Linker Script) 调整链接器的行为,使得最终生成的可执行文件的内存布局符合我们的预期。

第3 行定义了一个常量 BASE_ADDRESS 为 0x80200000 ,也就是我们之前提到的初始化代码被放置的地址;

从第 5 行开始体现了链接过程中对输入的目标文件的段的合并。

因为所有的段都从 BASE_ADDRESS 也即 0x80200000 开始放置,这就能够保证内核的第一条指令正好放在 0x80200000 从而能够正确对接到 Qemu 上。

UTPUT_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)
    }
}

手动加载内核可执行文件

我们不能将其直接提交给 Qemu ,因为它除了实际会被用到的代码和数据段之外还有一些多余的元数据,这些元数据无法被 Qemu 在加载文件时利用,且会使代码和数据段被加载到错误的位置。

使用如下命令可以丢弃内核可执行文件中的元数据得到内核镜像:
rust-objcopy --strip-all target/riscv64gc-unknown-none-elf/release/os -O binary target/riscv64gc-unknown-none-elf/release/os.bin

分配并使用启动栈

分配启动栈空间,并在控制权被转交给 Rust 入口之前将栈指针 sp 设置为栈顶的位置。

# os/src/entry.asm
    .section .text.entry
    .globl _start
_start:
    la sp, boot_stack_top
    call rust_main

    .section .bss.stack
    .globl boot_stack
boot_stack:
    .space 4096 * 16
    .globl boot_stack_top
boot_stack_top:

控制权转交给 Rust 入口之前会执行两条指令,它们分别位于 entry.asm 的第 5、6 行。第 5 行我们将栈指针 sp 设置为先前分配的启动栈栈顶地址,这样 Rust 代码在进行函数调用和返回的时候就可以正常在启动栈上分配和回收栈帧了。第 6 行我们通过伪指令 call 调用 Rust 编写的内核入口点 rust_main 将控制权转交给 Rust 代码。

在 rust_main 函数的开场白中,我们将第一次在栈上分配栈帧并保存函数调用上下文,它也是内核运行全程最深的栈帧。

我们顺便完成对 .bss 段的清零。这是内核很重要的一部分初始化工作,在使用任何被分配到 .bss 段的全局变量之前我们需要确保 .bss 段已被清零。我们就在 rust_main 的开头完成这一工作,由于控制权已经被转交给 Rust ,我们终于不用手写汇编代码而是可以用 Rust 来实现这一功能了:

// os/src/main.rs
#[no_mangle]
pub fn rust_main() -> ! {
    clear_bss();
    loop {}
}

fn clear_bss() {
    extern "C" {
        fn sbss();
        fn ebss();
    }
    (sbss as usize..ebss as usize).for_each(|a| {
        unsafe { (a as *mut u8).write_volatile(0) }
    });
}

使用 RustSBI 提供的服务

在内核运行时响应内核的请求为内核提供服务。当内核发出请求时,计算机会转由 RustSBI 控制来响应内核的请求,待请求处理完毕后,计算机控制权会被交还给内核。

// os/src/main.rs
mod sbi;

// os/src/sbi.rs
use core::arch::asm;
#[inline(always)]
fn sbi_call(which: usize, arg0: usize, arg1: usize, arg2: usize) -> usize {
    let mut ret;
    unsafe {
        asm!(
            "ecall",
            inlateout("x10") arg0 => ret,
            in("x11") arg1,
            in("x12") arg2,
            in("x17") which,
        );
    }
    ret
}

服务 SBI_CONSOLE_PUTCHAR 可以用来在屏幕上输出一个字符。我们将这个功能封装成 console_putchar 函数:

// os/src/sbi.rs
pub fn console_putchar(c: usize) {
    sbi_call(SBI_CONSOLE_PUTCHAR, c, 0, 0);
}

将关机服务 SBI_SHUTDOWN 封装成 shutdown 函数:

// os/src/sbi.rs
pub fn shutdown() -> ! {
    sbi_call(SBI_SHUTDOWN, 0, 0, 0);
    panic!("It should shutdown!");
}

实现格式化输出

console_putchar 的功能过于受限,如果想打印一行 Hello world! 的话需要进行多次调用。自己编写基于 console_putchar 的 println! 宏。

// os/src/main.rs
#[macro_use]
mod console;

// os/src/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)+)?));
    }
}

处理致命错误

借助前面实现的 println! 宏和 shutdown 函数,我们可以在 panic 函数中打印错误信息并关机:

// os/src/main.rs
#![feature(panic_info_message)]

// os/src/lang_item.rs
use crate::sbi::shutdown;
use core::panic::PanicInfo;

#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
    if let Some(location) = info.location() {
        println!(
            "Panicked at {}:{} {}",
            location.file(),
            location.line(),
            info.message().unwrap()
        );
    } else {
        println!("Panicked: {}", info.message().unwrap());
    }
    shutdown()
}

最终测试

1


参考:rCore-Tutorial文档

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/23880.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

寻找更好的分类模型loss

寻找更好的loss1.CE loss并不完美2.可能更好的loss函数2.1 CC-LOSS2.2 Center-LOSS参考文献1.CE loss并不完美 最常用于深度学习分类模型的损失函数可以说就是CE(交叉熵) loss了。正如CC-LOSS paper中所述,该loss更关注各类是否separated,而非不同类之间…

Zookeeper:实现“通知协调”的 Demo

应用配置集中到节点上,应用启动时主动获取,并在节点上注册一个 watcher,每次配置更新都会通知到应用。数据发布/订阅(Publish/Subscribe)系统,即所谓的配置中心,顾名思义就是发布者将数据发布到…

[机器翻译]——pivot-based zero-shot translation based on fairseq

文章目录前言翻译到en生成"伪"的、到英语的数据文件把每一个zs语言对翻译到en从fairseq-generate生成的文件中,抽取纯en文件把en数据和所有zs语言对的tgt数据形成平行语料,然后做预处理形成en到tgt的平行语料预处理在en到tgt语言的"伪&qu…

IMC附录A

目录 A.1 恒等式与不等式 THEOREM A.1 (Binomial expansion theorem) PROPOSITION A.2 PROPOSITION A.3 PROPOSITION A.4 A.2 渐进符号 DEFINITION A.5 A.3 概率论基础 PROPOSITION A.7 (Union Bound) THEOREM A.8 (Bayes’ Theorem) PROPOSITION A.9 PROPOSITI…

图扑 Web SCADA 零代码组态水泥生产工艺流程 HMI

水泥是建筑工业三大基本材料之一,素有“建筑工业的粮食”之称。2022 年 1-9 月水泥产量为 15.63 亿吨,生产方法包括新型干法、立窑、湿窑、干法中空窑和立波尔窑等。 水泥生产线链条长、关键环节多的特性要求执行严密的流程监控。图扑软件大屏组态、UI 组…

国内家具行业数据浅析

大家好,这里是小安说网控。 家具是国民消费必需品之一,受疫情影响,近期销量数据不佳。2022年上半年,规模以上家具制造业企业营业收入3604亿元,同比下降4%;实现利润总额174.8亿元,同比增长2.6%。…

数据结构排序算法之冒泡排序

一 相关概念 稳定排序:如果原数据中a在b之前,而且ab,排序后a任然在b之前 不稳定排序:如果原数据中a在b之前,而且ab,排序后a在b之后 时间复杂度:对排序数据的总的操作次数,反映当n变…

安装Ruby和安装Rails详细步骤详解

rbenv安装Ruby rbenv可以管理多个版本的ruby。可以分为3种范围(或者说不同生效作用域)的版本: local版:本地,针对各项目范围(只在某个目录下有效) global版:全局,没有shell和local版时使用global版 shell版&#xf…

[MySQL]-删库后恢复

[MySQL]-删库后恢复 sen格 | 2022年11月 本文旨在记录个人在数据库的删库恢复演练过程中的一些总结,如有不足,欢迎指正。 一、恢复场景 1)假设生产实例MySQL端口为:3306 2)本地实例MySQL端口为:3307 在这…

pytorch快速入门

文章目录一、Tensorstensors的初始化(四种):tensors的属性和numpy的联系二、数据集的数据加载器加载数据集标号和可视化自己创建数据集用DataLoaders准备数据用于训练Transforms三、神经网络准备训练设备定义网络的类模型的layersnn.Flattennn.Linearnn.ReLUnn.Sequentialnn.So…

Python之基本扩展模块

一、datetime模块 1.1 主要的模块 datetime.date() #处理日期(年、月、日) datetime.time() #处理时间(时、分、秒和毫秒) datetime.datetime() #处理日期时间 datetime.timedelta() #处理时段(时间间隔…

基于Java+springboot+SSM的医疗报销系统的设计与实现

项目开发工具: IDEA, MYSQL, JDK1.8 项目使用技术: SpringBoot, SSM, H-UI, JSP, JQUERY, HTML 医疗报销系统【功能列表】 【前台用户】登录,注册,首页新闻轮播图,首页新闻按分类展示列表,栏目分类模块,报销流程模块,修改密码,个人信息展示,新增家庭成员, 家庭成品列表展示,…

Spring框架教程

Spring框架教程Spring框架教程1. 前言2. Spring框架概述2.1 什么是spring?2.2 Spring有哪些优点?2.3 Spring 有两个核心部分:IoC 和AOP2.4 Spring 特点2.5 Spring架构图,Spring由哪些模块组成?3. IOC容器3.1 IOC底层原理3.2 什么…

three.js初时基础

第一步:找到Three.js – JavaScript 3D Library (threejs.org) 第二步 第三步: 第四步: 安装依赖 第五步:新建一个项目文件,在文件中npm init 进行初始化出现一个package.json 第六步:配置安装🚀 快速开…

QtAV环境配置

本文章主要是使用MSVC编译器,因为QtAV是依赖FFmpeg的,所以需要下载QtAV源码和QtAV-depends-windows-x86x64; 官网地址:http://www.qtav.org/ Github 地址:https://github.com/wang-bin/QtAV 1,解压 将文件…

产品生命周期(PLM)发展历程及技术核心分析指导

产品生命周期管理(Product Lifecycle Management,简称PLM),是一种为企业产品全生命周期提供服务的软件解决方案,可以应用于在单一地点或分散在多个地点的企业内部,以及在产品研发领域,具有协作关系的企业之间&#xff…

windows 锁屏时执行某个程序

目录 前言 1 打开锁屏事件 2 创建任务计划程序 3 测试 前言 以windows10为例,这个功能的核心是使用windows自带的“任务计划程序”,可以帮助您实现触发器操作。 1 打开锁屏事件 默认情况下,锁屏事件并不会被系统记录,需要手动打…

vue3项目的创建、入口文件、全局方法、生命周期函数、setup中的生命周期函数使用、data的函数方式

文章目录1. 创建vue3项目1.1 基于webpack的工程创建1.2 通过vite来创建vue3项目vue3插件推荐1.3 通过npm init vue3创建项目2. vue3入口文件3. vue3中的全局方法修改4. vue3中封装全局方法5. vue3生命周期函数6. setup中生命周期使用7. data函数方式1. 创建vue3项目 1.1 基于w…

Spring Boot中消息是什么?同步异步消息是啥/都包含那些技术?Activate MQ消息怎么整合

写在前面: 继续记录自己的SpringBoot学习之旅,这次是SpringBoot应用相关知识学习记录。若看不懂则建议先看前几篇博客,详细代码可在我的Gitee仓库SpringBoot克隆下载学习使用! 3.5.4 消息 3.5.4.1 简述 消息发送方:…

力扣(LeetCode)29. 两数相除(C++)

快速乘 题解只使用了 intintint 。 万恶的 INT_MININT\_MININT_MIN,怎么处理?打不过就加入——被除数和除数转为负数计算 。 xxx 除以 yyy ,等于从 xxx 中拿出若干个 yyy 。 从 xxx 中拿出 yyy 的数量,就是 xyx\div yxy 的商。 y…