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

news2024/11/25 20:36:33

文章目录

  • 前言
  • 操作系统执行环境
  • 创建裸机平台项目
  • 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

总体架构

在这里插入图片描述

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

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

相关文章

峰回网关数采PLC

1.网络配置 例如:plc地址是192.168.1.56 1.访问网关 峰回网关默认网关地址 192.168.3.18,或者(10.10.253.354),本案例按照3.18讲解。 1和1相连,0和电脑相连 本地电脑修改ip为192.168.3.3(和3…

一分钟搞定!快速去除图片背景底色的步骤教程

最近不少小伙伴过来咨询,怎么样才能去除公章上的白色背景,一般电子公章很多都带有背景底色,使用起来非常不方便,所以今天就教大家一个不用ps就可以把图片转化透明底的方法,而且这是一款专业的在线ps工具;不…

SoftwareTest4 - 咋设计一个好的测试用例

咋设计一个好的测试用例 一 . 设计测试用例的万能公式功能测试性能测试界面测试兼容性测试易用性测试安全测试案例案例1 : 对水杯设计测试用例案例 2 : 对登录页面设计测试用例 二 . 具体设计测试用例的方法2.1 等价类等价类的概念等价类的用例编写 2.2 边界值2.3 判定表2.4 场…

docker部署MySQL服务

部署 MySQL8.0.35社区版 1.下载镜像 docker pull container-registry.oracle.com/mysql/community-server:8.0.35 查看镜像 docker images 2. 启动MySQL服务器实例 docker run --namemysql8 --restart on-failure -p 3309:3306 -d container-registry.oracle.com/mysql/comm…

2023-11-04 LeetCode每日一题(数组中两个数的最大异或值)

2023-11-04每日一题 一、题目编号 421. 数组中两个数的最大异或值二、题目链接 点击跳转到题目位置 三、题目描述 给你一个整数数组 nums &#xff0c;返回 nums[i] XOR nums[j] 的最大运算结果&#xff0c;其中 0 ≤ i ≤ j < n 。 示例 1&#xff1a; 示例 2&…

【项目管理】项目计划中常见影响进度的风险汇总

哈喽&#xff0c;大家好&#xff0c;我是雷工。 在项目实施过程中针对项目进度的计划常常会有各种各样的的风险&#xff0c;相比出了问题去救火与填坑&#xff0c;能够提前预知风险&#xff0c;并提前调整计划&#xff0c;更能有利于项目的如期交付。 以下为项目计划中影响进度…

周赛369(位运算、分类讨论、记忆化搜索==>动态规划、树形DP)

文章目录 周赛369[2917. 找出数组中的 K-or 值](https://leetcode.cn/problems/find-the-k-or-of-an-array/)位运算模拟 [2918. 数组的最小相等和](https://leetcode.cn/problems/minimum-equal-sum-of-two-arrays-after-replacing-zeros/)分类讨论 [2919. 使数组变美的最小增量…

计算机学院的老师们的实际编程能力如何?

计算机学院的老师们的实际编程能力如何&#xff1f; 国内的多数老师基本上就是学术界&#xff0c;工业界&#xff0c;教育界&#xff0c;这三个领域哪个他们都不沾边&#xff0c;几乎就是混日子&#xff0c;顺便能多混点钱就多混点钱的那种。 最近很多小伙伴找我&#xff0c;…

Day18力扣打卡

打卡记录 寻找重复数&#xff08;双指针&#xff09; 链接 Floyd判圈法&#xff0c;先用快慢指针以不同速率进行移动&#xff0c;最终一定会出现相遇点&#xff0c;然后在使一指针从初始开始&#xff0c;两指针再以同步调移动&#xff0c;再次相遇的点一定为循环开始的点位。 …

mysql 中!= 到底走不走索引?

mysql 中! 到底走不走索引&#xff1f; 很多人疑惑! 到底走不走索引&#xff0c; 这里可以肯定的说该操作是可以走索引的&#xff0c;但实际情况中都为啥都不走索引呢&#xff1f; 首先我们要知道走索引与数据量和数据趋势&#xff08;cardinality&#xff09;有很大的关系&…

用于比例方向控制阀的电控模块

比例方向控制阀电控放大器是一种电子装置&#xff0c;其主要功能是对弱电的控制信号进行整形、运算和功率放大&#xff0c;从而驱动比例方向控制阀。用于无位置传感器的比例方向控制阀。 通过控制PWM脉冲的占空比&#xff0c;可以控制比例阀端口的驱动电流&#xff0c;进而达到…

SelectorsHub插件使用教程

一、安装 进入chrome商店进行安装 二、使用SelectorsHub复制xpath 三、检查复制的xpath是否正确 1、右键检查 2、ctrlf&#xff0c;进行查找 3、复制xpath到查找框内 4、可以看出查找出一个元素 四、复制不同xpath的区别 1、copy relative xpath 相对XPath是相对于当前元素…

memtest86 prosite v10.6

passmark官方的memtest86 v10开始支持颗粒级别的坏内存芯片定位了&#xff0c;对于特定的若干种CPU和芯片组的组合&#xff0c;支持这项功能。 当然支持颗粒定位的site版本售价4800美金&#xff0c;是比较贵的。所以网络上出现了破解版的&#xff0c;人才真是。但是鼓励大家支…

运维基础-Docker容器命令部署

Docker基础知识 安装问题-有podmanCentos8使用yum install docker -y时&#xff0c;默认安装的是podman-docker软件安装docker yum list installed | grep dockeryum -y remove xxxxDocker安装配置下载安装docker启动docker&#xff0c;并设置开机启动下载所需镜像 centos镜像进…

精品基于Python的汽车销售趋势分析-爬虫可视化大屏

《[含文档PPT源码等]精品基于Python的汽车销售趋势分析-爬虫》该项目含有源码、文档、PPT、配套开发软件、软件安装教程、项目发布教程、包运行成功&#xff01; 软件开发环境及开发工具&#xff1a; 开发语言&#xff1a;python 使用框架&#xff1a;Django 前端技术&…

NiceGui:Python中的轻量级GUI框架初体验

目录 一、引言 二、NiceGui概述 三、NiceGui实战&#xff1a;一个简单的计算器应用 四、NiceGui与其他GUI框架的比较 五、注意事项 总结与展望 一、引言 Python作为一门功能强大且易于学习的编程语言&#xff0c;广泛应用于各种领域。在图形用户界面&#xff08;GUI&…

【实战Flask API项目指南】之二 Flask基础知识

实战Flask API项目指南之 Flask基础知识 本系列文章将带你深入探索实战Flask API项目指南&#xff0c;通过跟随小菜的学习之旅&#xff0c;你将逐步掌握Flask 在实际项目中的应用。让我们一起踏上这个精彩的学习之旅吧&#xff01; 前言 当小菜踏入Flask后端开发的世界&…

CodeWhisperer 的安装及体验

文章作者&#xff1a;Pony CodeWhisperer 是亚马逊出品的一款基于机器学习的通用代码生成器&#xff0c;可实时提供代码建议。类似 Cursor 和 Github Copilot 编码工具。 官网&#xff1a;https://aws.amazon.com/cn/codewhisperer/?trkcndc-detail 在编写代码时&#xff0c…

【unity3D】使用RawImage实现UI上的帧动画

&#x1f4a6;本专栏是我关于游戏开发的笔记 &#x1f236;本篇是一个简短的小知识点 使用RawImage实现帧动画 找一个帧动画连续的图片拖到工程中&#xff0c;将Texture Type改成Sprite&#xff08;2D和UI&#xff09;&#xff0c;点击apply应用上 在工程中新建一个RawImage,将…

0基础学习VR全景平台篇第117篇:利用插件地拍补地 - PS教程

上课&#xff01;全体起立~ 大家好&#xff0c;欢迎观看蛙色官方系列全景摄影课程&#xff01; 嗨&#xff0c;大家好&#xff0c;今天我们来介绍【PS利用插件地拍补地】。 之前已经教给大家补地插件的安装方法&#xff0c;今天我们教给大家如何使用这个插件进行补地。 首…