rCore_Tutorial_CP2
这一章的文件结构略微的复杂了一点,需要开始之前先稍微强化一下对rust mod use等关键字的认知。。。
rust mod/use/extern
Packages and Crates rust认为src下的main.rs是二进制crate的主文件,而lib.rs则是库crate的主文件,所以这里的lib.rs是主文件,和cargo.toml中的name赋值为user_lib
并无关联。。。
When we entered the command, Cargo created a Cargo.toml file, giving us a package. Looking at the contents of Cargo.toml, there’s no mention of src/main.rs because Cargo follows a convention that src/main.rs is the crate root of a binary crate with the same name as the package. Likewise, Cargo knows that if the package directory contains src/lib.rs, the package contains a library crate with the same name as the package, and src/lib.rs is its crate root. Cargo passes the crate root files to rustc to build the library or binary.
Here, we have a package that only contains src/main.rs, meaning it only contains a binary crate named my-project. If a package contains src/main.rs and src/lib.rs, it has two crates: a binary and a library, both with the same name as the package. A package can have multiple binary crates by placing files in the src/bin directory: each file will be a separate binary crate.
所以user/src/bin下面塞的几个独立文件实际上也是都依赖于lib.rs这个库的
其次是mod关键字,mod就是用来进行模块化编程的,但是网上常见的教程都是直接写一个文件里面mod套mod折磨人,并且mod的实现直接写在定义后的body内,而实际使用时的mod给人的感觉却像是一个import语句,这里解释了什么情况
The difference between mod and use in Rust
In the example above we see that we can write a mod statement without declaring a body (the curly brackets {}). When you do this and compile your crate, Cargo will look for a .rs file in the current directory with the name given after the mod declaration.
所以在正常开发流程中一般不会出现mod套mod,也不会出现主文件外的地方使用mod关键字,因为mod的实际结构应当由文件结构实现,而mod也不是一个import的语句,虽然表现上有点像,但实际上这个是对自身子模块的定义,为了较为高效的开发将子模块从主文件中移除出去的关键字罢了
这里console.rs中的第一句use super::write
就是子模块使用父模块的write函数。直接用crate来表示最顶级模块也是可以的,比如use crate::write
,模块与模块之间可以使用路径进行引入(use关键字其实也不是引入关键字,感觉更像别名关键字),在use crate::write
之后可以直接使用write来表名这个函数,但你不用use也可以直接crate::write完整路径调用这个方法
bin目录下的extern crate就是引入外部crate,实际上不引入用全名也一样。并且rust2018之后直接use也一样。。。垃圾语言嗼
What’s the difference between use and extern?
所以魔改成这样也跑得起来
#![no_std]
#![no_main]
#[macro_use]
use user_lib::*;
#[no_mangle]
fn main() -> i32 {
println!("Hello, world!");
0
}
或者删了use直接user_lib::println!
都行
总而言之整体和之前写的js python什么的不一样,不是一份文件写好了哪里用哪里import,而是整个项目有一个目录层级,项目内的文件都互相直接可达,直接按照目录层级访问方法即可。mod关键字就是用来组织目录层级的
实现应用程序
原始操作系统的部分以库函数的形式与应用程序进行强耦合。这里编写的lib.rs实现了系统调用和函数入口点装载等操作。
可以先使用qemu-riscv64在riscv64的linux系统上先模拟运行一下,用户应用程序中未实现CH1中的asm手搓初始化栈和预留初始栈空间以及裁剪elf header。操作系统通过我们提供的elf header直接将程序加载并在entry处进入,并设置好了初始栈。操作系统,我滴超人
实现批处理操作系统
rust,我滴精神病人
这个语言真反人类啊
光看懂代码就花了半天x..y
表示[x,y)
我是知道的,原来x..=y
是[x,y]
啊
各种生命周期加借用就已经给我整无语了,这里还来了一套rust指针操作借用切片混合双打,垃圾语言嗼
What is the difference between a slice and an array?
这里多次使用了from_raw_parts(_mut)
这个函数,这个是一个unsafe的函数,参数为指针和长度,获得一个指向目标地址对应长度的切片的引用。mut就是得到的引用还可更改,后面有用这个情况内存,功能类似memset
let mut app_start: [usize; MAX_APP_NUM + 1] = [0; MAX_APP_NUM + 1];
let app_start_raw: &[usize] =
core::slice::from_raw_parts(num_app_ptr.add(1), num_app + 1);
app_start[..=num_app].copy_from_slice(app_start_raw);
所以这里先是指向获取到数据段上的app地址数组,再把个数组拷过来
在load_app中,系统将程序加载到了0x80400000的位置,与编译程序时使用的base addr一致。但在上一小节的评论区有人指出,rust编译出来的代码为位置无关码,因此load app中的base addr即使与编译时的base addr不同,只要指令本身位置无关,并不会产生影响。
(然后在本章评论中作者又指出我们生成的代码是位置有关的。。。什么情况捏)
但又指出如果修改编译时的base addr,会导致bss段的两个符号位置改变,在clear bss时可能破坏程序数据。但如果bss段的符号在编译时就确定且导出到符号表的话,在运行时如果加载位置存在偏差可能导致符号指向的位置存在问题
试着魔改了一下ch1的os加载地址,完全不行,因为rustsbi默认跳到0x80200000,即使修改启动参数也不行,所以内核被加载到另一个地址就算位置无关但pc指不到入口。并且qemu那个加载也怪怪的,在编译地址和加载地址不一致的时候并不是想象中简单的把整个二进制文件移到对应地址,会出现问题。好怪
实现特权级的切换
主要看一下汇编,指令都忘了sd ra imm(sp)
该指令将ra的值放入栈顶指针sp加上立即数偏移imm的位置
这里手写汇编保存寄存器,由于栈是往低地址生长的,所以入栈是往高地址加
这里一共x0-x31 32个寄存器+两个状态sstatus/sepc,因此开了34个空间。x0不存,所以初始偏移直接从1*8开始
等于说在内核栈上手搓了一个TrapContext实例,然后塞进a0里面寄存器传参调用trap_handler。。。真是狂野又原始的编程风格。大概call这个伪指令会完成实际调用时的栈指针移动8
在__restore
处把当做参数的a0塞回了sp,文档中未给出解释,估计是因为这个结构体是手搓入栈的,导致正常栈结构不太对,call一轮正常入栈出栈导致栈顶指针位置歪了吧?然后正常恢复寄存器,栈顶移回去,把内核栈与用户栈指针换回来,完成
有兴趣的同学可以思考: sscratch 是何时被设置为内核栈顶的?
评论下面给出了答案,但仍然略微的有些抽象,内核栈顶的指针在内核初始化时就已经被设定为sp了,在远古时期写的entry.asm中设定的,但实际上我们使用的内核栈数据结果和初始asm中在汇编里定下的bss段的栈是一个栈吗?
似乎不一定。前文提到了文档中未解释的mv sp, a0
,由于程序初始化时新建了一个初始context恢复给用户态,并将该context放置在后续设定的kernel stack上,而push_context函数将context作为返回值,寄存器传参的情况下,a0就变成了指向后续定义的内核栈的指针,成功一波偷梁换柱,将asm中的栈指向了自定义的栈。
翻了下评论,挺多说这个问题的,作者也有给出解释
64KiB的空间是启动栈,仅在内核初始化自身,也就是调用rust_main函数直到task::run_first_task之前由内核使用。在此之后应用使用UserStack而内核使用KernelStack,启动栈不再被使用。
还有很多小朋友问用户栈没看到这个代码用?用户栈内核用锤子,用户空间自己调用函数的时候用的。栈又不需要在编译时确定,运行的时候给片空间给个栈指针就能自动加加减减
tutorial中没有提到app的生成情况,在os目录下有一个build.rs写了怎么样把user目录下的程序编译成目标汇编并导出符号的。在os的main.rs的开头引入了link_app.S,然后靠导出的符号确定内存中的位置
总结一下批处理系统的运行逻辑?
相较于第一章整个程序之间和操作系统编译到一起运行,这回起码抽出来了一个内核,然后系统调用部分以库函数的形式和用户程序一起编译,内核则实现了简单的隔离和特权。也就实现了一次多读几个程序进来一个完了立即处理下一个。程序出了问题也能及时杀掉运行下一个,保证了效率。
特权的隔离想了半天没想通。因为这里操作系统的最终实现还是一层ecall发起系统调用,感觉似乎用户程序也能写一模一样的汇编绕过操作系统和硬件交互
所以这里有这样子的一个图
environment call flow
实际上ecall应该会根据当前所在的特权级提升特权,最终的硬件交互可能需要提升至machine级别才能完成执行,而用户运行在user下,直接手搓的ecall也只能抵达操作系统handler的trap中
但是指向trap的寄存器stvec只有一个,也许machine级别的代码有另一个地方会存储M级的trap handler吧
应当是所有可能危害操作系统和其他应用程序的指令均在硬件层面只允许内核态使用,确保用户态即使在知道交互的方法时也无法直接手搓汇编完成访问。同时,用户态程序可以通过手搓汇编的方法绕过操作系统提供的系统调用(比如user中直接搓汇编调用而不是调用sys_write,这样一来如果在sys_write中添加检查就会无效,比如windows下经典的syswhispher就能在这个程度上过掉库函数层面的hook),但无论如何ecall会进入trap,且用户态无法改变stvec
指向的trap处理程序,而trap又会将控制流移交操作系统,操作系统在trap后实现安全检查即可保证安全性。
其实就是划分了权限的高低级,低权限代码写的再花反正硬件规定了这些操作权限不够做不了,只能去找高权限的内核做,而高权限的内核只提供几个不会破坏系统安全的功能(系统调用)出来,确保整个系统能平稳运行
但是操作系统应当确保进程间的隔离,现在这个原始系统完全没有完成隔离,导致用户程序可以随意读写内核和其他程序的数据。。。
练习
伏笔回收,在练习里面就提到了这个内存没有隔离的问题。但是也只是简单的限制系统调用而已。。。毕竟没有虚拟内存的话,都不需要系统调用,就把所有数据当自己的读写就行了。不过实现了虚拟内存之后,物理内存读取做好隔离就能防住了吧
只需要把用户栈的范围和用户APP的范围搞出来,然后在系统调用的时候检查一下就行了
但是rust垃圾并不知道怎么样跨文件使用全局变量。。。并且user stack的get sp是作为结构体的方法。。。rust垃圾只能自己重新在batch.rs里面写了一堆pub的获取地址的函数。。。
pub fn get_user_stack_range() -> (usize,usize){
(USER_STACK.data.as_ptr() as usize, USER_STACK.data.as_ptr() as usize + USER_STACK_SIZE)
}
pub fn get_current_app_range() -> (usize, usize){
let mut app_manager = APP_MANAGER.exclusive_access();
let i = app_manager.current_app - 1;
(app_manager.app_start[i], app_manager.app_start[i+1])
}
check
let start = buf as usize;
let end = start + len;
let (stack_top, stack_bottom) = crate::batch::get_user_stack_range();
let (app_bottom, app_top) = crate::batch::get_current_app_range();
if !(start > stack_top && end < stack_bottom ||
start > app_bottom && end < app_top){
return -1;
}
寄存器都是usize,应该不会给length搓出来个负数吧。。。
git checkout ch2-lab
切到实验代码,但是这里面rust-toolchain那个文件版本回退了,是21年的,编译之后asm那块超级报错。看评论区换成ch1的22年版,然后原来用的#![feature(asm)]
也屁用没有,全部手动加use core::arch::asm/global_asm;
启动的make run TEST=1
简单看了一下,就是通配test$(TEST)*
的文件编译加载,所以能把两个test的输出文件编译加载进去
但是好像跑不起来。。。检查了半天发现了问题,在操作系统中定义的用户栈长度是0x2000,而write0中定义的用户栈长度为0x1000,导致write0认为的溢出了的栈在我这边是没有溢出的。。。
把检测时返回的用户栈范围变成0x1000即可
pub fn get_user_stack_range() -> (usize,usize){
(USER_STACK.data.as_ptr() as usize, USER_STACK.data.as_ptr() as usize + 0x1000)
}
pub fn get_current_app_range() -> (usize, usize){
let mut app_manager = APP_MANAGER.exclusive_access();
let index = app_manager.current_app-1;
(app_manager.app_start[index], app_manager.app_start[index+1])
}
current_app那里要减一是因为load app的时候就加了一,需要减一复原回去