Trilobita OS

个人理解:写裸机上的OS相当于是使用SBI,指令集来替换我们之前使用的syscall/SBI,移除系统的支持

代码架构:

├── bootloader (内核依赖的运行在 M 特权级的 SBI 实现,本项目中我们使用 RustSBI)
│   └── rustsbi-qemu.bin
├── os
│   ├── Cargo.toml (cargo 项目配置文件)
│   ├── Makefile
│   └── src
│       ├── console.rs (将打印字符的 SBI 接口进一步封装实现更加强大的格式化输出)
│       ├── entry.asm (设置内核执行环境的的一段汇编代码)
│       ├── lang_items.rs (需要我们提供给 Rust 编译器的一些语义项,目前包含内核 panic 时的处理逻辑)
│       ├── linker.ld (控制内核内存布局的链接脚本以使内核运行在 qemu 虚拟机上)
│       ├── main.rs (内核主函数)
│       └── sbi.rs (封装底层 SBI 实现提供的 SBI 接口)
└── rust-toolchain (整个项目的工具链版本)

实验步骤:

  • 建好开发与实验环境
  • 移除标准库依赖
  • 支持函数调用
  • 基于SBI服务完成输出与关机

【建好开发与实验环境】

#创建开发环境
$ cargo new os
#------------------------------------------------------------------------------------------------------------#
#查看当前的结构
$ tree os
os
├── Cargo.toml
└── src
    └── main.rs
#------------------------------------------------------------------------------------------------------------#
#查看当前的构建信息
	#--version:此标志将打印出rustc的版本。
	#--verbose:与其他标记结合使用的时候,该标志将产生额外的输出。
	#host:一项表明默认目标平台是 x86_64-unknown-linux-gnu,CPU架构是x86_64,CPU厂商是unknown,操作系统是linux,运行时库是gnu libc。
$ rustc --version --verbose
rustc 1.62.1 (e092d0b6b 2022-07-16)
binary: rustc
commit-hash: e092d0b6b43f2de967af0887873151bb1c0b18d3
commit-date: 2022-07-16
host: x86_64-unknown-linux-gnu
release: 1.62.1
LLVM version: 14.0.5
	#host:windows下安装后三元组显示的则为此x86_64-pc-windows-msvc
C:\Users\11932>rustc --version --verbose
rustc 1.62.0 (a8314ef7d 2022-06-27)
binary: rustc
commit-hash: a8314ef7d0ec7b75c336af2c9857bfaf43002bfc
commit-date: 2022-06-27
host: x86_64-pc-windows-msvc
release: 1.62.0
LLVM version: 14.0.5
#------------------------------------------------------------------------------------------------------------#
#修改构建的目标平台
	#--target:选择要构建的目标三元组。
	#riscv64gc-unknown-none-elf:CPU架构是riscv64gc,厂商是unknown,操作系统是none, elf表示没有标准的运行时库。没有任何系统调用的封装支持,但可以生成ELF格式的执行程序。
$ cargo run --target riscv64gc-unknown-none-elf
   Compiling os v0.1.0 (/home/kondl/OS_Training/os)
error[E0463]: can't find crate for `std`
  |
  = note: the `riscv64gc-unknown-none-elf` target may not be installed
  = help: consider downloading the target with `rustup target add riscv64gc-unknown-none-elf`

error: cannot find macro `println` in this scope
 --> src/main.rs:2:5
  |
2 |     println!("Hello, world!");
  |     ^^^^^^^

error: requires `sized` lang_item

For more information about this error, try `rustc --explain E0463`.
error: could not compile `os` due to 3 previous errors

报错的原因是目标平台上确实没有 Rust 标准库 std,也不存在任何受 OS 支持的系统调用。 这样的平台被我们称为 裸机平台 (bare-metal)。

【移除标准库依赖】

😃 安装交叉编译环境

#rustup是rust官方的版本管理工具。当你第一次安装工具链时,rustup只安装你的主机平台的标准库,也就是你目前运行的架构和操作系统[x86_64-unknown-linux-gnu]。要编译到其他平台,你必须安装其他目标平台。这可以通过rustup target add命令完成[riscv64gc-unknown-none-elf]。之后便可以通过--target 标志用 Cargo 构建。
$ rustup target add riscv64gc-unknown-none-elf

os 目录下新建 .cargo 目录,并在这个目录下创建 config 文件。在文件中添加如下所示内容使cargo 工具在 os 目录下默认会使用 riscv64gc-unknown-none-elf 作为目标平台

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

😃 修改错误:解除std依赖

main.rs的开头添加\#![no_std]表明不使用Rust标准库std使用核心库core,核心库 core 中的功能是 std 的子集无需依赖任何操作系统集成或堆分配即可支持。

在这里插入图片描述

$ cargo build
   Compiling os v0.1.0 (/home/kondl/OS_Training/os)
error: cannot find macro `println` in this scope
 --> src/main.rs:3:5
  |
3 |     println!("Hello, world!");
  |     ^^^^^^^

error: `#[panic_handler]` function required, but not found

error: could not compile `os` due to 2 previous errors

println! 宏是由标准库 std 提供的,且会使用到一个名为 write 的系统调用,所以报错,注释此行后编译。

$ cargo build
   Compiling os v0.1.0 (/home/kondl/OS_Training/os)
error: `#[panic_handler]` function required, but not found

error: could not compile `os` due to previous error

标准库 std 提供了 Rust 错误处理函数 #[panic_handler],其大致功能是打印出错位置和原因并杀死当前应用。 但核心库 core 并没有提供这项功能所以报错,在ossrc目录下新建一个子模块 lang_items.rs,在里面编写 panic 处理函数,通过标记 #[panic_handler] 告知编译器采用我们的实现:

😃 修改错误:增加panic处理

use core::panic::PanicInfo;

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

#[panic_handler]用于定义panic!#![no_std]程序中的行为。#[panic_handler]必须应用于签名为fn(&PanicInfo) -> !的函数,并且这样的函数仅能在一个二进制程序/动态链接库的整个依赖图中仅出现一次。

$ cargo build
   Compiling os v0.1.0 (/home/kondl/OS_Training/os)
error: requires `start` lang_item

error: could not compile `os` due to previous error

编译器提醒我们缺少一个名为 start 的语义项。 start 语义项代表了标准库 std 在执行应用程序之前需要进行的一些初始化工作。由于我们禁用了标准库,编译器也就找不到这项功能的实现了。

😃 修改错误:移除main函数

main.rs 的开头加入设置 #![no_main] 告诉编译器我们没有一般意义上的 main 函数, 并将原来的 main 函数删除。这样编译器也就不需要考虑初始化工作了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WLBzE92F-1661408083480)(C:\Users\11932\AppData\Roaming\Typora\typora-user-images\image-20220824180730649.png)]

$ cargo build
   Compiling os v0.1.0 (/home/kondl/OS_Training/os)
    Finished dev [unoptimized + debuginfo] target(s) in 0.27s

😃 查看编译后的文件

cargo build 后在 target/riscv64gc-unknown-none-elf/debug/ 目录下有可执行的二进制文件 os

$ tree os
os
├── Cargo.lock
├── Cargo.toml
├── src
│   ├── lang_items.rs
│   └── main.rs
#------------------------------------------------------------------------------------------------------------#
#查看os的文件格式
$ file target/riscv64gc-unknown-none-elf/debug/os
target/riscv64gc-unknown-none-elf/debug/os: ELF 64-bit LSB executable, UCB RISC-V, version 1 (SYSV), statically linked, with debug_info, not stripped
#------------------------------------------------------------------------------------------------------------#
#通过rust-readobj工具查看ELF头文件信息
	#ELF magic:用于将文件标识为ELF可执行文件
	#ELF Entry:给出了可执行文件的入口点为 0x0所以此处虽然这个二进制程序合法,但它是一个空程序
$ rust-readobj -h target/riscv64gc-unknown-none-elf/debug/os 
File: target/riscv64gc-unknown-none-elf/debug/os
Format: elf64-littleriscv
Arch: riscv64
AddressSize: 64bit
LoadName: <Not found>
ElfHeader {
  Ident {
    Magic: (7F 45 4C 46)
    Class: 64-bit (0x2)
    DataEncoding: LittleEndian (0x1)
    FileVersion: 1
    OS/ABI: SystemV (0x0)
    ABIVersion: 0
    Unused: (00 00 00 00 00 00 00)
  }
  Type: Executable (0x2)
  Machine: EM_RISCV (0xF3)
  Version: 1
  Entry: 0x0
  ProgramHeaderOffset: 0x40
  SectionHeaderOffset: 0x1B30
  Flags [ (0x5)
    EF_RISCV_FLOAT_ABI_DOUBLE (0x4)
    EF_RISCV_RVC (0x1)
  ]
  HeaderSize: 64
  ProgramHeaderEntrySize: 56
  ProgramHeaderCount: 3
  SectionHeaderEntrySize: 64
  SectionHeaderCount: 14
  StringTableSectionIndex: 12
}

【构建用户态执行环境】

😃 个人理解:用户态的执行环境主要是通过使用RISC-V架构的Linux系统调用和RISCV指令集来实现一些基本的目标

【构建用户态最小化执行环境】

😃 增加入口函数

给 Rust 编译器编译器提供入口函数 _start() , 在 main.rs 中添加如下内容:

#[no_mangle]
extern "C" fn _start() {
    loop{};
}

#[no_mangle]:表示生成的函数名经过编译后依然为_start,从而和c语言保持一致

extern “C” :该函数可以提供给其他库或者语言调用,并且采用c语言的调用约定。

$ cargo build
   Compiling os v0.1.0 (/home/kondl/OS_Training/os)
    Finished dev [unoptimized + debuginfo] target(s) in 0.20s
$ rust-objdump -S target/riscv64gc-unknown-none-elf/debug/os
target/riscv64gc-unknown-none-elf/debug/os:     file format elf64-littleriscv

Disassembly of section .text:

0000000000011120 <_start>:
;     loop{};
   11120: 09 a0         j       0x11122 <_start+0x2>
   11122: 01 a0         j       0x11122 <_start+0x2>

rust-objdump:可以反汇编ELF文件进行详细的信息查看

#注释掉loop循环后进行编译运行
$ cargo build
   Compiling os v0.1.0 (/home/kondl/OS_Training/os)
    Finished dev [unoptimized + debuginfo] target(s) in 0.20s
#反汇编后查看只有个ret
$ rust-objdump -S target/riscv64gc-unknown-none-elf/debug/os 
target/riscv64gc-unknown-none-elf/debug/os:     file format elf64-littleriscv

Disassembly of section .text:

0000000000011120 <_start>:
; }
   11120: 82 80         ret
#执行发生段错误
$ qemu-riscv64 target/riscv64gc-unknown-none-elf/debug/os
Segmentation fault (core dumped)

QEMU有两种运行模式:

User mode 模式,即用户态模拟,如 qemu-riscv64 程序, 能够模拟不同处理器的用户态指令的执行,并可以直接解析ELF可执行文件, 加载运行那些为不同处理器编译的用户级Linux应用程序。

System mode 模式,即系统态模式,如 qemu-system-riscv64 程序, 能够模拟一个完整的基于不同CPU的硬件系统,包括处理器、内存及其他外部设备,支持运行完整的操作系统。

目前的执行环境还缺了一个退出机制,我们需要操作系统提供的 exit 系统调用来退出程序。所以报错

😃 增加退出机制

// os/src/main.rs

const SYSCALL_EXIT: usize = 93;

fn syscall(id: usize, args: [usize; 3]) -> isize {
    let mut ret;
    unsafe {
        core::arch::asm!(
            "ecall",
            inlateout("x10") args[0] => ret,
            in("x11") args[1],
            in("x12") args[2],
            in("x17") id,
        );
    }
    ret
}

pub fn sys_exit(xstate: i32) -> isize {
    syscall(SYSCALL_EXIT, [xstate as usize, 0, 0])
}

#[no_mangle]
extern "C" fn _start() {
    sys_exit(9);
}
  • 第三行:系统调用号
  • 第五行:将所有的系统调用封装成syscall,它支持传入 syscall ID 和 3 个参数
  • 第八行开始:使用 Rust 提供的 asm! 宏在代码中内嵌汇编。 Rust 编译器无法判定汇编代码的安全性,所以我们需要将其包裹在 unsafe 块中。
    • x10~x17: 对应 a0~a7 x1 :对应 ra 所以是以寄存器 a0~a2 来保存系统调用的参数,以及寄存器 a7 保存 syscall ID, 返回值通过寄存器 a0 传递给局部变量 ret

系统调用的 ecall 指令会使用 a0 和 a7 寄存器,其中 a7 寄存器保存的是系统调用号,a0 寄存器保存的是系统调用参数,返回值会保存在 a0 寄存器中。为了能让系统调用指令能被集成进当前的流水线,ecall 指令只支持一个返回值和一个参数。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ww4rfB6S-1661408083481)(C:\Users\11932\AppData\Roaming\Typora\typora-user-images\image-20220825094526644.png)]

根据查找RISC-V System call table(RISC-V架构的Linux系统调用列表)。能找到93号ID为exit退出指令

$ cargo build --target riscv64gc-unknown-none-elf
   Compiling os v0.1.0 (/home/kondl/OS_Training/os)
    Finished dev [unoptimized + debuginfo] target(s) in 0.26s
$ qemu-riscv64 target/riscv64gc-unknown-none-elf/debug/os; echo $?
9
【有显示支持的用户态执行环境】

😃 增加sys_write系统调用

在main.rs中添加下列内容:

const SYSCALL_WRITE: usize = 64;
pub fn sys_write(fd: usize, buffer: &[u8]) -> isize {
    syscall(SYSCALL_WRITE, [fd, buffer.as_ptr() as usize, buffer.len()])
}
  • 增加系统调用号的ID,增加封装使用这个WRITE的函数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2LAWtlkZ-1661408083481)(C:\Users\11932\AppData\Roaming\Typora\typora-user-images\image-20220825094819525.png)]

😃 封装sys_write系统调用

创建 src/console.rs 文件,添加下列内容:

//  os/src/console.rs
use core::fmt::{Write, Arguments, Result};
use crate::sys_write;

struct Stdout;

impl Write for Stdout {
    fn write_str(&mut self, s: &str) -> Result {
        sys_write(1, s.as_bytes());
        Ok(())
    }
}

pub fn print(args: Arguments) {
    Stdout.write_fmt(args).unwrap();
}

macro_rules! print {
    ($fmt: literal $(, $($arg: tt)+)?) => {
        $crate::console::print(format_args!($fmt $(, $($arg)+)?));
    }
}

macro_rules! println {
    ($fmt: literal $(, $($arg: tt)+)?) => {
        $crate::console::print(format_args!(concat!($fmt, "\n") $(, $($arg)+)?));
    }
}
  • 第二行:引入core的部分包
  • 第三行:引入main.rs文件中的sys_write方法
  • 第十八行:构建 print 宏定义
  • 第十八行:构建 println 宏定义

😃 增加console.rs的功能调用

添加对 console 的引用

  #[macro_use]
  mod console;

😃 增加入口函数的打印

#[no_mangle]
extern "C" fn _start() {
    print!("Hello, ");
    println!("world!");
    sys_exit(9);
}
$ qemu-riscv64 target/riscv64gc-unknown-none-elf/debug/os;echo $?
Hello, world!
9

【构建裸机执行环境】

😃 个人理解:裸机的执行环境主要是通过使用SBI和RISCV指令集来实现一些基本的目标,与用户态相比是与OS站在同一个层面上的程序

【裸机启动过程】

QEMU 软件 qemu-system-riscv64 来模拟 RISC-V 64 计算机。加载内核程序的命令如下:

qemu-system-riscv64 \
            -machine virt \
            -nographic \
            -bios $(BOOTLOADER) \
            -device loader,file=$(KERNEL_BIN),addr=$(KERNEL_ENTRY_PA)
  • -machine virt:表明QEMU的启动平台为virt 平台,其全称是 QEMU RISC-V VirtIO Board,由 SiFive 公司设计,并且包含 16550a UART 和 VirtIO MMIO 作为外设和 I/O 接口。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CUfioeL1-1661408083482)(C:\Users\11932\AppData\Roaming\Typora\typora-user-images\image-20220825100638674.png)]

  • -nographic:禁用图形输出和重定向串行I/ o到控制台

  • -bios $(BOOTLOADER) :意味着硬件加载了一个 BootLoader 程序,即 RustSBI

  • -device loader,file=$(KERNEL_BIN),addr=$(KERNEL_ENTRY_PA) :表示硬件内存中的特定位置 $(KERNEL_ENTRY_PA) 放置了操作系统的二进制代码 $(KERNEL_BIN)$(KERNEL_ENTRY_PA) 的值是 0x80200000

执行上述命令等于给这台虚拟的 RISC-V64 计算机加电了,此时 CPU 的其它通用寄存器清零,而 PC 会指向 0x1000 的位置,这里有固化在硬件中的一小段引导代码, 它会很快跳转到 0x80000000 的 RustSBI 处。 RustSBI完成硬件初始化后,会跳转到 $(KERNEL_BIN) 所在内存位置 0x80200000 处, 执行操作系统的第一条指令。

【RustSBI 是什么?】

SBI 是 RISC-V 的一种底层规范,RustSBI 是它的一种实现。 操作系统内核与 RustSBI 的关系有点像应用与操作系统内核的关系,后者向前者提供一定的服务。只是SBI提供的服务很少, 比如关机,显示字符串等。

操作系统往往不一定是硬件上的第一层软件。

RustSBI是RISC-V平台下的引导程序实现,它完全由Rust编写,并已经被录入RISC-V SBI国际标准。

在这里插入图片描述

【实现关机功能】

😃 通过使用RustSBI来实现SHUTDOWN功能

// bootloader/rustsbi-qemu.bin 直接添加的SBI规范实现的二进制代码,给操作系统提供基本支持服务
#![allow(unused)]

const SBI_SET_TIMER: usize = 0;
const SBI_CONSOLE_PUTCHAR: usize = 1;
const SBI_CONSOLE_GETCHAR: usize = 2;
const SBI_SHUTDOWN: usize = 8;

#[inline(always)]
fn sbi_call(which: usize, arg0: usize, arg1: usize, arg2: usize) -> usize {
    let mut ret;
    unsafe {
        core::arch::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 console_getchar() -> usize {
    sbi_call(SBI_CONSOLE_GETCHAR, 0, 0, 0)
}

pub fn shutdown() -> ! {
    sbi_call(SBI_SHUTDOWN, 0, 0, 0);
    panic!("It should shutdown!");
}

应用程序访问操作系统提供的系统调用的指令是 ecall ,操作系统访问 RustSBI提供的SBI调用的指令也是 ecall , 虽然指令一样,但它们所在的特权级是不一样的。 简单地说,应用程序位于最弱的用户特权级(User Mode), 操作系统位于内核特权级(Supervisor Mode), RustSBI位于机器特权级(Machine Mode)。

# 编译生成ELF格式的执行文件
	# cargo build --release:生成的target文件夹下不再有debug目录,替代的是release目录。相比debug版本,release 版本可执行文件的大小好像并未变化多大。
$ cargo build --release
 Compiling os v0.1.0 (/media/chyyuu/ca8c7ba6-51b7-41fc-8430-e29e31e5328f/thecode/rust/os_kernel_lab/os)
  Finished release [optimized] target(s) in 0.15s
#------------------------------------------------------------------------------------------------------------#
# 把ELF执行文件转成bianary文件
	# rust-objcopy:当前的ELF执行程序有许多与执行无直接关系的信息(如调试信息等),可以通过 rust-objcopy 工具来清除。
	# 把ELF执行文件转成bianary文件主要是利用 rust-objcopy 工具删除掉 ELF 文件中的所有 header 只保留各个段的实际数据得到一个没有任何符号的纯二进制镜像文件,例如下所示:
	#rust-objcopy --strip-all target/debug/os -O binary target/debug/os.bin
	#这样就生成了一个没有任何符号的纯二进制镜像文件。由于缺少了必要的元数据,我们的 file 工具也没有办法对它完成解析了。而后,我们可直接将这个二进制镜像文件手动载入到内存中合适位置即可。
$ rust-objcopy --binary-architecture=riscv64 target/riscv64gc-unknown-none-elf/release/os --strip-all -O binary target/riscv64gc-unknown-none-elf/release/os.bin
#------------------------------------------------------------------------------------------------------------#
# 加载运行
$ qemu-system-riscv64 
	-machine virt \
	-nographic \
	-bios ../bootloader/rustsbi-qemu.bin \
	-device loader,file=target/riscv64gc-unknown-none-elf/release/os.bin,addr=0x80200000
[rustsbi] RustSBI version 0.2.2, adapting to RISC-V SBI v1.0.0
.______       __    __      _______.___________.  _______..______   __
|   _  \     |  |  |  |    /       |           | /       ||   _  \ |  |
|  |_)  |    |  |  |  |   |   (----`---|  |----`|   (----`|  |_)  ||  |
|      /     |  |  |  |    \   \       |  |      \   \    |   _  < |  |
|  |\  \----.|  `--'  |.----)   |      |  |  .----)   |   |  |_)  ||  |
| _| `._____| \______/ |_______/       |__|  |_______/    |______/ |__|
[rustsbi] Implementation     : RustSBI-QEMU Version 0.1.1
[rustsbi] Platform Name      : riscv-virtio,qemu
[rustsbi] Platform SMP       : 1
[rustsbi] Platform Memory    : 0x80000000..0x88000000
[rustsbi] Boot HART          : 0
[rustsbi] Device Tree Region : 0x87000000..0x87000ef2
[rustsbi] Firmware Address   : 0x80000000
[rustsbi] Supervisor Address : 0x80200000
[rustsbi] pmp01: 0x00000000..0x80000000 (-wr)
[rustsbi] pmp02: 0x80000000..0x80200000 (---)
[rustsbi] pmp03: 0x80200000..0x88000000 (xwr)
后面就卡住了...:(
#------------------------------------------------------------------------------------------------------------#
# 通过rust-readobj工具查看ELF头文件信息
	#其Entry: 0x1122C不是约定好的0x80200000
$ rust-readobj -h target/riscv64gc-unknown-none-elf/release/os

File: target/riscv64gc-unknown-none-elf/release/os
Format: elf64-littleriscv
Arch: riscv64
AddressSize: 64bit
LoadName: <Not found>
ElfHeader {
  Ident {
    Magic: (7F 45 4C 46)
    Class: 64-bit (0x2)
    DataEncoding: LittleEndian (0x1)
    FileVersion: 1
    OS/ABI: SystemV (0x0)
    ABIVersion: 0
    Unused: (00 00 00 00 00 00 00)
  }
  Type: Executable (0x2)
  Machine: EM_RISCV (0xF3)
  Version: 1
  Entry: 0x1122C
  ProgramHeaderOffset: 0x40
  SectionHeaderOffset: 0xD5670
  Flags [ (0x5)
    EF_RISCV_FLOAT_ABI_DOUBLE (0x4)
    EF_RISCV_RVC (0x1)
  ]
  HeaderSize: 64
  ProgramHeaderEntrySize: 56
  ProgramHeaderCount: 5
  SectionHeaderEntrySize: 64
  SectionHeaderCount: 18
  StringTableSectionIndex: 16
}

😃 修改程序的内存布局并设置好栈空间,使得Entry为我们所约定好的0x80200000

修改 Cargo 的配置文件来使用我们自己的链接脚本 os/src/linker.ld

[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_ADDRESS0x80200000 ,RustSBI 期望的 OS 起始地址;
  • 第 5 行:SECTIONS命令告诉链接器如何将输入部分映射到输出部分,以及如何将输出部分放置在内存中。
    • 每个段都有两个全局变量给出其起始和结束地址(比如 .text 段的开始和结束地址分别是 stextetext ),中间分配大小。

【内存布局】

在这里插入图片描述

  • 栈 (stack)向低地址增长
  • 堆 (heap)向高地址增长
  • .bss段:程序中未初始化的全局变量的一块内存 int a ;
  • .data段:程序中初始化的全局变量的一块内存 int a = 0;
  • 已初始化数据段.rodata:只读的全局数据(常数或者是常量字符串)、.data:可修改的全局数据。
  • text: 代码段,存放执行代码的区域,大小确定,通常为只读。

通过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:
  • 第1行.section:与上面链接脚本进行对应,触发它的设置。把代码划分到不同的段。

    在这里插入图片描述

  • 第2行.globl:进行一个全局的声明_start

    在这里插入图片描述

  • 第4行la:建立栈空间,sp指针为栈指针

    在这里插入图片描述

  • 第7行.section .bss.stack:在链接脚本 linker.ld 中 .bss.stack 段最终会被汇集到 .bss 段中

  • 第9行boot_stack:栈空间的低地址

  • 第10行.space 4096 * 16:建立了一个64K的栈地址

  • 第12行boot_stack_top:栈空间的高地址

😃 在main.rs中嵌入这些汇编代码并声明应用入口 rust_main

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

#[no_mangle]
pub fn rust_main() -> ! {
    shutdown();
}
  • 第 1 行:我们使用 global_asm 宏,将同目录下的汇编文件 entry.asm 嵌入到代码中。
  • 第 3 行:声明了应用的入口点 rust_main ,需要注意的是,这里通过宏将 rust_main 标记为 #[no_mangle] 以避免编译器对它的名字进行混淆,不然在链接时, entry.asm 将找不到 main.rs 提供的外部符号 rust_main,导致链接失败
$ cargo build --release
   Compiling os v0.1.0 (/home/kondl/OS_Training/os)
warning: unused import: `crate::sbi::shutdown`
 --> src/lang_items.rs:1:5
  |
1 | use crate::sbi::shutdown;
  |     ^^^^^^^^^^^^^^^^^^^^
  |
  = note: `#[warn(unused_imports)]` on by default

warning: `os` (bin "os") generated 1 warning
    Finished release [optimized] target(s) in 0.38s
#------------------------------------------------------------------------------------------------------------#
$ rust-objcopy --binary-architecture=riscv64 target/riscv64gc-unknown-none-elf/release/os --strip-all -O binary target/riscv64gc-unknown-none-elf/release/os.bin
#------------------------------------------------------------------------------------------------------------#
# 执行程序发现成功退出了 :)
$ qemu-system-riscv64 -machine virt -nographic -bios ../bootloader/rustsbi-qemu.bin -device loader,file=target/riscv64gc-unknown-none-elf/release/os.bin,addr=0x80200000
[rustsbi] RustSBI version 0.2.2, adapting to RISC-V SBI v1.0.0
.______       __    __      _______.___________.  _______..______   __
|   _  \     |  |  |  |    /       |           | /       ||   _  \ |  |
|  |_)  |    |  |  |  |   |   (----`---|  |----`|   (----`|  |_)  ||  |
|      /     |  |  |  |    \   \       |  |      \   \    |   _  < |  |
|  |\  \----.|  `--'  |.----)   |      |  |  .----)   |   |  |_)  ||  |
| _| `._____| \______/ |_______/       |__|  |_______/    |______/ |__|
[rustsbi] Implementation     : RustSBI-QEMU Version 0.1.1
[rustsbi] Platform Name      : riscv-virtio,qemu
[rustsbi] Platform SMP       : 1
[rustsbi] Platform Memory    : 0x80000000..0x88000000
[rustsbi] Boot HART          : 0
[rustsbi] Device Tree Region : 0x87000000..0x87000ef2
[rustsbi] Firmware Address   : 0x80000000
[rustsbi] Supervisor Address : 0x80200000
[rustsbi] pmp01: 0x00000000..0x80000000 (-wr)
[rustsbi] pmp02: 0x80000000..0x80200000 (---)
[rustsbi] pmp03: 0x80200000..0x88000000 (xwr)
#------------------------------------------------------------------------------------------------------------#
# 查看发现Entry: 0x80200000
$ rust-readobj -h target/riscv64gc-unknown-none-elf/release/os

File: target/riscv64gc-unknown-none-elf/release/os
Format: elf64-littleriscv
Arch: riscv64
AddressSize: 64bit
LoadName: <Not found>
ElfHeader {
  Ident {
    Magic: (7F 45 4C 46)
    Class: 64-bit (0x2)
    DataEncoding: LittleEndian (0x1)
    FileVersion: 1
    OS/ABI: SystemV (0x0)
    ABIVersion: 0
    Unused: (00 00 00 00 00 00 00)
  }
  Type: Executable (0x2)
  Machine: EM_RISCV (0xF3)
  Version: 1
  Entry: 0x80200000
  ProgramHeaderOffset: 0x40
  SectionHeaderOffset: 0xD8598
  Flags [ (0x5)
    EF_RISCV_FLOAT_ABI_DOUBLE (0x4)
    EF_RISCV_RVC (0x1)
  ]
  HeaderSize: 64
  ProgramHeaderEntrySize: 56
  ProgramHeaderCount: 4
  SectionHeaderEntrySize: 64
  SectionHeaderCount: 18
  StringTableSectionIndex: 16
}
【清空 .bss 段】

通过链接脚本 linker.ld 中给出的全局符号 sbssebss 让我们能轻松确定 .bss 段的位置进行清空。

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) }
   });
}
【添加裸机打印相关函数】

在内部将原来的sys_write(1, s.as_bytes()); 替换成console_putchar(c as usize); 内部使用ecallconst SBI_CONSOLE_PUTCHAR: usize = 1;,使用RUSTSBI替换了syscall

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

将内部的信息进行了输出

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

在这里插入图片描述

$ cargo build --release
   Compiling os v0.1.0 (/home/kondl/OS_Training/os)
    Finished release [optimized] target(s) in 0.26s
#------------------------------------------------------------------------------------------------------------#
$ rust-objcopy --binary-architecture=riscv64 target/riscv64gc-unknown-none-elf/release/os --strip-all -O binary target/riscv64gc-unknown-none-elf/release/os.bin
#------------------------------------------------------------------------------------------------------------#
$ qemu-system-riscv64 -machine virt -nographic -bios ../bootloader/rustsbi-qemu.bin -device loader,file=target/riscv64gc-unknown-none-elf/release/os.bin,addr=0x80200000
[rustsbi] RustSBI version 0.2.2, adapting to RISC-V SBI v1.0.0
.______       __    __      _______.___________.  _______..______   __
|   _  \     |  |  |  |    /       |           | /       ||   _  \ |  |
|  |_)  |    |  |  |  |   |   (----`---|  |----`|   (----`|  |_)  ||  |
|      /     |  |  |  |    \   \       |  |      \   \    |   _  < |  |
|  |\  \----.|  `--'  |.----)   |      |  |  .----)   |   |  |_)  ||  |
| _| `._____| \______/ |_______/       |__|  |_______/    |______/ |__|
[rustsbi] Implementation     : RustSBI-QEMU Version 0.1.1
[rustsbi] Platform Name      : riscv-virtio,qemu
[rustsbi] Platform SMP       : 1
[rustsbi] Platform Memory    : 0x80000000..0x88000000
[rustsbi] Boot HART          : 0
[rustsbi] Device Tree Region : 0x87000000..0x87000ef2
[rustsbi] Firmware Address   : 0x80000000
[rustsbi] Supervisor Address : 0x80200000
[rustsbi] pmp01: 0x00000000..0x80000000 (-wr)
[rustsbi] pmp02: 0x80000000..0x80200000 (---)
[rustsbi] pmp03: 0x80200000..0x88000000 (xwr)
Hello, world!
Panicked at src/main.rs:128 Shutdown machine!

😃 个人总结:

  1. Trilobita OS是一个与应用程序不区分的最简单的OS,OS并不是最底层的软件,此OS依赖于uboot的SBI和RISCV的基础指令
  2. OS的整体启动流程简化来说可以认为是:bootloader 到 OS,二者通过地址进行跳转
  3. 目前写一个最简单的OS注意的事情:设置内存布局,配置入口地址,配置链接文件
  4. 通过此文档应该能实现最基本的Trilobita OS的实现
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值