内核重映射实现之三:完结

在内存模块初始化时,我们新建一个精细映射的 MemorySet 并切换过去供内核使用。

  1. // src/memory/mod.rs
  2. ......
  3. pub fn init(l: usize, r: usize) {
  4. FRAME_ALLOCATOR.lock().init(l, r);
  5. init_heap();
  6. // 内核重映射
  7. kernel_remap();
  8. println!("++++ setup memory! ++++");
  9. }
  10. pub fn kernel_remap() {
  11. let mut memory_set = MemorySet::new();
  12. extern "C" {
  13. fn bootstack(); //定义在src/boot/entry64.asm
  14. fn bootstacktop(); //定义在src/boot/entry64.asm
  15. }
  16. // 將启动栈 push 进来
  17. memory_set.push(
  18. bootstack as usize,
  19. bootstacktop as usize,
  20. MemoryAttr::new(),
  21. Linear::new(PHYSICAL_MEMORY_OFFSET),
  22. );
  23. unsafe {
  24. memory_set.activate();
  25. }
  26. }

这里要注意的是,我们不要忘了将启动栈加入实际可用的虚拟内存空间。因为我们现在仍处于启动过程中,因此离不开启动栈。

主函数里则是:

  1. // src/init.rs
  2. #[no_mangle]
  3. pub extern "C" fn rust_main() -> ! {
  4. crate::interrupt::init();
  5. extern "C" {
  6. fn end();
  7. }
  8. crate::memory::init(
  9. ((end as usize - KERNEL_BEGIN_VADDR + KERNEL_BEGIN_PADDR) >> 12) + 1,
  10. PHYSICAL_MEMORY_END >> 12
  11. );
  12. crate::timer::init();
  13. loop {}
  14. }

运行一下,可以发现屏幕上仍在整齐的输出着 100 ticks! 我们回过头来验证一下关于读、写、执行的权限是否被正确处理了。 写这么几个测试函数:

  1. // 只读权限,却要写入
  2. fn write_readonly_test() {
  3. extern "C" {
  4. fn srodata();
  5. }
  6. unsafe {
  7. let ptr = srodata as usize as *mut u8;
  8. *ptr = 0xab;
  9. }
  10. }
  11. // 不允许执行,非要执行
  12. fn execute_unexecutable_test() {
  13. extern "C" {
  14. fn sbss();
  15. }
  16. unsafe {
  17. asm!("jr $0" :: "r"(sbss as usize) :: "volatile");
  18. }
  19. }
  20. // 找不到页表项
  21. fn read_invalid_test() {
  22. println!("{}", unsafe { *(0x12345678 as usize as *const u8) });
  23. }

memory::init 后面调用任一个测试函数,都会发现内核 panic 并输出:

undefined trap

  1. panicked at 'undefined trap!', src/interrupt.rs:40:14

这说明内核意识到出了某些问题进入了中断,但我们并没有加以解决。 我们在中断处理里面加上对应的处理方案:

  1. // src/interrupt.rs
  2. #[no_mangle]
  3. pub fn rust_trap(tf: &mut TrapFrame) {
  4. match tf.scause.cause() {
  5. Trap::Exception(Exception::Breakpoint) => breakpoint(&mut tf.sepc),
  6. Trap::Interrupt(Interrupt::SupervisorTimer) => super_timer(),
  7. Trap::Exception(Exception::InstructionPageFault) => page_fault(tf),
  8. Trap::Exception(Exception::LoadPageFault) => page_fault(tf),
  9. Trap::Exception(Exception::StorePageFault) => page_fault(tf),
  10. _ => panic!("undefined trap!")
  11. }
  12. }
  13. fn page_fault(tf: &mut TrapFrame) {
  14. println!("{:?} va = {:#x} instruction = {:#x}", tf.scause.cause(), tf.stval, tf.sepc);
  15. panic!("page fault!");
  16. }

我们再依次运行三个测试,会得到结果为:

权限测试

  1. // read_invalid_test Result
  2. Exception(LoadPageFault) va = 0x12345678 instruction = 0xffffffffc020866c
  3. panicked at 'page fault!', src/interrupt.rs:65:5
  4. // execute_unexecutable_test Result
  5. Exception(InstructionPageFault) va = 0xffffffffc021d000 instruction = 0xffffffffc021d000
  6. panicked at 'page fault!', src/interrupt.rs:65:5
  7. // write_readonly_test Result
  8. Exception(StorePageFault) va = 0xffffffffc0213000 instruction = 0xffffffffc020527e
  9. panicked at 'page fault!', src/interrupt.rs:65:5

从中我们可以清楚的看出内核成功的找到了错误的原因,内核各段被成功的设置了不同的权限。我们达到了内核重映射的目的!目前的代码能在这里找到。

如何找到产生错误的源码位置

在上面的三个测试中,虽然可以看到出错的指令的虚拟地址,但还是不能很直接地在源码级对应到出错的地方。这里有两个方法可以做到源码级错误定位,一个是 Qemu+GDB 的动态调试方法(这里不具体讲解),另外一个是通过addr2line工具来帮助我们根据指令的虚拟地址来做到源码的位置,具体方法如下:

  1. #先找到编译初的ELF格式的OS
  2. $ cd rCore_tutorial/os/target/riscv64imac-unknown-none-elf/debug
  3. $ file os # 这个就是我们要分析的目标
  4. os: ELF 64-bit LSB executable, UCB RISC-V, version 1 (SYSV), statically linked, with debug_info, not stripped
  5. $ riscv64-unknown-elf-addr2line -e os 0xffffffffc020527e
  6. rCore_tutorial/os/src/init.rs:35
  7. #查看rCore_tutorial/os/src/init.rs第35行的位置,可以看到
  8. 29: fn write_readonly_test() {
  9. 30: extern "C" {
  10. 31: fn srodata();
  11. 32: }
  12. 33: unsafe {
  13. 34: let ptr = srodata as usize as *mut u8;
  14. 35: *ptr = 0xab;
  15. 36: }
  16. 37: }
  17. #可以轻松定位到出错的语句``*ptr = 0xab;``