文章目录
- 前言
- 操作系统执行环境
- 创建裸机平台项目
- Rust的Core库
- 移除标准库依赖
- Qemu 启动流程
- 内存布局
- 编译流程
- 内核的初始指令
- 调整内核的内存布局
- 手动加载内核可执行文件
- 使用RustSBI提供的服务
- 添加bootloader模块
- 添加Makefile
- 运行
- 停止
- 总体架构
前言
我们既然是手写操作系统,那么就不能再继续依赖底层操作系统了,就需要我们和硬件去对线
操作系统执行环境
创建裸机平台项目
- 新建项目
cargo new os--bin
- 简单运行一下程序,输出hello world
cargo run
- 将目标平台切换到risc-v裸机平台
rustup target add riscv64gc-unknown-none-elf
# os/.cargo/config
[build]
target = "riscv64gc-unknown-none-elf"
- 再次运行
cargo run
# 编译报错,没有发现rust的std标准库
Rust的Core库
我们rust的std库在risc-v裸机平台不能使用了,不过rust提供了一个相对受限的core库来支持在这种裸机平台上运行,这样就可以继续和计算机硬件对线了,我们可以借助它来完成我们的裸机应用程序
移除标准库依赖
- 增加错误处理
os/src/lang_items.rs
use core::panic::PanicInfo;
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop{}
}
- 移除main函数
os/src/main.rs
#![no_main]
#![no_std]
mod lang_items;
- 构建测试
cargo build
此时不出意外的话,可以成功编译
Qemu 启动流程
- 第一阶段
加电后pc寄存器会存放rom(只读存储器)的物理地址,然后cpu会运行rom内部的固件代码来初始化cpu,并加载bootloader,然后控制权切换到bootloader - 第二阶段
bootloader继续初始化cpu,然后加载操作系统镜像,之后控制权切换到操作系统 - 第三阶段
操作系统开始运转
内存布局
经典程序内存布局
- stack
存放函数调用上下文,函数作用域内的局部变量,向低地址增长 - heap
存放程序运行时动态分配的数据,向高地址增长 - .bss
存放程序中未初始化的全局数据,通常由程序的加载者代为进行零初始化,即将这块区域逐字节清零 - .data
存放已初始化数据段保存程序中那些已初始化的且可修改的全局数据 - .rodata
存放已初始化数据段保存程序中那些已初始化的且但不可修改的全局常量数据 - .text
存放程序的汇编代码
编译流程
-
编译
编译器 (Compiler) 将每个源文件从某门高级编程语言转化为汇编语言,注意此时源文件仍然是一个 ASCII 或其他编码的文本文件 -
汇编
汇编器 (Assembler) 将上一步的每个源文件中的文本格式的指令转化为机器码,得到一个二进制的目标文件 (Object File) -
链接
链接器 (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