系统调用
rCore 的系统调用格式与 Linux 相同。第二章里首先实现了两个系统调用,sys_write
和 sys_exit
,分别用于将数据写入文件和退出应用程序。通过 syscall
函数,按照规定的格式向特定寄存器存值,通过 ecall
触发 Trap,然后在内核中处理。
在用户程序中,可以发现 console.rs
有了些许不同,先前在内核中是借助了 RustSBI 来实现输出字符,而在这里是借助了系统调用。在调用的时候实际上是将字符串拆成了若干 byte
,对各个 byte
,借助系统调用 sys_write
来输出。
构建脚本
当前的系统是构建完用户代码,然后再将生成的 bin 文件以嵌入的方式,和内核一起构建的。这一步是借助构建脚本 build.rs 完成的,在文档中没有提及实现,这里我简单地看一下是如何完成的。
构建脚本将会监控两个目录是否发生变化:../user/src/
和 ../user/build/bin/
,如果发生变化就会生成相应代码,这是在 insert_app_data
函数中完成的。
1 2 3 4 5 6 7 8 9
| let mut apps: Vec<_> = read_dir("../user/build/bin/") .unwrap() .into_iter() .map(|dir_entry| { let mut name_with_ext = dir_entry.unwrap().file_name().into_string().unwrap(); name_with_ext.drain(name_with_ext.find('.').unwrap()..name_with_ext.len()); name_with_ext }) .collect();
|
关键在于这一步,先是读取了目录,然后进行 unwrap
,如果失败会 panic,接着转为迭代器,再进行处理,最后收集到 Vec
中。处理的步骤先是获取文件名,转换成字符串,然后移除扩展名,返回结果。
所有用户程序按名称排序后开始生成 link_app.S
,第一行 .align 3
指明按八字节对齐,这是由于后面 .quad
存储八字节数据的需要。
执行流程
捋一下执行流程:
在进入内核后,先是清除 .bss 段,然后用 trap::init()
设置 trap 的处理函数,接下来当用户程序执行 ecall
,就会自动跳转到 __alltraps
了。由于 lazy_static
,加载用户程序是在 batch::init()
这一步完成的。
通过 batch::run_next_app
,读取了第一个 app 并调用 __restore
函数。调用 __restore
函数前,先向内核栈压入了跳转到第一个程序并执行所需的上下文,__restore
函数将会把上下文从内核栈中恢复过来(虽然第一次其实寄存器都是空的),并调用 sret
进入 U 模式。
当用户程序需要用到系统调用时,会触发 __alltraps
,__alltraps
先保存了用户程序的上下文(上下文在 Rust 代码中的类型为 TrapContext
) ,然后调用trap_handler
处理 trap。根据不同 trap,trap_handler
会有不同行为,比如出错就运行下一个应用,如果是系统调用就调用 syscall
函数。syscall
函数根据不同的系统调用类型,执行不同的系统调用。结束后将会继续执行 __alltrap
下面的代码,也就是 __restore
,由此回到用户程序中。
当用户程序执行完,或者出错,就会进入下一个用户程序,直到结束,退出。
作业实现
作业实现方面,由于对内容不够熟悉,还是踩了不少坑的,这里记录一下实现过程。
首先考虑什么样的地址是合法的,可以想到,在用户程序内的地址显然是合法的,在 sys_write
系统调用中可以检查一下内存范围是否合法。写完,一运行,发现没输出了。于是打印了一些调试信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| [kernel] num_app = 7 [kernel] app_0 [0x80209048, 0x80209f78) [kernel] app_1 [0x80209f78, 0x8020af40) [kernel] app_2 [0x8020af40, 0x8020c210) [kernel] app_3 [0x8020c210, 0x8020d1c0) [kernel] app_4 [0x8020d1c0, 0x8020e058) [kernel] app_5 [0x8020e058, 0x8020f60a) [kernel] app_6 [0x8020f60a, 0x80210cbc) [kernel] Loading app_0 [ INFO] app_src: [0x80209048, 0x80209f78) [ INFO] app_dst: [0x80400000, 0x80400f30) range: [0x80209f78,0x8020af40), buffer_addr: 0x80400cc8, size: 0xe out of range! [kernel] Loading app_1 [ INFO] app_src: [0x80209f78, 0x8020af40) [ INFO] app_dst: [0x80400000, 0x80400fc8) range: [0x8020af40,0x8020c210), buffer_addr: 0x80400cf0, size: 0x44 out of range! range: [0x8020af40,0x8020c210), buffer_addr: 0x80400d48, size: 0x25 out of range!
|
这里先是踩了第一个坑,在 batch::run_next_app
中,加载完当前用户程序后,current_app
会被设置为下一个用户程序的 id,所以获取范围时,当前的 id 要用 manager.get_current_app()
减去一。
改完之后,大部分程序都能运行了,只有 02power.rs
没法正常运行,数字全没了,符号都还在。继续打印调试信息:
1 2 3 4 5 6 7 8 9 10 11
| [kernel] Loading app_2 [ INFO] app_src: [0x8020af40, 0x8020c210) [ INFO] app_dst: [0x80400000, 0x804012d0) [ INFO] UserStack: [0x80207000, 0x80208000), l: 0x80206ddf, r: 0x80206de0 ^[ INFO] UserStack: [0x80207000, 0x80208000), l: 0x80206ddb, r: 0x80206de0 =[ INFO] UserStack: [0x80207000, 0x80208000), l: 0x80206ddc, r: 0x80206de0 (MOD [ INFO] UserStack: [0x80207000, 0x80208000), l: 0x80206ddb, r: 0x80206de0 ) [ INFO] UserStack: [0x80207000, 0x80208000), l: 0x80206ddf, r: 0x80206de0 ^[ INFO] UserStack: [0x80207000, 0x80208000), l: 0x80206ddb, r: 0x80206de0 =[ INFO] UserStack: [0x80207000, 0x80208000), l: 0x80206ddc, r: 0x80206de0
|
可以观察到,byte
的内存地址的范围有的时候在 0x802xxxxx
,有的时候在 0x804xxxxx
,在 0x804xxxxx
的字符都能正常输出,而在 0x802xxxxx
的都没了。这些数字作为函数中的变量,应当是存在了用户栈中。修改后,发现还是不正确。我打印了用户栈和内核栈的范围,发现内核栈竟然包含了用户栈?这里踩了第二个坑:get_sp
的地址是栈顶的,栈顶的内存地址是大的,而我获取栈的范围时反而加了 USER_STACK_SIZE
。改完这个,就获得了正确的结果。
不过在调试的时候我还是发现了一点问题。我最初尝试定位字符的范围是在内存的哪个段,于是用 readelf
查看了内核的 image,结果是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 节头: [号] 名称 类型 地址 偏移量 大小 全体大小 旗标 链接 信息 对齐 [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .text PROGBITS 0000000080200000 00001000 00000000000030aa 0000000000000000 AX 0 0 4 [ 2] .rodata PROGBITS 0000000080204000 00005000 00000000000055b0 0000000000000000 AM 0 0 4096 [ 3] .data PROGBITS 000000008020a000 0000b000 0000000000007cd0 0000000000000000 WA 0 0 8 [ 4] .bss NOBITS 0000000080212000 00012cd0 00000000000100b8 0000000000000000 WA 0 0 8 [ 5] .comment PROGBITS 0000000000000000 00012cd0 0000000000000093 0000000000000001 MS 0 0 1 [ 6] .riscv.attributes RISCV_ATTRIBUTE 0000000000000000 00012d63 0000000000000047 0000000000000000 0 0 1 [ 7] .symtab SYMTAB 0000000000000000 00012db0 0000000000019290 0000000000000018 9 4215 8 [ 8] .shstrtab STRTAB 0000000000000000 0002c040 000000000000004f 0000000000000000 0 0 1 [ 9] .strtab STRTAB 0000000000000000 0002c08f 00000000000094d4 0000000000000000 0 0 1
|
可以看到,栈位于 .rodata
段,这很奇怪,按道理 .rodata
段是只读的,不应该放可变的栈。
问题出在这里:
1 2 3 4 5 6
| static KERNEL_STACK: KernelStack = KernelStack { data: [0; KERNEL_STACK_SIZE], }; static USER_STACK: UserStack = UserStack { data: [0; USER_STACK_SIZE], };
|
这里是用了不可变的变量,所以被分配到了 .rodata
段,外加对 .rodata
段的写入没有保护,就造成了这种结果。虽然很奇怪,但毕竟跑起来了。不知道后面会不会解决这个问题,先记录下来吧。