操作系统实验-rCore-lab2

目录

编程题

第1题

第2题

第3题

第4题

第5题

问答题

第1题

第2题

第3题

第4题

第5题

第6题

第7题

第8题

实验练习

实践作业

问答作业

第1题

第2题

第3题


编程题

第1题

题目要求:实现一个裸机应用程序A,能打印调用栈。

实现

以 rCore tutorial ch2 代码为例,在编译选项中我们已经让编译器对所有函数调用都保存栈指针(参考 os/.cargo/config ),因此我们可以直接从 fp 寄存器追溯调用栈:

os/src/stack_trace.rs
use core::{arch::asm, ptr};
​
pub unsafe fn print_stack_trace() -> () {
    let mut fp: *const usize;
    asm!("mv {}, fp", out(reg) fp);
​
    println!("== Begin stack trace ==");
    while fp != ptr::null() {
        let saved_ra = *fp.sub(1);
        let saved_fp = *fp.sub(2);
​
        println!("0x{:016x}, fp = 0x{:016x}", saved_ra, saved_fp);
​
        fp = saved_fp as *const usize;
    }
    println!("== End stack trace ==");
}

之后我们将其加入 main.rs 作为一个子模块:

加入 os/src/main.rs

// ...
mod syscall;
mod trap;
mod stack_trace;
// ...

作为一个示例,我们可以将打印调用栈的代码加入 panic handler 中,在每次 panic 的时候打印调用栈:

os/src/lang_items.rs
use crate::sbi::shutdown;
use core::panic::PanicInfo;
use crate::stack_trace::print_stack_trace;
​
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
    // ...
​
    unsafe { print_stack_trace(); }
​
    shutdown()
}

现在,进入panic 的时候输入的信息变成了这样:

Panicked at src/batch.rs:68 All applications completed!
== Begin stack trace ==
0x0000000080200e12, fp = 0x0000000080205cf0
0x0000000080201bfa, fp = 0x0000000080205dd0
0x0000000080200308, fp = 0x0000000080205e00
0x0000000080201228, fp = 0x0000000080205e60
0x00000000802005b4, fp = 0x0000000080205ef0
0x0000000080200424, fp = 0x0000000000000000
== End stack trace ==

这里打印的两个数字,第一个是栈帧上保存的返回地址,第二个是保存的上一个 frame pointer。

但在我本地运行时,感觉是由于作者给出的代码不会进入panic状态,所以不会有最后这个trace输出;

但是,我们可以看一下如下路径;

os/src/batch.rs

我们在这个文件中插入下面两条代码;

use crate::stack_trace::print_stack_trace;
​
​
unsafe { print_stack_trace(); }

 

这样在每次运行完一个app后就会打印调用栈;

 

angin,在评论区里发现一个ch2-lab版本;

git clone https://github.com/rcore-os/rCore-Tutorial-v3.git
cd rCore-Tutorial-v3
git checkout ch2-lab

然后根据如下图的流程进行更改;

就有了如下输出;

 

第2题

题目要求:扩展内核,实现新系统调用get_taskinfo,能显示当前task的id和task name;实现一个裸机应用程序B,能访问get_taskinfo系统调用。

实现

    pub fn print_app_info(&self) {
        println!("[kernel] num_app = {}", self.num_app);
        for i in 0..self.num_app {
            println!(
                "[kernel] app_{} [{:#x}, {:#x})",
                i,
                self.app_start[i],
                self.app_start[i + 1]
            );
        }
    }
​

第3题

题目要求:扩展内核,能够统计多个应用的执行过程中系统调用编号和访问此系统的调用的次数。

实现

(未实现)

第4题

题目要求:扩展内核,能够统计每个应用执行后的完成时间。

实现

(未实现)

第5题

题目要求:扩展内核,统计执行异常的程序的异常情况(主要是各种特权级涉及的异常),能够打印异常程序的出错的地址和指令等信息。

实现

在trap.c中添加相关异常情况的处理:

os/src/trap/trap.c

(但是这个路径下并没有c文件,只有一个.S汇编文件)

以下是要添加的.c程序

void usertrap()
{
   set_kerneltrap();    //使用 set_kerneltrap() 函数设置一个内核陷阱,以便操作系统能够捕获该错误并处理它
   struct trapframe *trapframe = curr_proc()->trapframe;    //用 curr_proc() 函数获取当前进程的 trapframe 结构体指针,该结构体包含了当前陷阱的信息。
   if ((r_sstatus() & SSTATUS_SPP) != 0)    //函数检查陷阱发生的位置是否在用户模式下,如果不是用户模式,函数会打印一条错误消息并自杀,因为在这种情况下,陷阱处理程序无法对错误做出任何有用的处理。
            panic("usertrap: not from user mode");
   uint64 cause = r_scause();
​
//如果陷阱发生在用户模式下,函数会检查陷阱原因的类型。
   if (cause & (1ULL << 63)) {
            cause &= ~(1ULL << 63);
            switch (cause) {
            case SupervisorTimer:   //如果陷阱原因是 SupervisorTimer,函数会打印一条消息并调用 set_next_timer() 函数来设置下一个定时器。
                  tracef("time interrupt!\n");
                  set_next_timer();
                  yield();
                  break;
            default:    //如果陷阱原因是其他类型,函数会调用 unknown_trap() 函数来报告错误并自杀。
                  unknown_trap();
                  break;
            }
   } else { 
            switch (cause) {
            case UserEnvCall:   如果陷阱原因为 UserEnvCall,函数会将陷阱指令的地址增加 4,并调用相应的系统调用。否则,如果陷阱原因为其他类型的错误,函数会打印错误消息并自杀。
                  trapframe->epc += 4;
                  syscall();
                  break;
            case StoreMisaligned:
            case StorePageFault:
            case InstructionMisaligned:
            case InstructionPageFault:
            case LoadMisaligned:
            case LoadPageFault:
                  printf("%d in application, bad addr = %p, bad instruction = %p, "
                           "core dumped.\n",
                           cause, r_stval(), trapframe->epc);
                  exit(-2);
                  break;
            case IllegalInstruction:
                  printf("IllegalInstruction in application, core dumped.\n");
                  exit(-3);
                  break;
            default:
                  unknown_trap();
                  break;
            }
   }
   usertrapret();   //函数使用 usertrapret() 函数返回到用户模式,以便用户程序能够继续执行
}

这段代码是一个用户级陷阱处理函数,它被调用时通常表示操作系统遇到了一个不可预测的错误或用户程序出现了错误。该函数的作用是处理这些错误并尽可能地保护系统的稳定性和完整性。

具体来说,该函数首先使用 set_kerneltrap() 函数设置一个内核陷阱,以便操作系统能够捕获该错误并处理它。然后,它使用 curr_proc() 函数获取当前进程的 trapframe 结构体指针,该结构体包含了当前陷阱的信息。

接着,函数检查陷阱发生的位置是否在用户模式下。如果不是用户模式,函数会打印一条错误消息并自杀,因为在这种情况下,陷阱处理程序无法对错误做出任何有用的处理。

如果陷阱发生在用户模式下,函数会检查陷阱原因的类型。如果陷阱原因是 SupervisorTimer,函数会打印一条消息并调用 set_next_timer() 函数来设置下一个定时器。如果陷阱原因是其他类型,函数会调用 unknown_trap() 函数来报告错误并自杀。

如果陷阱原因为 UserEnvCall,函数会将陷阱指令的地址增加 4,并调用相应的系统调用。否则,如果陷阱原因为其他类型的错误,函数会打印错误消息并自杀。

最后,函数使用 usertrapret() 函数返回到用户模式,以便用户程序能够继续执行。

问答题

(此次问答题大多都可以在实验指导书习题之前的章节中直接获取答案,请仔细阅读实验指导书,不必拘泥于习题答案解答中给出的答案)

第1题

题目要求:函数调用与系统调用有何区别?

解答

  • 函数调用用普通的控制流指令,不涉及特权级的切换;系统调用使用专门的指令(如 RISC-V 上的 ecall),会切换到内核特权级。

  • 函数调用可以随意指定调用目标;系统调用只能将控制流切换给调用操作系统内核给定的目标。

第2题

题目要求:为了方便操作系统处理,M态软件会将 S 态异常/中断委托给 S 态软件,请指出有哪些寄存器记录了委托信息,rustsbi 委托了哪些异常/中断?(也可以直接给出寄存器的值)

 

解答

  • 两个寄存器记录了委托信息: mideleg (中断委托)和 medeleg (异常委托)

  • 参考 RustSBI 输出

    [rustsbi] mideleg: ssoft, stimer, sext (0x222)
    [rustsbi] medeleg: ima, ia, bkpt, la, sa, uecall, ipage, lpage, spage (0xb1ab)

    可知委托了中断

    • ssoft : S-mode 软件中断

    • stimer : S-mode 时钟中断

    • sext : S-mode 外部中断

    委托了异常

    • ima : 指令未对齐

    • ia : 取指访问异常

    • bkpt : 断点

    • la : 读异常

    • sa : 写异常

    • uecall : U-mode 系统调用

    • ipage : 取指 page fault

    • lpage : 读 page fault

    • spage : 写 page fault

第3题

题目要求:如果操作系统以应用程序库的形式存在,应用程序可以通过哪些方式破坏操作系统?

解答

如果操作系统以应用程序库的形式存在,那么编译器在链接OS库时会把应用程序跟OS库链接成一个可执行文件,两者处于同一地址空间,这也是LibOS(Unikernel)架构,此时存在如下几个破坏操作系统的方式:

  • 缓冲区溢出:应用程序可以覆盖写其合法内存边界之外的部分,这可能会危及 OS;

  • 整数溢出:当对整数值的运算产生的值超出整数数据类型可以表示的范围时,就会发生整数溢出, 这可能会导致OS出现意外行为和安全漏洞。 例如,如果允许应用程序分配大量内存,攻击者可能会在内存分配例程中触发整数溢出,从而可能导致缓冲区溢出或其他安全漏洞;

  • 系统调用拦截:应用程序可能会拦截或重定向系统调用,从而可能损害OS的行为。例如,攻击者可能会拦截读取敏感文件的系统调用并将其重定向到他们选择的文件,从而可能危及 unikernel 的安全性。

  • 资源耗尽:应用程序可能会消耗内存或网络带宽等资源,可能导致拒绝服务或其他安全漏洞。

第4题

题目要求:编译器/操作系统/处理器如何合作,可采用哪些方法来保护操作系统不受应用程序的破坏?

解答

编译器、操作系统和处理器之间的合作是通过保护模式实现的。保护模式是一种CPU硬件原生提供的模式,它提供了更为安全的管理内存,处理器在硬件上提供地址转换部件,操作系统提供转换过程中所需要的页表等。在保护模式下,操作系统可以控制应用程序的访问权限,防止应用程序破坏操作系统。编译器可以生成符合保护模式的代码,以便在保护模式下运行。

硬件操作系统运行在一个硬件保护的安全执行环境中,不受到应用程序的破坏;应用程序运行在另外一个无法破坏操作系统的受限执行环境中。

现代CPU提供了很多硬件机制来保护操作系统免受恶意应用程序的破坏,包括如下几个:

  • 特权级模式:处理器能够设置不同安全等级的执行环境,即用户态执行环境和内核态特权级的执行环境。处理器在执行指令前会进行特权级安全检查,如果在用户态执行环境中执行内核态特权级指令,会产生异常阻止当前非法指令的执行。

  • TEE(可信执行环境):CPU的TEE能够构建一个可信的执行环境,用于抵御恶意软件或攻击,能够确保处理敏感数据的应用程序(例如移动银行和支付应用程序)的安全。

  • ASLR(地址空间布局随机化):ASLR 是CPU的一种随机化进程地址空间布局的安全功能,其能够随机生成进程地址空间,例如栈、共享库等关键部分的起始地址,使攻击者预测特定数据或代码的位置。

第5题

题目要求:RISC-V处理器的S态特权指令有哪些,其大致含义是什么,有什么作用?

解答

RISC-V处理器的S态特权指令有两类:

  1. 指令本身属于高特权级的指令,如 sret 指令(表示从 S 模式返回到 U 模式)。

  2. 指令访问了S模式特权级下才能访问的寄存器或内存,如表示S模式系统状态的 控制状态寄存器 sstatus 等。

    如下所示:

  • sret:从 S 模式返回 U 模式。如可以让位于S模式的驱动程序返回U模式。

  • wfi:让CPU在空闲时进入等待状态,以降低CPU功耗。

  • sfence.vma:刷新 TLB 缓存,在U模式下执行会尝试非法指令异常。

  • 访问 S 模式 CSR 的指令:通过访问spce/stvec/scause/sscartch/stval/sstatus/satp等CSR来改变系统状态。

第6题

题目要求:RISC-V处理器在用户态执行特权指令后的硬件层面的处理过程是什么?

解答

CPU 执行完一条指令(如 ecall )并准备从用户特权级 陷入( Trap )到 S 特权级的时候,硬件会自动完成如下这些事情:

  • sstatus 的 SPP 字段会被修改为 CPU 当前的特权级(U/S)。

  • sepc 会被修改为 Trap 处理完成后默认会执行的下一条指令的地址。

  • scause/stval 分别会被修改成这次 Trap 的原因以及相关的附加信息。

  • cpu 会跳转到 stvec 所设置的 Trap 处理入口地址,并将当前特权级设置为 S ,然后从Trap 处理入口地址处开始执行。

CPU 完成 Trap 处理准备返回的时候,需要通过一条 S 特权级的特权指令 sret 来完成,这一条指令具体完成以下功能:

  • CPU 会将当前的特权级按照 sstatus 的 SPP 字段设置为 U 或者 S ;

  • CPU 会跳转到 sepc 寄存器指向的那条指令,然后继续执行。

 

第7题

题目要求:操作系统在完成用户态<->内核态双向切换中的一般处理过程是什么?

解答

当 CPU 在用户态特权级( RISC-V 的 U 模式)运行应用程序,执行到 Trap,切换到内核态特权级( RISC-V的S 模式),批处理操作系统的对应代码响应 Trap,并执行系统调用服务,处理完毕后,从内核态返回到用户态应用程序继续执行后续指令。

第8题

题目要求:程序陷入内核的原因有中断、异常和陷入(系统调用),请问riscv64支持哪些中断/异常?如何判断进入内核是由于中断还是异常?描述陷入内核时的几个重要寄存器及其值。

解答

  • 具体支持的异常和中断,参见 RISC-V 特权集规范 The RISC-V Instruction Set Manual Volume II: Privileged Architecture

  • scause 的最高位,为 1 表示中断,为 0 表示异常

  • 重要的寄存器

    • scause :发生了具体哪个异常或中断

    • sstatus :其中的一些控制为标志发生异常时的处理器状态,如 sstatus.SPP 表示发生异常时处理器在哪个特权级。

    • sepc :发生异常或中断的时候,将要执行但未成功执行的指令地址

    • stval :值与具体异常相关,可能是发生异常的地址,指令等

 

第9题

题目要求:在哪些情况下会出现特权级切换:用户态->内核态,以及内核态->用户态?

解答

  • 用户态–>内核态:应用程序发起系统调用;应用程序执行出错,需要到批处理操作系统中杀死该应用并加载运行下一个应用;应用程序执行结束,需要到批处理操作系统中加载运行下一个应用。

  • 内核态–>用户态:启动应用程序需要初始化应用程序的用户态上下文时;应用程序发起的系统调用执行完毕返回应用程序时。

第10题

题目要求:Trap上下文的含义是什么?在本章的操作系统中,Trap上下文的具体内容是什么?如果不进行Trap上下文的保存与恢复,会出现什么情况?

解答

Trap上下文的主要有两部分含义:

  • 在触发 Trap 之前 CPU 运行在哪个特权级;

  • CPU 需要切换到哪个特权级来处理该 Trap ,并在处理完成之后返回原特权级。

在本章的实际操作系统中,Trap上下文的具体内容主要包括通用寄存器和栈两部分。

如果不进行Trap的上下文保存与恢复,CPU就无法在处理完成之后,返回原特权级。

实验练习

实践作业

sys_write 安全检查

ch2 中,我们实现了第一个系统调用 sys_write,这使得我们可以在用户态输出信息。但是 os 在提供服务的同时,还有保护 os 本身以及其他用户程序不受错误或者恶意程序破坏的功能。

由于还没有实现虚拟内存,我们可以在用户程序中指定一个属于其他程序字符串,并将它输出,这显然是不合理的,因此我们要对 sys_write 做检查:

  • sys_write 仅能输出位于程序本身内存空间内的数据,否则报错。

实验要求

  • 实现分支: ch2-lab

  • 目录要求不变

  • 为 sys_write 增加安全检查

    在 os 目录下执行 make run TEST=1 测试 sys_write 安全检查的实现,正确执行目标用户测例,并得到预期输出(详见测例注释)。

    注意:如果设置默认 log 等级,从 lab2 开始关闭所有 log 输出。

challenge: 支持多核,实现多个核运行用户程序。

实验约定

在第二章的测试中,我们对于内核有如下仅仅为了测试方便的要求,请调整你的内核代码来符合这些要求。

  • 用户栈大小必须为 4096,且按照 4096 字节对齐。这一规定可以在实验4开始删除,仅仅为通过 lab2/3 测例设置。

解决方式

const FD_STDOUT: usize = 1;
const APP_BASE_ADDRESS: usize = 0x80400000;
const APP_SIZE_LIMIT: usize = 0x20000;
use crate::batch::{USER_STACK};
const USER_STACK_SIZE: usize = 4096;
​
​
pub fn sys_write(fd: usize, buf: *const u8, len: usize) -> isize {
    match fd {
        FD_STDOUT => {
            // unsafe {println!("#{:#x} {:#x} #", buf as usize , USER_STACK.get_sp() - USER_STACK_SIZE);}
            if (((buf as usize)  >= USER_STACK.get_sp() - USER_STACK_SIZE) && ((buf as usize) + len <= USER_STACK.get_sp())) 
            || (((buf as usize) + len <= APP_SIZE_LIMIT + APP_BASE_ADDRESS) && ((buf as usize) >= APP_BASE_ADDRESS)){
                let slice = unsafe { core::slice::from_raw_parts(buf, len) };
                let str = core::str::from_utf8(slice).unwrap();
                print!("{}", str);
                len as isize
            }else{
                -1 as isize
            }
        },
        _ => {
            -1 as isize
            //panic!("Unsupported fd in sys_write!");
        }
    }
}

在ch2-lab分支下执行make run TEST=1,效果如下;

运行app的数量从6变为2,只执行用户态程序;

 

问答作业

第1题

题目要求:正确进入U态后,程序的特征还应有:使用S态特权指令,访问S态寄存器后会报错。请自行测试这些内容(运行Rust三个bad测例),描述程序出错行为,注明你是用的sbi及其版本。

[rustsbi] RustSBI version 0.3.1, adapting to RISC-V SBI v1.0.0

在用户态访问S态的指令,访问属于S态的寄存器等都会出错

 

在这两个程序中,已经体现了这两个情况,03是U态使用S态特权指令,04是U态使用S态寄存器;

报错信息如下;

 

第2题

题目要求

请结合用例理解 trap.S 中两个函数 __alltraps__restore 的作用,并回答如下几个问题:

L40:刚进入 __restore 时,a0 代表了什么值。请指出 __restore 的两种使用情景。

a0代表了返回用户态时从哪里开始执行. __restore 系统启动时从内核态进入用户态, 中断返回时从内核态返回用户态. 其实在lab2 中依次运行各个进程的过程就是: 内核启动 -> sret到第一个用户态进程 -> 进程exit -> sret到第二个用户态进程 -> 直到结束…

L46-L51:这几行汇编代码特殊处理了哪些寄存器?这些寄存器的的值对于进入用户态有何意义?请分别解释。

ld t0, 32*8(sp)
ld t1, 33*8(sp)
ld t2, 2*8(sp)
csrw sstatus, t0
csrw sepc, t1
csrw sscratch, t2

通过观察在__alltraps中保存的内容可以得知: t0-t1其实就是TrapContext 的后两个字段sstatus和sepc ; t2 是存的用户栈的地址 ;

L53-L59:为何跳过了 x2x4

ld x1, 1*8(sp)
ld x3, 3*8(sp)
.set n, 5
.rept 27
   LOAD_GP %n
   .set n, n+1
.endr

tp(x2): 因为我们要基于它来找到每个寄存器应该被保存到的正确的位置

tp(x4) :除非我们手动出于一些特殊用途使用它

L63:该指令之后,spsscratch 中的值分别有什么意义?

csrrw sp, sscratch, sp

将sp中的内容与sscratch中的内容进行交换

__restore:中发生状态切换在哪一条指令?为何该指令执行之后会进入用户态?

sret

 

从 U 态进入 S 态是哪一条指令发生的?

系统调用,例如ecall;

第3题

题目要求:对于任何中断,__alltraps 中都需要保存所有寄存器吗?你有没有想到一些加速 __alltraps 的方法?简单描述你的想法。

要保存所有寄存器,对于通用寄存器而言,两条控制流(应用程序控制流和内核控制流)运行在不同的特权级,所属的软件也可能由不同的编程语言编写,虽然在 Trap 控制流中只是会执行 Trap 处理相关的代码,但依然可能直接或间接调用很多模块,因此很难甚至不可能找出哪些寄存器无需保存。既然如此我们就只能全部保存了。但这里也有一些例外,如 x0 被硬编码为 0 ,它自然不会有变化;还有 tp(x4) 寄存器,除非我们手动出于一些特殊用途使用它,否则一般也不会被用到。虽然它们无需保存,但我们仍然在 TrapContext 中为它们预留空间,主要是为了后续的实现方便。

想法:只需要保留在中断执行过程中可能改变的寄存器

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值