
OS 简单教程四
特权级
目标是这次也打印 Hello OS 不同的是,这是在用户态运行的应用。 为什么需要系统调用?特权?trap?CPU 虚拟化?直接都给权限,都在一个态不香么?试想一个应用就能破坏操作系统,操作系统多脆弱啊。应用有意无意的错误不要影响到操作系统和其他应用,这就需要硬件和操作系统配合来提供特权。应用只能运行在用户态,这样应用就可以放心使用 CPU ,这是 CPU 第一种虚拟化。 应用在用户态,操作系统在内核态。应用不能自己执行危险操作,想要执行必须通过系统调用委托给操作系统,而我们信赖操作系统。今天我们尝试在 k210 上实现特权级。
应用
像之前的内核一样,不同的是 lib.rs 内容是
#![no_std]
#![feature(asm)]
#![feature(linkage)]
use scall_os::{sys_exit, sys_write};
#[macro_use]
pub mod console;
mod lang;
mod scall_os;
#[no_mangle]
#[link_section = ".text.entry2"]
pub extern "C" fn _start() -> ! {
clear_bss();
exit(main());
panic!("unreachable after sys_exit!");
}
#[linkage = "weak"]
#[no_mangle]
fn main() -> i32 {
panic!("Cannot find main!");
}
fn clear_bss() {
extern "C" {
fn start_bss();
fn end_bss();
}
(start_bss as usize..end_bss as usize).for_each(|addr| {
unsafe { (addr as *mut u8).write_volatile(0); }
});
}
pub fn write(fd: usize, buf: &[u8]) -> isize { sys_write(fd, buf) }
pub fn exit(exit_code: i32) -> isize { sys_exit(exit_code) }
可以看见入口是 _start ,先给全局未初始化的变量都赋值 0 ,然后调用 main 函数,最后系统调用 OS 退出应用。注意 main 函数是 weak 的,链接的时候如果有 strong 就用 strong ,而他在 user/bin/hello.rs
#![no_std]
#![no_main]
#![feature(llvm_asm)]
#[macro_use]
extern crate user;
#[no_mangle]
fn main() -> i32 {
println!("Hello OS");
0
}
根据 rust 的规范,bin 下的文件会独立构建成可执行文件
trap
trap 是不是很耳熟,应用要打印就会用 ecall 触发 trap ,我们先来实现 trap 。 先来看 trap 的初始化,trap/mod.rs 的内容
pub fn init() {
extern "C" {
fn __alltraps();
}
unsafe {
stvec::write(__alltraps as usize, TrapMode::Direct);
}
}
这个 init 函数在 main.rs 调用。 那 __alltraps 是什么呢?trap/trap.asm 的内容
.altmacro
.macro SAVE_GP n
sd x\n, \n*8(sp)
.endm
.macro LOAD_GP n
ld x\n, \n*8(sp)
.endm
.section .text
.globl __alltraps
.globl __restore
.align 2
__alltraps:
csrrw sp, sscratch, sp
# now sp->kernel stack, sscratch->user stack
# allocate a TrapContext on kernel stack
addi sp, sp, -34*8
# save general-purpose registers
sd x1, 1*8(sp)
# skip sp(x2), we will save it later
sd x3, 3*8(sp)
# skip tp(x4), application does not use it
# save x5~x31
.set n, 5
.rept 27
SAVE_GP %n
.set n, n+1
.endr
# we can use t0/t1/t2 freely, because they were saved on kernel stack
csrr t0, sstatus
csrr t1, sepc
sd t0, 32*8(sp)
sd t1, 33*8(sp)
# read user stack from sscratch and save it on the kernel stack
csrr t2, sscratch
sd t2, 2*8(sp)
# set input argument of trap_handler(cx: &mut TrapContext)
mv a0, sp
call trap_handler
__restore:
# case1: start running app by __restore
# case2: back to U after handling trap
mv sp, a0
# now sp->kernel stack(after allocated), sscratch->user stack
# restore sstatus/sepc
ld t0, 32*8(sp)
ld t1, 33*8(sp)
ld t2, 2*8(sp)
csrw sstatus, t0
csrw sepc, t1
csrw sscratch, t2
# restore general-purpuse registers except sp/tp
ld x1, 1*8(sp)
ld x3, 3*8(sp)
.set n, 5
.rept 27
LOAD_GP %n
.set n, n+1
.endr
# release TrapContext on kernel stack
addi sp, sp, 34*8
# now sp->kernel stack, sscratch->user stack
csrrw sp, sscratch, sp
sret
__alltraps 都干了什么呢?应用运行要使用寄存器,于是系统调用前就保存寄存器、之后再恢复。sepc 寄存器比较特殊,trap 之后由它来决定后面执行什么。我们也可以利用它来执行第一个应用,一会就看见了。alltraps 最后 call trap_handler ,trap_handler 最后根据 system call id 来决定到底该调用哪个系统调用,这里用的 linux 的系统调用规范,当然也可以用别的,约定好就行。 我们再看看 trap/mod.rs 的最后的内容,包含 trap_handler
use riscv::register::{
scause::{self, Exception, Trap},
stval, stvec,
utvec::TrapMode,
};
pub use trap_ctx::TrapContext;
use crate::scall_sbi::syscall;
mod trap_ctx;
global_asm!(include_str!("trap.asm"));
pub fn init() {
extern "C" {
fn __alltraps();
}
unsafe {
stvec::write(__alltraps as usize, TrapMode::Direct);
}
}
#[no_mangle]
pub fn trap_handler(cx: &mut TrapContext) -> &mut TrapContext {
let scause = scause::read();
let stval = stval::read();
match scause.cause() {
Trap::Exception(Exception::UserEnvCall) => {
cx.sepc += 4;
cx.x[10] = syscall(cx.x[17], [cx.x[10], cx.x[11], cx.x[12]]) as usize;
}
Trap::Exception(Exception::StoreFault) | Trap::Exception(Exception::StorePageFault) => {
println!("[kernel] PageFault in application, core dumped.");
panic!("StoreFault!");
}
Trap::Exception(Exception::IllegalInstruction) => {
println!("[kernel] IllegalInstruction in application, core dumped.");
panic!("IllegalInstruction!");
}
_ => {
panic!(
"Unsupported trap {:?}, stval = {:#x}!",
scause.cause(),
stval
);
}
}
cx
}
task
那应用第一怎么执行的呢?
- 把应用放到内核数据段
- 用符号确定应用的位置
- 把应用复制到 0x80400000
- 应用的内存布局也是 0x80400000 开头,因为内存地址有用到绝对地址
- 最后从 0x80400000 开始执行
- 1、2 是由 build.rs 根据应用生成的,build.rs 在 cargo build 时候自动调用,生成 link_app.S ,我们看看它
.align 3
.section .data
.global _num_app
_num_app:
.quad 1
.quad app_0_start
.quad app_0_end
.section .data
.global app_0_start
.global app_0_end
app_0_start:
.incbin "../user/target/riscv64gc-unknown-none-elf/release/hello.bin"
app_0_end:
可以看到应用在内核的数据段,start 和 end 符号指定了应用的开始和结束地址
- 3 是由 task/mod.rs 的 load_app 函数负责把应用复制到 0x80400000
unsafe fn load_app(&self) {
// clear app area
(APP_BASE_ADDRESS..APP_BASE_ADDRESS + APP_SIZE_LIMIT).for_each(|addr| {
(addr as *mut u8).write_volatile(0);
});
let app_src = core::slice::from_raw_parts(
self.app_start[0] as *const u8,
self.app_start[1] - self.app_start[0],
);
let app_dst = core::slice::from_raw_parts_mut(APP_BASE_ADDRESS as *mut u8, app_src.len());
app_dst.copy_from_slice(app_src);
}
- 4 是由 user/src/link.ld 指定应用的内存布局,应用的内存布局也是 0x80400000 开头,因为内存地址有用到绝对地址
OUTPUT_ARCH(riscv)
ENTRY(_start)
BASE_ADDRESS = 0x80400000;
SECTIONS
{
. = BASE_ADDRESS;
.text : {
*(.text.entry2)
*(.text .text.*)
}
.rodata : {
*(.rodata .rodata.*)
*(.srodata .srodata.*)
}
.data : {
*(.data .data.*)
*(.sdata .sdata.*)
}
.bss : {
start_bss = .;
*(.bss .bss.*)
*(.sbss .sbss.*)
end_bss = .;
}
/DISCARD/ : {
*(.eh_frame)
*(.debug*)
}
}
- 最后我们看看应用是怎么执行的 ,task/mod.rs/run 函数
pub fn run() -> ! {
unsafe {
APP_MANAGER.inner.borrow().load_app();
}
extern "C" {
fn __restore(cx_addr: usize);
}
unsafe {
__restore(KERNEL_STACK.push_context(TrapContext::app_init_context(
APP_BASE_ADDRESS,
USER_STACK.get_sp(),
)) as *const _ as usize);
}
panic!("Unreachable in task::run!");
}
__restore 汇编函数会设置 sepc 寄存器的值,sepc 刚好被设置成 0x80400000 ,下一条指令就是 0x80400000 啦。 One more thing. 看看结果
[rustsbi] RustSBI version 0.2.0-alpha.3
.______ __ __ _______.___________. _______..______ __
| _ \ | | | | / | | / || _ \ | |
| |_) | | | | | | (----`---| |----`| (----`| |_) || |
| / | | | | \ \ | | \ \ | _ < | |
| |\ \----.| `--' |.----) | | | .----) | | |_) || |
| _| `._____| \______/ |_______/ |__| |_______/ |______/ |__|
[rustsbi] Platform: K210 (Version 0.2.0)
[rustsbi] misa: RV64ACDFIMSU
[rustsbi] mideleg: 0x22
[rustsbi] medeleg: 0x1ab
[rustsbi] Kernel entry: 0x80020000
[kernel] app_0 [0x8002b018, 0x8002b938)
Hello OS from app
[kernel] Application exited with code 0
具体完整代码可参考 https://github.com/buhe/bugu/tree/0.2.0