特权级的软硬件协同设计
让相对安全可靠的操作系统运行在一个硬件保护的安全执行环境中,不受到应用程序的破坏;而让应用程序运行在另外一个无法破坏操作系统的受限执行环境中
为确保操作系统的安全,对应用程序而言,需要限制的主要有两个方面:
- 应用程序不能访问任意的地址空间
- 应用程序不能执行某些可能破坏计算机系统的指令(本章的重点)
由于Libos中的函数调用方式(call ret )会直接绕过硬件的特权级保护检查
对应的硬件机制
RISC-V 提供了新的机器指令:执行环境调用指令(Execution Environment Call,简称 ecall
)和一类执行环境返回(Execution Environment Return,简称 eret
)指令。其中:
ecall
具有用户态到内核态的执行环境切换能力的函数调用指令;sret
:具有内核态到用户态的执行环境切换能力的函数返回指令。
eret指的是一大类的执行环境返回指令
sret
特指从 Supervisor 模式的执行环境(即 OS 内核)返回的那条指令
mret
特指从 Machine 模式的执行环境返回时使用, RustSBI 会用到这条指令
对应的操作系统需要完成的
1.应用程序调用ecall后,对系统调用参数检查,确保其不会破坏os
2.sret返回前,恢复用户态执行应用程序的上下文
RISC-V 特权级架构
RISC-V 架构中一共定义了 4 种特权级:
级别 |
编码 |
名称 |
0 |
00 |
用户/应用模式 (U, User/Application) |
1 |
01 |
监督模式 (S, Supervisor) |
2 |
10 |
虚拟监督模式 (H, Hypervisor) |
3 |
11 |
机器模式 (M, Machine) |
在CPU硬件层面,除了M模式必须存在外,其它模式可以不存在。
按需实现 RISC-V 特权级
RISC-V 架构中,只有 M 模式是必须实现的,剩下的特权级则可以根据跑在 CPU 上应用的实际需求进行调整:
- 简单的嵌入式应用只需要实现 M 模式;
- 带有一定保护能力的嵌入式系统需要实现 M/U 模式;
- 复杂的多任务系统则需要实现 M/S/U 模式。
在 RISC-V 架构上的引导加载程序一般运行在 M 模式上
执行环境的两种功能
1.执行它支持的上层软件之前进行一些初始化工作
2.对上层软件的执行进行监控管理
RISC-V架构下的常规控制流
顺序、循环、分支、函数调用
RISC-V架构下的异常控制流
异常控制流 (ECF, Exception Control Flow) 被称为 异常(Exception) ,是 RISC-V 语境下的 Trap 种类之一
2种从用户态切换的内核态的具体原因
1.用户软件执行特殊指令以获取操作系统提供的服务功能
2.用户程序执行指令时出错被CPU检测到
RISC-V下的异常
Interrupt |
Exception Code |
Description |
0 |
0 |
Instruction address misaligned |
0 |
1 |
Instruction access fault |
0 |
2 |
Illegal instruction |
0 |
3 |
Breakpoint |
0 |
4 |
Load address misaligned |
0 |
5 |
Load access fault |
0 |
6 |
Store/AMO address misaligned |
0 |
7 |
Store/AMO access fault |
0 |
8 |
Environment call from U-mode |
0 |
9 |
Environment call from S-mode |
0 |
11 |
Environment call from M-mode |
0 |
12 |
Instruction page fault |
0 |
13 |
Load page fault |
0 |
15 |
Store/AMO page fault |
Trap类指令指的是什么?
断点 Breakpoint
执行环境调用异常Environment call
操作系统对于其他类型的错误和异常是如何处理的?
将控制转交给高特权级的软件(如操作系统)来处理
1.当错误/异常恢复后,则可重新回到低优先级软件去执行;
2.如果不能恢复错误/异常,那高特权级软件可以杀死和清除低特权级软件,避免破坏整个执行环境。
RISC-V的特权指令
在 RISC-V 中,会有两类属于高特权级 S 模式的特权指令:
- 指令本身属于高特权级的指令,如
sret
指令(表示从 S 模式返回到 U 模式)。 - 指令访问了 S模式特权级下才能访问的寄存器 或内存,如表示S模式系统状态的 控制状态寄存器
sstatus
等。
指令 |
含义 |
sret |
从 S 模式返回 U 模式:在 U 模式下执行会产生非法指令异常 |
wfi |
处理器在空闲时进入低功耗状态等待中断:在 U 模式下执行会产生非法指令异常 |
sfence.vma |
刷新 TLB 缓存:在 U 模式下执行会产生非法指令异常 |
访问 S 模式 CSR 的指令 |
通过访问 sepc/stvec/scause/sscartch/stval/sstatus/satp等CSR 来改变系统状态:在 U 模式下执行会产生非法指令异常 |
wfi( (Wait For Interrupt) ):
wfi
指令的主要作用是让处理器进入低功耗状态,等待中断唤醒。- 在用户模式 (
U 模式
) 下执行wfi
会导致非法指令异常,因为这是一个特权指令,普通应用程序没有权限直接调用它。只有在更高权限的模式下(如内核模式)才能合法执行wfi
指令。
sfence.vma
sfence.vma
指令是 RISC-V 架构中的一条用于刷新 TLB(Translation Lookaside Buffer,翻译后备缓冲区)的指令。它与虚拟内存管理有关,通常用于在更改了页表映射后刷新 TLB,以确保后续内存访问使用更新的页表映射。
分析这条指令及描述
sfence.vma
的作用:
-
sfence.vma
指令用于在虚拟内存系统中刷新 TLB,以保证新的页表条目或虚拟地址到物理地址的映射在内存访问时能正确使用。- 当操作系统修改了进程的页表或切换了虚拟内存上下文时,TLB 可能还缓存了旧的地址映射,导致访问旧数据。
sfence.vma
指令就是用来强制清空或刷新 TLB,使处理器重新从页表中读取正确的地址映射。
- 什么时候会刷新TLB呢?
- 为何在 U 模式下会产生非法指令异常:
-
- U 模式(User Mode,用户模式)是 RISC-V 架构中的最低特权模式,普通应用程序在该模式下运行,并且没有权限直接管理系统资源,比如页表或 TLB。
总结:
sfence.vma
指令的作用是刷新 TLB,确保处理器使用最新的虚拟地址到物理地址映射。- 在 U 模式下执行
sfence.vma
会触发非法指令异常,因为该指令属于特权指令,只有操作系统或具有更高权限的代码才能管理虚拟内存映射和 TLB 缓存。
这是一种保护机制,确保普通应用程序不能随意操作底层硬件资源,避免破坏系统稳定性或安全性。
在批处理系统中运行应用程序
[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)
[kernel] Hello, world!
[kernel] num_app = 5
[kernel] app_0 [0x8020a038, 0x8020b4b8)
[kernel] app_1 [0x8020b4b8, 0x8020c9d0)
[kernel] app_2 [0x8020c9d0, 0x8020e080)
[kernel] app_3 [0x8020e080, 0x8020f580)
[kernel] app_4 [0x8020f580, 0x80210a80)
[kernel] Loading app_0
Hello, world!
[kernel] Application exited with code 0
[kernel] Loading app_1
Into Test store_fault, we will insert an invalid store operation...
Kernel should kill this application!
[kernel] PageFault in application, kernel killed it.
[kernel] Loading app_2
3^10000=5079(MOD 10007)
3^20000=8202(MOD 10007)
3^30000=8824(MOD 10007)
3^40000=5750(MOD 10007)
3^50000=3824(MOD 10007)
3^60000=8516(MOD 10007)
3^70000=2510(MOD 10007)
3^80000=9379(MOD 10007)
3^90000=2621(MOD 10007)
3^100000=2749(MOD 10007)
Test power OK!
[kernel] Application exited with code 0
[kernel] Loading app_3
Try to execute privileged instruction in U Mode
Kernel should kill this application!
[kernel] IllegalInstruction in application, kernel killed it.
[kernel] Loading app_4
Try to access privileged CSR in U Mode
Kernel should kill this application!
[kernel] IllegalInstruction in application, kernel killed it.
All applications completed!
oslab@oslab-virtual-machine:~/Desktop/rCore-Tutorial-v3/os$
在内存中一次加载一个应用程序,加载完一个再加载另一个
应用程序 的功能
[package]
name = "user_lib"#此处可以指定lib.rs所代表的库名,Lib.rs在/user/src/lib.rs
version = "0.1.0"
authors = ["Yifan Wu <shinbokuow@163.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
riscv = { git = "https://github.com/rcore-os/riscv", features = ["inline-asm"] }
[profile.release]
debug = true
[features]
board_qemu = []
board_k210 = []
#![no_std]
#![feature(linkage)]
#![feature(panic_info_message)]
#[macro_use]
pub mod console;
mod lang_items;
mod syscall;
#[no_mangle]
//一个编译器属性,用于防止 Rust 编译器对函数名进行“名称修饰”(name mangling)。
//这一属性确保生成的函数名在目标代码中依然是 _start,不会被修饰成其他形式。
#[link_section = ".text.entry"]
//将 _start 这段代码编译后的汇编代码中放在一个名为 .text.entry 的代码段中
//并且以此作为用户库的入口
pub extern "C" fn _start() -> ! {//定义程序的入口点start()
// extern "C":声明这个函数使用 C 语言的调用约定(ABI,Application Binary Interface),
// 这对与 C 语言或汇编代码的互操作性非常重要。
// 在系统编程中,入口点通常需要遵循 C 语言的 ABI 规则,
// 以便引导程序或硬件能够正确调用它。
clear_bss();
exit(main());//后调用 main 函数得到一个类型为 i32 的返回值,
//最后调用用户库提供的 exit 接口退出应用程序,
//并将 main 函数的返回值告知批处理系统。
panic!("unreachable after sys_exit!");
// 作用:这是一个 panic 语句,用于抛出一个运行时错误。
// 如果程序在调用 exit() 后继续执行到这里,说明出现了逻辑错误,
// 因为 exit 调用应该终止程序。
// 这个 panic 的信息是 "unreachable after sys_exit!",
// 表明此处的代码不应被执行。
}
#[linkage = "weak"]
// 第 1 行,我们使用 Rust 的宏将其函数符号 main 标志为弱链接。
// 这样在最后链接的时候,虽然在 lib.rs 和 bin 目录下的某个应用程序都有 main 符号,
// 但由于 lib.rs 中的 main 符号是弱链接,链接器会使用 bin 目录下的应用主逻辑作为 main
// 这里我们主要是进行某种程度上的保护,
// 如果在 bin 目录下找不到任何 main ,那么编译也能够通过,但会在运行时报错。
#[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);
});//由于直接操作内存地址,这是一个不安全的操作,因此需要用 unsafe 包裹。
// write_volatile 是一种写操作,它告诉编译器这次写入操作是不可优化的。
// 编译器通常会试图优化代码,例如删除多余的内存写入,
// 尤其是在没有其他代码读取这些内存时。
// 而 write_volatile 则确保每次写入操作都会被执行,
// 适用于硬件寄存器操作、内存映射 I/O 或其他需要防止优化的场景。
}
use syscall::*;
pub fn write(fd: usize, buf: &[u8]) -> isize {
sys_write(fd, buf)
}
pub fn exit(exit_code: i32) -> isize {
sys_exit(exit_code)
}
名称修饰
名称修饰(name mangling)是编译器在编译程序时,对函数名或变量名进行的编码转换,以便区分不同的函数、变量、以及其作用域或参数类型。特别是在像 C++ 和 Rust 这种支持函数重载或模块系统的语言中,编译器需要通过名称修饰来避免名字冲突。
名称修饰的目的
编译器在编译过程中,会将函数或变量的名字进行修饰,以包含更多的元信息,比如:
- 所在的命名空间或模块名。
- 参数的类型(用于支持函数重载)。
- 函数的返回类型等。
通过这种方式,编译器可以确保在不同模块、文件、或作用域中,函数或变量名字相同但意义不同的情况不会冲突。
Rust 中的名称修饰
Rust 编译器默认会对函数名进行名称修饰。假设我们有如下的 Rust 代码:
fn my_function() {
println!("Hello, Rust!");
}
Rust 编译器可能会将 my_function
进行名称修饰,变成一个类似于 my_function_9h3fkdjs3js
这样的符号,以确保不同模块、不同参数类型的函数不会冲突。
举例说明名称修饰
例如,假设我们有以下两个模块和两个函数:
mod module_a {
pub fn my_function() {
println!("This is module_a's function");
}
}
mod module_b {
pub fn my_function() {
println!("This is module_b's function");
}
}
Rust 编译器会对这两个 my_function
进行名称修饰,以确保在编译后的二进制文件中,它们可以共存。可能会生成类似以下的符号(以编译器实际生成的符号为准):
module_a::my_function
→_ZN8module_a11my_function17h9fbd2a4e12ef5ce3E
module_b::my_function
→_ZN8module_b11my_function17h8f21a24c9bf7cd1bE
这些修饰后的符号使得链接器可以在二进制文件中区分这两个函数,即使它们在源代码中名称相同。
#[no_mangle]
的作用
当你不希望编译器对函数名进