rCore 学习笔记 - 第二章

系统调用

rCore 的系统调用格式与 Linux 相同。第二章里首先实现了两个系统调用,sys_writesys_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 段的写入没有保护,就造成了这种结果。虽然很奇怪,但毕竟跑起来了。不知道后面会不会解决这个问题,先记录下来吧。


rCore 学习笔记 - 第二章
http://xiao-h.com/2025/01/12/rCore-0x02/
作者
小H
发布于
2025年1月12日
许可协议