手写操作系统篇:实现裸机应用程序

文章目录

  • 前言
  • 操作系统执行环境
  • 创建裸机平台项目
  • Rust的Core库
  • 移除标准库依赖
  • Qemu 启动流程
  • 内存布局
  • 编译流程
  • 内核的初始指令
  • 调整内核的内存布局
  • 手动加载内核可执行文件
  • 使用RustSBI提供的服务
  • 添加bootloader模块
  • 添加Makefile
  • 运行
  • 停止
  • 总体架构

前言

我们既然是手写操作系统,那么就不能再继续依赖底层操作系统了,就需要我们直接和硬件对接

操作系统执行环境

在这里插入图片描述

创建裸机平台项目

  1. 新建项目
cargo new os--bin
  1. 简单运行一下程序,输出hello world
cargo run
  1. 将目标平台切换到risc-v裸机平台
rustup target add riscv64gc-unknown-none-elf
# os/.cargo/config
[build]
target = "riscv64gc-unknown-none-elf"
  1. 再次运行
cargo run
# 编译报错,没有发现rust的std标准库

Rust的Core库

我们rust的std库在risc-v裸机平台不能使用了,不过rust提供了一个相对受限的core库来支持在这种裸机平台上运行,这样就可以继续和计算机硬件对线了,我们可以借助它来完成我们的裸机应用程序

移除标准库依赖

  1. 增加错误处理

os/src/lang_items.rs

use core::panic::PanicInfo;

#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop{}
}
  1. 移除main函数

os/src/main.rs

#![no_main]
#![no_std]
mod lang_items;
  1. 构建测试
cargo build

此时不出意外的话,可以成功编译

Qemu 启动流程

  1. 第一阶段
    加电后pc寄存器会存放rom(只读存储器)的物理地址,然后cpu会运行rom内部的固件代码来初始化cpu,并加载bootloader,然后控制权切换到bootloader
  2. 第二阶段
    bootloader继续初始化cpu,然后加载操作系统镜像,之后控制权切换到操作系统
  3. 第三阶段
    操作系统开始运转
    在这里插入图片描述

内存布局

经典程序内存布局
在这里插入图片描述

  1. stack
    存放函数调用上下文,函数作用域内的局部变量,向低地址增长
  2. heap
    存放程序运行时动态分配的数据,向高地址增长
  3. .bss
    存放程序中未初始化的全局数据,通常由程序的加载者代为进行零初始化,即将这块区域逐字节清零
  4. .data
    存放已初始化数据段保存程序中那些已初始化的且可修改的全局数据
  5. .rodata
    存放已初始化数据段保存程序中那些已初始化的且但不可修改的全局常量数据
  6. .text
    存放程序的汇编代码

编译流程

  1. 编译
    编译器 (Compiler) 将每个源文件从某门高级编程语言转化为汇编语言,注意此时源文件仍然是一个 ASCII 或其他编码的文本文件

  2. 汇编
    汇编器 (Assembler) 将上一步的每个源文件中的文本格式的指令转化为机器码,得到一个二进制的目标文件 (Object File)

  3. 链接
    链接器 (Linker) 将上一步得到的所有目标文件以及一些可能的外部目标文件链接在一起形成一个完整的可执行文件

内核的初始指令

os/src/entry.asm

    .section .text.entry
    .globl _start
_start:
    la sp, boot_stack_top
    call chenix_main

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

第1行是将第1行后面的内容放进.text.entry的段中,.text.entry比其它的.text代码段存放在更低的地址上,所以这段指令最先执行,是内核的入口
第2行是声明全局符号_start
第3行_start符号指向紧跟在符号后面的内容,因此符号 _start 的地址即为第 5 行的指令所在的地址
第4行将栈指针 sp 设置为栈顶 boot_stack_top 的位置
第5行调用rust的入口函数
第7行我们将这块空间放置在一个名为 .bss.stack 的段中,后续情节中的链接脚本 linker.ld 把 .bss.stack 段最终会被汇集到 .bss 段中
第8行声明全局符号boot_stack_lower_bound
第9行boot_stack_lower_bound符号指向后面的内容,符号 boot_stack_lower_bound指向了一块大小为 4096 * 16 字节的栈空间的栈底
第12行声明全局符号boot_stack_top
第13行用更高地址的boot_stack_top符号做为栈顶

嵌入该指令到项目里,确保编译器能够正常识别
os/src/main.rs

#![no_main]
#![no_std]
mod lang_items;

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

第7行include_str!宏将同目录下的entry.asm的内容转换成字符串,然后global_asm!宏嵌入将其嵌入代码中

调整内核的内存布局

因为链接器默认的内存布局不能满足我们的需求,为了和 Qemu 对接,我们能使用链接脚本调整链接器(Linker Script)的行为,使链接器生成的可执行文件的内存布局来适配 Qemu ,我们需要让内核的第一条指令的地址位于 0x80200000

修改配置
os/.cargo/config

 [build]
 target = "riscv64gc-unknown-none-elf"

 [target.riscv64gc-unknown-none-elf]
 rustflags = [
     "-Clink-arg=-Tsrc/linker.ld", "-Cforce-frame-pointers=yes"
 ]

链接脚本

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

第1行设置了目标平台为riscv
第2行设置了程序的入口点为之前定义的全局符号 _start
第3行定义了常量BASE_ADDRESS为0x80200000,即 Qemu 执行内核的初始化代码的起始地址
从第5行开始体现了链接过程中对输入的目标文件的段的合并,其中 . 表示当前地址,链接器会从它指向的位置开始往下放置从输入的目标文件中收集来的段。可以对 . 赋值来调整接下来的段放在哪里,也可以创建一些全局符号赋值为 . 从而记录这一时刻的位置。我们还能够看到这样的格式:

.rodata : {
    *(.rodata)
}

冒号前面表示最终生成的可执行文件的一个段的名字,花括号内按照放置顺序描述将所有输入目标文件的哪些段放在这个段中,每一行格式为 (SectionName),表示目标文件 ObjectFile 的名为 SectionName 的段需要被放进去。可以使用通配符来书写 和 分别表示可能的输入目标文件和段名。最终的合并结果是,在最终可执行文件中各个常见的段 .text, .rodata .data, .bss 从低地址到高地址按顺序放置,每个段里面都包括了所有输入目标文件的同名段,且每个段都有两个全局符号给出了它的开始和结束地址(比如 .text 段的开始和结束地址分别是 stext 和 etext )

第 12 行我们将包含内核第一条指令的 .text.entry 段放在最终的 .text 段的最开头,同时注意到在最终内存布局中代码段 .text 又是先于任何其他段的。因为所有的段都从 BASE_ADDRESS 也即 0x80200000 开始放置,这就能够保证内核的第一条指令正好放在 0x80200000 从而能够正确对接到 Qemu 上

生成内核可执行文件

cargo build --release

手动加载内核可执行文件

虽然目前我们的内核可执行文件符合 Qemu 的内存布局要求,但是还不能将其直接提交给 Qemu ,因为它除了实际会被用到的代码和数据段之外还有一些多余的元数据,这些元数据无法被 Qemu 在加载文件时利用,且会使代码和数据段被加载到错误的位置
在这里插入图片描述

rust-objcopy --strip-all target/riscv64gc-unknown-none-elf/release/os -O binary target/riscv64gc-unknown-none-elf/release/os

os/src/main.rs

#[no_mangle]
pub fn chenix_main() -> ! {
	clean_bss()
    loop {}
}

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

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

clean_bss函数用来清零.bss段,也就是清零从sbss开始到ebss内存,其中sbss和ebss是链接脚本linker.ld提供的

使用RustSBI提供的服务

内核向RustSBI发送请求,当请求处理完毕时,计算机将控制权转移给内核,从内存布局的角度来思考,每一层执行环境都对应到内存中的一段代码和数据,这里的控制权转移指的是 CPU 从执行一层软件的代码到执行另一层软件的代码的过程。这个过程和函数调用比较像,但是内核无法通过函数调用来请求 RustSBI 提供的服务,这是因为内核并没有和 RustSBI 链接到一起,我们仅仅使用 RustSBI 构建后的可执行文件,因此内核对于 RustSBI 的符号一无所知

os/src/sbi.rs

use core::{arch::asm};

const SBI_SET_TIMER: usize = 0;
const SBI_CONSOLE_PUTCHAR: usize = 1;
const SBI_CONSOLE_GETCHAR: usize = 2;
const SBI_CLEAR_IPI: usize = 3;
const SBI_SEND_IPI: usize = 4;
const SBI_REMOTE_FENCE_I: usize = 5;
const SBI_REMOTE_SFENCE_VMA: usize = 6;
const SBI_REMOTE_SFENCE_VMA_ASID: usize = 7;
const SBI_SHUTDOWN: usize = 8;

#[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
}

pub fn console_putchar(c: usize) {
    sbi_call(SBI_CONSOLE_PUTCHAR, c, 0, 0);
}

pub fn shutdown() -> ! {
    sbi_call(SBI_SHUTDOWN, 0, 0, 0);
    unreachable!()
}

实现sbi的调用接口,并实现了console_putchar函数和shutdown函数

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

使用console_putchar实现标准输出,并实现print!宏和println!宏

os/src/lang_items.rs

use core::panic::{PanicInfo, Location};
use crate::{sbi::shutdown, println};

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

panic函数,打印panic的信息,若存在位置信息就额外打印调用panic的文件信息,行数信息

os/src/main.rs

#![no_main]
#![no_std]
#![feature(panic_info_message)]
mod lang_items;
mod sbi;

#[macro_use]
mod console;

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

#[no_mangle]
pub fn chenix_main() {
    clean_bss();
    println!("Hello Chenix!");
    loop{};
}

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

主函数打印信息后panic

添加bootloader模块

https://github.com/go75/chenix/tree/main/bootloader
下载这个bootloader包,和os目录放在同一级目录下,内核依赖的运行在 M 特权级的 SBI 实现,本项目中使用 RustSBI

添加Makefile

os/Makefile

# Building
TARGET := riscv64gc-unknown-none-elf
MODE := release
KERNEL_ELF := target/$(TARGET)/$(MODE)/os
KERNEL_BIN := $(KERNEL_ELF).bin
DISASM_TMP := target/$(TARGET)/$(MODE)/asm

# Building mode argument
ifeq ($(MODE), release)
	MODE_ARG := --release
endif

# BOARD
BOARD := qemu
SBI ?= rustsbi
BOOTLOADER := ../bootloader/$(SBI)-$(BOARD).bin

# KERNEL ENTRY
KERNEL_ENTRY_PA := 0x80200000

# Binutils
OBJDUMP := rust-objdump --arch-name=riscv64
OBJCOPY := rust-objcopy --binary-architecture=riscv64

# Disassembly
DISASM ?= -x

build: env $(KERNEL_BIN)

env:
	(rustup target list | grep "riscv64gc-unknown-none-elf (installed)") || rustup target add $(TARGET)
	cargo install cargo-binutils
	rustup component add rust-src
	rustup component add llvm-tools-preview

$(KERNEL_BIN): kernel
	@$(OBJCOPY) $(KERNEL_ELF) --strip-all -O binary $@

kernel:
	@echo Platform: $(BOARD)
	@cp src/linker-$(BOARD).ld src/linker.ld
	@cargo build $(MODE_ARG)
	@rm src/linker.ld

clean:
	@cargo clean

disasm: kernel
	@$(OBJDUMP) $(DISASM) $(KERNEL_ELF) | less

disasm-vim: kernel
	@$(OBJDUMP) $(DISASM) $(KERNEL_ELF) > $(DISASM_TMP)
	@vim $(DISASM_TMP)
	@rm $(DISASM_TMP)

run: run-inner

QEMU_ARGS := -machine virt \
			 -nographic \
			 -bios $(BOOTLOADER) \
			 -device loader,file=$(KERNEL_BIN),addr=$(KERNEL_ENTRY_PA)

run-inner: build
	@qemu-system-riscv64 $(QEMU_ARGS)

debug: build
	@tmux new-session -d \
		"qemu-system-riscv64 $(QEMU_ARGS) -s -S" && \
		tmux split-window -h "riscv64-unknown-elf-gdb -ex 'file $(KERNEL_ELF)' -ex 'set arch riscv:rv64' -ex 'target remote localhost:1234'" && \
		tmux -2 attach-session -d

gdbserver: build
	@qemu-system-riscv64 $(QEMU_ARGS) -s -S

gdbclient:
	@riscv64-unknown-elf-gdb -ex 'file $(KERNEL_ELF)' -ex 'set arch riscv:rv64' -ex 'target remote localhost:1234'

.PHONY: build env kernel clean disasm disasm-vim run-inner gdbserver gdbclient

运行

弄了这么久,在os目录下,执行命令

make run

运行截图

在这里插入图片描述

chenix启动!

停止

按 ctrl + a 然后松开,再按 x

总体架构

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

行业追踪,2023-11-03

自动复盘 2023-11-03 凡所有相,皆是虚妄。若见诸相非相,即见如来。 k 线图是最好的老师,每天持续发布板块的rps排名,追踪板块,板块来开仓,板块去清仓,丢弃自以为是的想法,板块去留让…

学习小结,学而时习之,坚持学习之,温顾学习之

学习python一个多月了,之前也有接触过,还花了不少钱报班,看了看入门的头两节课,就止步了。每一种编程语言的入门感觉都差不多,学到现在,我对python的基本数据类型还是没掌握好啊,每次列表字典怎…

js 根据word文档模板导出内容

一、创建word导出模板 1、本地创建一个test.docx 2、将最终需要的文档内容及样式编辑完成(图1) 3、将所需动态值的位置,替换为变量参数(图2) 注: 动态值书写 图1 图2 模板值的书写要求 二、项目中使用 1、安装依赖 npm install docxtemplater-image-module-free --save n…

4+m6A+机器学习+分型,要素过多,没有思路的同学可借鉴

今天给同学们分享一篇生信文章“Diagnostic, clustering, and immune cell infiltration analysis of m6A regulators in patients with sepsis”,这篇文章发表在Sci Rep.期刊上,影响因子为4.6。 结果解读: 脓毒症中m6A调节因子的转录改变 …

[PyTorch][chapter 61][强化学习-免模型学习1]

前言: 在现实的学习任务中,环境 其中的转移概率P,奖赏函数R 是未知的,或者状态X也是未知的 称为免模型学习(model-free learning) 目录: 1: 蒙特卡洛强化学习 2:同策略-蒙特卡洛强化学习 3&am…

非关系型数据库Redis的安装【Linux】及常用命令

前言 Redis(Remote Dictionary Server)是一种开源的内存数据库管理系统,它以键值存储方式来存储数据,并且支持多种数据结构,如字符串、哈希、列表、集合、有序集合等。Redis最初由Salvatore Sanfilippo开发&#xff0c…

Java配置47-Spring Eureka 未授权访问漏洞修复

文章目录 1. 背景2. 方法2.1 Eureka Server 添加安全组件2.2 Eureka Server 添加参数2.3 重启 Eureka Server2.4 Eureka Server 升级版本2.5 Eureka Client 配置2.6 Eureka Server 添加代码2.7 其他问题 1. 背景 项目组使用的 Spring Boot 比较老,是 1.5.4.RELEASE…

React使用富文本CKEditor 5,上传图片并可设置大小

上传图片 基础使用(标题、粗体、斜体、超链接、缩进段落、有序无序、上传图片) 官网查看:https://ckeditor.com/docs/ckeditor5/latest/installation/integrations/react.html 安装依赖 npm install --save ckeditor/ckeditor5-react cked…

【window系统】win10家庭版没有hyper-V的选项 问题解决办法

目录 事件起因环境和工具操作过程参考内容结束语 事件起因 在给我的新电脑安装docker和对应的Linux环境时 在 控制面版-程序-启用该或关闭Windows功能 时 没有找到对应网上那种教程里的Hyper-V的功能的选项,查找了一下,说是win10家庭版没有这个功能 专…

速学数据结构 | 循环队列怎么写才最高效?只需要掌握这些技巧

🎬 鸽芷咕:个人主页 🔥 个人专栏:《Linux深造日志》《C干货基地》 ⛺️生活的理想,就是为了理想的生活! 文章目录 📋 前言一、什么是循环队列?二、如何实现循环队列?2.1 循环队列的结构2.2 循环…

SpringBoot集成MyBatis-Plus

SpringBoot集成MyBatis-Plus 文章目录 SpringBoot集成MyBatis-Plusapplication.ymlpom.xmlpojomapperserviceserviceimplconfigutilsweb 懒得打一遍,直接copy: SpringBoot集成MyBatis-Plus application.yml # 端口 server:port: 8080 # 数据源 spring:…

期中成绩怎么发布?

作为一名老师,期中考试结束后,你可能正在为如何发布成绩而烦恼。传统的纸质方式不仅耗时而且容易出错,那么有没有一种方式可以让学生自助查询成绩呢?答案是肯定的。下面就为你介绍几种实用的方法,让成绩发布变得轻松又…

默认路由配置

默认路由: 在末节路由器上使用。(末节路由器是前往其他网络只有一条路可以走的路由器) 默认路由被称为最后的关卡,也就是静态路由不可用并且动态路由也不可用,最后就会选择默认路由。有时在末节路由器上写静态路由时…

性能优于BERT的FLAIR:一篇文章入门Flair模型

文章目录 What is FLAIR?FLAIR ModelContextual String Embedding for Sequence Labelingexample FLAIR Application AreaSentiment AnalysisNamed Entity RecognitionText Classification FLAIR一、什么是FLAIR?二、FLAIR Library的优势是什么&#xff…

日本it培训学费 想去日本做IT,需要具备哪些技术?

日本的IT行业历史比较悠久,业务以上层前端业务为主,如设计和构建软件。日本IT公司组织庞大,行业内部有着严格的分工和部署,工作会被细分化,分配给个人的工作量不会太大,难度也不会很高,所以日本…

【JAVA学习笔记】59 - JUnit框架使用、本章作业

项目代码 https://github.com/yinhai1114/Java_Learning_Code/tree/main/IDEA_Chapter15/src/com/yinhai/homework JUnit测试框架 1.基本介绍 1. JUnit是一个Java语言的单元测试框架 2.多数Java的开发环境都已经集成了JUnit作为单元测试的工具 2.如何使用 创建方法后&#x…

关于msvcp120.dll丢失的解决方法详解,快速解决dll丢失问题

在计算机使用过程中,经常会遇到“msvcp120.dll丢失”的错误提示。这个错误提示通常出现在运行某些程序或游戏时,造成相关应用程序可能无法正常启动或运行。那么,究竟是什么原因导致了msvcp120.dll文件的丢失呢?本文将详细解析msvc…

react使用react-sortable-hoc实现拖拽

react-sortable-hoc拖拽 安装 npm install react-sortable-hoc --save 代码如下(示例): import React, { useImperativeHandle, forwardRef, memo, useState } from react;import { DrawerForm } from ant-design/pro-form;import { messag…

竖拍的视频怎么做二维码?竖版视频二维码制作技巧

为了方便视频的展示和传播,现在将视频生成二维码后来使用的方式越来越常见,很多做二维码工具都可以制作视频二维码,但是无法设置下载权限或者播放竖版视频。那么如果做有下载功能的视频码该如何制作,可能很多小伙伴都不知道怎么做…

Idea 对容器中的 Java 程序断点远程调试

第一种:简单粗暴型 直接在java程序中添加log.info(),根据需要打印信息然后打包覆盖,根据日志查看相关信息 第二种:远程调试 在IDEA右上角点击编辑配置设置相关参数在Dockerfile中加入 "-jar", "-agentlib:jdwp…