新版 QEMU 中内置了 OpenSBI 固件,它主要负责在操作系统运行前的硬件初始化和加载操作系统的功能。我们使用以下命令尝试运行一下:
{% label %}运行输出{% endlabel %}
$ qemu-system-riscv64 \
--machine virt \
--nographic \
--bios default
OpenSBI v0.6
____ _____ ____ _____
/ __ \ / ____| _ \_ _|
| | | |_ __ ___ _ __ | (___ | |_) || |
| | | | '_ \ / _ \ '_ \ \___ \| _ < | |
| |__| | |_) | __/ | | |____) | |_) || |_
\____/| .__/ \___|_| |_|_____/|____/_____|
| |
|_|
Platform Name : QEMU Virt Machine
Platform HART Features : RV64ACDFIMSU
Platform Max HARTs : 8
Current Hart : 0
Firmware Base : 0x80000000
Firmware Size : 120 KB
Runtime SBI Version : 0.2
MIDELEG : 0x0000000000000222
MEDELEG : 0x000000000000b109
PMP0 : 0x0000000080000000-0x000000008001ffff (A)
PMP1 : 0x0000000000000000-0xffffffffffffffff (A,R,W,X)
可以看到我们已经在 qemu-system-riscv64 模拟的 QEMU Virt Machine 硬件上将 OpenSBI 这个固件跑起来了。QEMU 可以使用 ctrl+a
(macOS 为 control+a
) 再按下 x
键退出。
为了确信我们已经跑起来了内核里面的代码,我们最好在 rust_main
里面加上简单的输出:
{% label %}os/src/main.rs{% endlabel %}
//! # 全局属性
//! - `#![no_std]`
//! 禁用标准库
#![no_std]
//!
//! - `#![no_main]`
//! 不使用 `main` 函数等全部 Rust-level 入口点来作为程序入口
#![no_main]
//! # 一些 unstable 的功能需要在 crate 层级声明后才可以使用
//! - `#![feature(llvm_asm)]`
//! 内嵌汇编
#![feature(llvm_asm)]
//!
//! - `#![feature(global_asm)]`
//! 内嵌整个汇编文件
#![feature(global_asm)]
// 汇编编写的程序入口,具体见该文件
global_asm!(include_str!("entry.asm"));
use core::panic::PanicInfo;
/// 当 panic 发生时会调用该函数
/// 我们暂时将它的实现为一个死循环
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
/// 在屏幕上输出一个字符,目前我们先不用了解其实现原理
pub fn console_putchar(ch: u8) {
let _ret: usize;
let arg0: usize = ch as usize;
let arg1: usize = 0;
let arg2: usize = 0;
let which: usize = 1;
unsafe {
llvm_asm!("ecall"
: "={x10}" (_ret)
: "{x10}" (arg0), "{x11}" (arg1), "{x12}" (arg2), "{x17}" (which)
: "memory"
: "volatile"
);
}
}
/// Rust 的入口函数
///
/// 在 `_start` 为我们进行了一系列准备之后,这是第一个被调用的 Rust 函数
#[no_mangle]
pub extern "C" fn rust_main() -> ! {
// 在屏幕上输出 "OK\n" ,随后进入死循环
console_putchar(b'O');
console_putchar(b'K');
console_putchar(b'\n');
loop {}
}
这样,如果我们将内核镜像加载完成后,屏幕上出现了 OK ,就说明我们之前做的事情没有问题。
现在我们生成内核镜像要通过多条命令来完成,我们可以通过在 os 目录下建立一个 Makefile 来简化这一过程:
{% label %}os/Makefile{% endlabel %}
TARGET := riscv64imac-unknown-none-elf
MODE := debug
KERNEL_FILE := target/$(TARGET)/$(MODE)/os
BIN_FILE := target/$(TARGET)/$(MODE)/kernel.bin
OBJDUMP := rust-objdump --arch-name=riscv64
OBJCOPY := rust-objcopy --binary-architecture=riscv64
.PHONY: doc kernel build clean qemu run
# 默认 build 为输出二进制文件
build: $(BIN_FILE)
# 通过 Rust 文件中的注释生成 os 的文档
doc:
@cargo doc --document-private-items
# 编译 kernel
kernel:
@cargo build
# 生成 kernel 的二进制文件
$(BIN_FILE): kernel
@$(OBJCOPY) $(KERNEL_FILE) --strip-all -O binary $@
# 查看反汇编结果
asm:
@$(OBJDUMP) -d $(KERNEL_FILE) | less
# 清理编译出的文件
clean:
@cargo clean
# 运行 QEMU
qemu: build
@qemu-system-riscv64 \
-machine virt \
-nographic \
-bios default \
-device loader,file=$(BIN_FILE),addr=0x80200000
# 一键运行
run: build qemu
这里我们通过参数 -device
来将内核镜像加载到 QEMU 中,我们指定了内核镜像文件,并告诉 OpenSBI 最后跳转到 0x80200000 这个入口地址。
最后,我们可以使用 make run
来用 Qemu 加载内核镜像并运行。匆匆翻过一串长长的 OpenSBI 输出,我们看到了 OK!于是历经了千辛万苦我们终于将我们的内核跑起来了!
下一节我们实现格式化输出来使得我们后续能够更加方便的通过输出来进行内核调试。