个人理解:写裸机上的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 并没有提供这项功能所以报错,在os
的src
目录下新建一个子模块 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_ADDRESS
为0x80200000
,RustSBI 期望的 OS 起始地址; - 第 5 行:SECTIONS命令告诉链接器如何将输入部分映射到输出部分,以及如何将输出部分放置在内存中。
- 每个段都有两个全局变量给出其起始和结束地址(比如
.text
段的开始和结束地址分别是stext
和etext
),中间分配大小。
- 每个段都有两个全局变量给出其起始和结束地址(比如
【内存布局】
- 栈 (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
中给出的全局符号 sbss
和 ebss
让我们能轻松确定 .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);
内部使用ecall
的const 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!
😃 个人总结:
- Trilobita OS是一个与应用程序不区分的最简单的OS,OS并不是最底层的软件,此OS依赖于uboot的SBI和RISCV的基础指令
- OS的整体启动流程简化来说可以认为是:bootloader 到 OS,二者通过地址进行跳转
- 目前写一个最简单的OS注意的事情:设置内存布局,配置入口地址,配置链接文件
- 通过此文档应该能实现最基本的Trilobita OS的实现