需要实现什么?
裸机运行rust程序,即在没有std标准库与系统调用的情况下实现println!宏。
拥有什么?
core核心库, QEMU 软件 qemu-system-riscv64 模拟 RISC-V 64 计算机,RustSBI。
思路
qemu逻辑运行代码。
qemu-system-riscv64 \
-machine virt \
-nographic \
-bios $(BOOTLOADER) \
-device loader,file=$(KERNEL_BIN),addr=$(KERNEL_ENTRY_PA)
我们需要实现os应用程序KERNEL_BIN和应用入口地址KERNEL_ENTRY_PA两项。
编写os程序入口函数->将os程序的入口地址链接到addr->使用sbi调用实现print
实现过程
跳过为qemu-riscv64实现应用程序,以下是qemu-system-riscv64的实现过程。
QEMU有两种运行模式:
User mode 模式,即用户态模拟,如 qemu-riscv64 程序, 能够模拟不同处理器的用户态指令的执行,并可以直接解析ELF可执行文件, 加载运行那些为不同处理器编译的用户级Linux应用程序。
System mode 模式,即系统态模式,如 qemu-system-riscv64 程序, 能够模拟一个完整的基于不同CPU的硬件系统,包括处理器、内存及其他外部设备,支持运行完整的操作系统。
提供入口函数
简易的入口函数
// os/src/main.rs
#[no_mangle]
extern "C" fn _start() {
loop{};
}
extern “C” fn _start() 定义一个程序的入口点。由于没有提供 main 函数的调用,此处定义一个 _start 函数,它会被链接器识别为程序的入口点。如果没有入口,程序会被编译为空程序。
extern “C” 修饰符告诉Rust编译器,这个函数的链接约定应该与C语言相同。因为C语言的链接约定在许多平台上都是已知的,并且可以直接被链接器和引导加载器理解。
最终入口函数
global_asm!(include_str!("entry.asm"));
/// the rust entry-point of os
#[no_mangle]
pub fn rust_main() -> ! {
extern "C" {
fn stext(); // begin addr of text segment
fn etext(); // end addr of text segment
fn srodata(); // start addr of Read-Only data segment
fn erodata(); // end addr of Read-Only data ssegment
fn sdata(); // start addr of data segment
fn edata(); // end addr of data segment
fn sbss(); // start addr of BSS segment
fn ebss(); // end addr of BSS segment
fn boot_stack_lower_bound(); // stack lower bound
fn boot_stack_top(); // stack top
}
clear_bss();
...
use crate::board::QEMUExit;
crate::board::QEMU_EXIT_HANDLE.exit_success(); // CI autotest success
//crate::board::QEMU_EXIT_HANDLE.exit_failure(); // CI autoest failed
}
/// clear BSS segment
pub 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) });
}
global_asm!(include_str!(“entry.asm”));内联汇编代码。入口函数rust_main,#[no_mangle]让rust编译器在编译过程中不会修改函数的名称。extern “C” 修饰符告诉Rust编译器,这个函数的链接约定应该与C语言相同。clear_bss清零 .bss 段,链接脚本 linker.ld 中给出的全局符号 sbss 和 ebss 让我们能轻松确定 .bss 段的位置。
# entry.asm
.section .text.entry
.globl _start
_start:
la sp, boot_stack_top
call rust_main
.section .bss.stack
.globl boot_stack_lower_bound
boot_stack_lower_bound:
.space 4096 * 16
.globl boot_stack_top
boot_stack_top:
asm定义程序入口_start,地址将依据链接脚本被放在 BASE_ADDRESS (0x80200000)处。,之后创建堆栈,调用rust_main函数
实现sbi调用
ecall调用从系统态进入sbi态。
#[inline(always)]
fn sbi_call(which: usize, arg0: usize, arg1: usize, arg2: usize) -> usize {
let mut ret;
unsafe {
// asm! 宏允许我们在 Rust 代码中嵌入汇编代码。
asm!(
"li x16, 0",
"ecall",
inlateout("x10") arg0 => ret,
in("x11") arg1,
in("x12") arg2,
in("x17") which,
);
}
ret
}
来看看这段的汇编代码
函数开头:
000000000001116c <_ZN2os7syscall17h2a6fcb2e627ab196E>:
这是函数 _ZN2os7syscall17h2a6fcb2e627ab196E 的地址标签。这个标签是编译器生成的,通常包含了一些关于函数的信息(如命名空间、函数名、哈希值等)。
函数体
1116c: 41 11 addi sp, sp, -0x10
这条指令将栈指针(sp)减少 16 字节(0x10),为局部变量和可能的临时数据腾出空间。
1116e: 2e 86 mv a2, a1
将参数寄存器 a1(在 RISC-V 中通常用于传递函数的第二个参数)的值复制到 a2 寄存器。这里 a2 稍后用于索引参数数组。
11170: aa 88 mv a7, a0
将参数寄存器 a0(在 RISC-V 中通常用于传递函数的第一个参数)的值复制到 a7 寄存器。这里 a7 用来保存系统调用编号 id。
11172: 46 e4 sd a7, 0x8(sp)
将 a7 寄存器(包含系统调用编号 id)的值存储到栈上,偏移量为 8 字节(0x8(sp))。这不是必需的,但在某些情况下,为了保持栈帧的一致性或便于调试,编译器可能会选择这样做。
11174: 08 62 ld a0, 0x0(a2)
从 a2 寄存器指向的地址(即参数数组的第一个元素)加载 8 字节数据到 a0 寄存器。这对应于 Rust 代码中的 args[0]。
11176: 0c 66 ld a1, 0x8(a2)
从 a2 寄存器指向的地址加上 8 字节(即参数数组的第二个元素)加载 8 字节数据到 a1 寄存器。这对应于 Rust 代码中的 args[1]。
11178: 10 6a ld a2, 0x10(a2)
从 a2 寄存器指向的地址加上 16 字节(即参数数组的第三个元素)加载 8 字节数据到 a2 寄存器。这对应于 Rust 代码中的 args[2]。
1117a: 73 00 00 00 ecall
执行 ecall 指令,触发系统调用。此时,a0、a1、a2 寄存器分别包含 args[0]、args[1]、args[2] 的值,而系统调用编号 id 应该在某个地方(可能是之前存储在栈上的 a7,但在这个例子中它没有被直接使用)。
ecall 指令将权限提升到内核模式并将程序跳转到指定的地址。在这个例子中,这个地址是内核的系统调用处理程序。
应用程序访问操作系统提供的系统调用的指令是 ecall ,操作系统访问 RustSBI提供的SBI调用的指令也是 ecall , 虽然指令一样,但它们所在的特权级是不一样的。 简单地说,应用程序位于最弱的用户特权级(User Mode), 操作系统位于内核特权级(Supervisor Mode), RustSBI位于机器特权级(Machine Mode)。
1117e: 2a e0 sd a0, 0x0(sp)
将 a0 寄存器的值(即系统调用的返回值)存储到栈上的某个位置。这通常是为了在后续指令中能够访问这个返回值。
11180: 02 65 ld a0, 0x0(sp)
从栈上的位置加载 8 字节数据到 a0 寄存器。这实际上是将之前存储的系统调用返回值重新加载回 a0 寄存器,因为 RISC-V 架构通常约定使用 a0 寄存器来返回函数的结果。
函数结尾
11182: 41 01 addi sp, sp, 0x10
将栈指针(sp)增加 16 字节,释放之前为局部变量和临时数据分配的空间。
11184: 82 80 ret
返回
实现退出机制
直接复制了,不重要
//ref:: https://github.com/andre-richter/qemu-exit
use core::arch::asm;
const EXIT_SUCCESS: u32 = 0x5555; // Equals `exit(0)`. qemu successful exit
const EXIT_FAILURE_FLAG: u32 = 0x3333;
const EXIT_FAILURE: u32 = exit_code_encode(1); // Equals `exit(1)`. qemu failed exit
const EXIT_RESET: u32 = 0x7777; // qemu reset
pub trait QEMUExit {
/// Exit with specified return code.
///
/// Note: For `X86`, code is binary-OR'ed with `0x1` inside QEMU.
fn exit(&self, code: u32) -> !;
/// Exit QEMU using `EXIT_SUCCESS`, aka `0`, if possible.
///
/// Note: Not possible for `X86`.
fn exit_success(&self) -> !;
/// Exit QEMU using `EXIT_FAILURE`, aka `1`.
fn exit_failure(&self) -> !;
}
/// RISCV64 configuration
pub struct RISCV64 {
/// Address of the sifive_test mapped device.
addr: u64,
}
/// Encode the exit code using EXIT_FAILURE_FLAG.
const fn exit_code_encode(code: u32) -> u32 {
(code << 16) | EXIT_FAILURE_FLAG
}
impl RISCV64 {
/// Create an instance.
pub const fn new(addr: u64) -> Self {
RISCV64 { addr }
}
}
impl QEMUExit for RISCV64 {
/// Exit qemu with specified exit code.
fn exit(&self, code: u32) -> ! {
// If code is not a special value, we need to encode it with EXIT_FAILURE_FLAG.
let code_new = match code {
EXIT_SUCCESS | EXIT_FAILURE | EXIT_RESET => code,
_ => exit_code_encode(code),
};
unsafe {
asm!(
"sw {0}, 0({1})",
in(reg)code_new, in(reg)self.addr
);
// For the case that the QEMU exit attempt did not work, transition into an infinite
// loop. Calling `panic!()` here is unfeasible, since there is a good chance
// this function here is the last expression in the `panic!()` handler
// itself. This prevents a possible infinite loop.
loop {
asm!("wfi", options(nomem, nostack));
}
}
}
fn exit_success(&self) -> ! {
self.exit(EXIT_SUCCESS);
}
fn exit_failure(&self) -> ! {
self.exit(EXIT_FAILURE);
}
}
const VIRT_TEST: u64 = 0x100000;
pub const QEMU_EXIT_HANDLE: RISCV64 = RISCV64::new(VIRT_TEST);
const SYSCALL_EXIT: usize = 93;
pub fn sys_exit(xstate: i32) -> isize {
syscall(SYSCALL_EXIT, [xstate as usize, 0, 0])
}
#[no_mangle]
extern "C" fn _start() {
sys_exit(9);
}
编译执行后可以打印程序的返回值(只是说明返回值的作用,这里暂时还不能运行,按照官方文档的顺序来写可以实现)
$ cargo build --target riscv64gc-unknown-none-elf
Compiling os v0.1.0 (/media/chyyuu/ca8c7ba6-51b7-41fc-8430-e29e31e5328f/thecode/rust/os_kernel_lab/os)
Finished dev [unoptimized + debuginfo] target(s) in 0.26s
[打印程序的返回值]
$ qemu-riscv64 target/riscv64gc-unknown-none-elf/debug/os; echo $?
9
提供语义项 panic_handler
简易的 panic_handler
panic_handler是一个特殊的函数,它用于处理panic(程序中的严重错误,如不可恢复的逻辑错误或资源不足),rust程序中必须实现此函数。返回类型 ! ,使用loop无限循环永不返回。
// os/src/lang_items.rs
use core::panic::PanicInfo;
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
加入shutdown关机
//! The panic handler
use crate::sbi::shutdown;
use core::panic::PanicInfo;
#[panic_handler]
/// panic handler
fn panic(info: &PanicInfo) -> ! {
if let Some(location) = info.location() {
println!(
"[kernel] Panicked at {}:{} {}",
location.file(),
location.line(),
info.message().unwrap()
);
} else {
println!("[kernel] Panicked: {}", info.message().unwrap());
}
shutdown()
}
实现println宏
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()])
}
struct Stdout;
impl Write for Stdout {
fn write_str(&mut self, s: &str) -> fmt::Result {
sys_write(1, s.as_bytes());
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)+)?) => {
print(format_args!(concat!($fmt, "\n") $(, $($arg)+)?));
}
}
修改程序入口地址并运行
qemu-system-riscv64 \
-machine virt \
-nographic \
-bios $(BOOTLOADER) \
-device loader,file=$(KERNEL_BIN),addr=$(KERNEL_ENTRY_PA)
BOOTLOADER := ../bootloader/$(SBI)-$(BOARD).bin
KERNEL_ENTRY_PA := 0x80200000
KERNEL_BIN := target/riscv64gc-unknown-none-elf/release/os.bin
当我们执行包含上述启动参数的 qemu-system-riscv64 软件,就意味给这台虚拟的 RISC-V64 计算机加电了。 此时,CPU 的其它通用寄存器清零,而 PC 会指向 0x1000 的位置,这里有固化在硬件中的一小段引导代码, 它会很快跳转到 0x80000000 的 RustSBI 处。 RustSBI完成硬件初始化后,会跳转到 $(KERNEL_BIN) 所在内存位置 0x80200000 处, 执行操作系统的第一条指令。
通过修改linker.ld文件将os程序的入口地址也设置为0x80200000。
// 修改 Cargo 的配置文件来使用我们自己的链接脚本 os/src/linker.ld
// 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/link.ld
OUTPUT_ARCH(riscv)
ENTRY(_start)
BASE_ADDRESS = 0x80200000;
...