|
8 | 8 | 本节导读 |
9 | 9 | ------------------------------------ |
10 | 10 |
|
11 | | -本节我们将进行构建“三叶虫”操作系统的最后一个步骤,即基于 RustSBI 提供的服务完成在屏幕上打印 ``Hello world!`` 和关机操作。事实上,作为对我们之前提到的 :ref:`应用程序执行环境 <app-software-stack>` 的细化,RustSBI 介于底层硬件和内核之间,是我们内核的底层执行环境。本节将会提到执行环境除了为上层应用进行初始化的第二种职责:即在上层应用运行时提供服务。本节的代码涉及的汇编和 Rust 的细节较多,不必完全理解其含义,重点在于将内核成功运行起来。 |
| 11 | +本节我们将进行构建“三叶虫”操作系统的最后一个步骤,即基于 RustSBI 提供的服务完成在屏幕上打印 ``Hello world!`` 和关机操作。事实上,作为对我们之前提到的 :ref:`应用程序执行环境 <app-software-stack>` 的细化,RustSBI 介于底层硬件和内核之间,是我们内核的底层执行环境。本节将会提到执行环境除了为上层应用进行初始化的第二种职责:即在上层应用运行时提供服务。 |
12 | 12 |
|
13 | 13 | 使用 RustSBI 提供的服务 |
14 | 14 | ------------------------------------------ |
15 | 15 |
|
16 | | -之前我们对 RustSBI 的了解仅限于它会在计算机启动时进行它所负责的环境初始化工作,并将计算机控制权移交给内核。但实际上作为内核的执行环境,它还有另一项职责:即在内核运行时响应内核的请求为内核提供服务。当内核发出请求时,计算机会转由 RustSBI 控制来响应内核的请求,待请求处理完毕后,计算机控制权会被交还给内核。从内存布局的角度来思考,每一层执行环境(或称软件栈)都对应到内存中的一段代码和数据,这里的控制权转移指的是 CPU 从执行一层软件的代码到执行另一层软件的代码的过程。这个过程和函数调用比较像,但是内核无法通过函数调用来请求 RustSBI 提供的服务,这是因为内核并没有和 RustSBI 链接到一起,我们仅仅使用 RustSBI 构建后的可执行文件,因此内核对于 RustSBI 的符号一无所知。事实上,内核需要通过另一种复杂的方式来“调用” RustSBI 的服务: |
| 16 | +之前我们对 RustSBI 的了解仅限于它会在计算机启动时进行它所负责的环境初始化工作,并将计算机控制权移交给内核。但实际上作为内核的执行环境,它还有另一项职责:即在内核运行时响应内核的请求为内核提供服务。当内核发出请求时,计算机会转由 RustSBI 控制来响应内核的请求,待请求处理完毕后,计算机控制权会被交还给内核。从内存布局的角度来思考,每一层执行环境(或称软件栈)都对应到内存中的一段代码和数据,这里的控制权转移指的是 CPU 从执行一层软件的代码到执行另一层软件的代码的过程。这个过程与我们使用高级语言编程时调用库函数比较类似。 |
17 | 17 |
|
18 | | -.. _term-llvm-sbicall: |
| 18 | +这里展开介绍一些相关术语:从第二章将要讲到的 :ref:`RISC-V 特权级架构 <riscv-priv-arch>` 的视角来看,我们编写的 OS 内核位于 Supervisor 特权级,而 RustSBI 位于 Machine 特权级,也是最高的特权级。类似 RustSBI 这样运行在 Machine 特权级的软件被称为 Supervisor Execution Environment(SEE),即 Supervisor 执行环境。两层软件之间的接口被称为 Supervisor Binary Interface(SBI),即 Supervisor 二进制接口。 `SBI Specification <https://github.com/riscv-non-isa/riscv-sbi-doc>`_ (简称 SBI spec)规定了 SBI 接口层要包含哪些功能,该标准由 RISC-V 开源社区维护。RustSBI 按照 SBI spec 标准实现了需要支持的大多数功能,但 RustSBI 并不是 SBI 标准的唯一一种实现,除此之外还有社区中的前辈 OpenSBI 等等。 |
19 | 19 |
|
20 | | -.. code-block:: rust |
21 | | - :linenos: |
| 20 | +目前, SBI spec 已经发布了 v2.0-rc8 版本,但本教程基于 2023 年 3 月份发布的 `v1.0.0 版本 <https://github.com/riscv-non-isa/riscv-sbi-doc/releases/download/v1.0.0/riscv-sbi.pdf>`_ 。我们可以来看看里面约定了 SEE 要向 OS 内核提供哪些功能,并寻找我们本节所需的打印到屏幕和关机的接口。可以看到从 Chapter 4 开始,每一章包含了一个 SBI 拓展(Chapter 5 包含多个 Legacy Extension),代表一类功能接口,这有点像 RISC-V 指令集的 IMAFD 等拓展。每个 SBI 拓展还包含若干子功能。其中: |
22 | 21 |
|
23 | | - // os/src/main.rs |
24 | | - mod sbi; |
| 22 | +- Chapter 5 列出了若干 SBI 遗留接口,其中包括串口的写入(正是我们本节所需要的)和读取接口,分别位于 5.2 和 5.3 小节。在教程第九章我们自己实现串口外设驱动之前,与串口的交互都是通过这两个接口来进行的。顺带一提,第三章开始还会用到 5.1 小节介绍的 set timer 接口。 |
| 23 | +- Chapter 10 包含了若干系统重启相关的接口,我们本节所需的关机接口也在其中。 |
25 | 24 |
|
26 | | - // os/src/sbi.rs |
27 | | - use core::arch::asm; |
28 | | - #[inline(always)] |
29 | | - fn sbi_call(which: usize, arg0: usize, arg1: usize, arg2: usize) -> usize { |
30 | | - let mut ret; |
31 | | - unsafe { |
32 | | - asm!( |
33 | | - "ecall", |
34 | | - inlateout("x10") arg0 => ret, |
35 | | - in("x11") arg1, |
36 | | - in("x12") arg2, |
37 | | - in("x17") which, |
38 | | - ); |
39 | | - } |
40 | | - ret |
41 | | - } |
| 25 | +内核应该如何调用 RustSBI 提供的服务呢?通过函数调用是行不通的,因为内核并没有和 RustSBI 链接到一起,我们仅仅使用 RustSBI 构建后的可执行文件,因此内核无从得知 RustSBI 中的符号或地址。幸而, RustSBI 开源社区的 `sbi_rt <https://github.com/rustsbi/sbi-rt>`_ 封装了调用 SBI 服务的接口,我们直接使用即可。首先,我们在 ``Cargo.toml`` 中引入 sbi_rt 依赖: |
| 26 | + |
| 27 | +.. code-block:: |
| 28 | + :linenos: |
42 | 29 |
|
43 | | -我们将内核与 RustSBI 通信的相关功能实现在子模块 ``sbi`` 中,因此我们需要在 ``main.rs`` 中加入 ``mod sbi`` 将该子模块加入我们的项目。在 ``os/src/sbi.rs`` 中,我们首先关注 ``sbi_call`` 的函数签名, ``which`` 表示请求 RustSBI 的服务的类型(RustSBI 可以提供多种不同类型的服务), ``arg0`` ~ ``arg2`` 表示传递给 RustSBI 的 3 个参数,而 RustSBI 在将请求处理完毕后,会给内核一个返回值,这个返回值也会被 ``sbi_call`` 函数返回。尽管我们还不太理解函数 ``sbi_call`` 的具体实现,但目前我们已经知道如何使用它了:当需要使用 RustSBI 服务的时候调用它就行了。 |
| 30 | + // os/Cargo.toml |
| 31 | + [dependencies] |
| 32 | + sbi-rt = { version = "0.0.2", features = ["legacy"] } |
44 | 33 |
|
45 | | -在 ``sbi.rs`` 中我们定义 RustSBI 支持的服务类型常量,它们并未被完全用到: |
| 34 | +这里需要带上 ``legacy`` 的 feature,因为我们需要用到的串口读写接口都属于 SBI 的遗留接口。 |
46 | 35 |
|
47 | | -.. code-block:: rust |
48 | | - :linenos: |
| 36 | +.. _term-llvm-sbicall: |
49 | 37 |
|
50 | | - // os/src/sbi.rs |
51 | | - #![allow(unused)] // 此行请放在该文件最开头 |
52 | | - const SBI_SET_TIMER: usize = 0; |
53 | | - const SBI_CONSOLE_PUTCHAR: usize = 1; |
54 | | - const SBI_CONSOLE_GETCHAR: usize = 2; |
55 | | - const SBI_CLEAR_IPI: usize = 3; |
56 | | - const SBI_SEND_IPI: usize = 4; |
57 | | - const SBI_REMOTE_FENCE_I: usize = 5; |
58 | | - const SBI_REMOTE_SFENCE_VMA: usize = 6; |
59 | | - const SBI_REMOTE_SFENCE_VMA_ASID: usize = 7; |
60 | | - const SBI_SHUTDOWN: usize = 8; |
61 | | -
|
62 | | -如字面意思,服务 ``SBI_CONSOLE_PUTCHAR`` 可以用来在屏幕上输出一个字符。我们将这个功能封装成 ``console_putchar`` 函数: |
| 38 | +我们将内核与 RustSBI 通信的相关功能实现在子模块 ``sbi`` 中,因此我们需要在 ``main.rs`` 中加入 ``mod sbi`` 将该子模块加入我们的项目。在 ``os/src/sbi.rs`` 中,我们直接调用 sbi_rt 提供的接口来将输出字符: |
63 | 39 |
|
64 | 40 | .. code-block:: rust |
65 | 41 | :linenos: |
66 | 42 |
|
67 | 43 | // os/src/sbi.rs |
68 | 44 | pub fn console_putchar(c: usize) { |
69 | | - sbi_call(SBI_CONSOLE_PUTCHAR, c, 0, 0); |
| 45 | + #[allow(deprecated)] |
| 46 | + sbi_rt::legacy::console_putchar(c); |
70 | 47 | } |
71 | 48 |
|
72 | | -注意我们并未使用 ``sbi_call`` 的返回值,因为它并不重要。如果同学们有兴趣的话,可以试着在 ``rust_main`` 中调用 ``console_putchar`` 来在屏幕上输出 ``OK`` 。接着在 Qemu 上运行一下,我们便可看到由我们自己输出的第一条 log 了。 |
| 49 | +注意我们为了简单起见并未用到 ``sbi_call`` 的返回值,有兴趣的同学可以在 SBI spec 中查阅 SBI 服务返回值的含义。到这里,同学们可以试着在 ``rust_main`` 中调用 ``console_putchar`` 来在屏幕上输出 ``OK`` 。接着在 Qemu 上运行一下,我们便可看到由我们自己输出的第一条 log 了。 |
73 | 50 |
|
74 | | -类似上述方式,我们还可以将关机服务 ``SBI_SHUTDOWN`` 封装成 ``shutdown`` 函数: |
| 51 | +同样,我们再来实现关机功能: |
75 | 52 |
|
76 | 53 | .. code-block:: rust |
77 | 54 | :linenos: |
78 | 55 |
|
79 | 56 | // os/src/sbi.rs |
80 | | - pub fn shutdown() -> ! { |
81 | | - sbi_call(SBI_SHUTDOWN, 0, 0, 0); |
82 | | - panic!("It should shutdown!"); |
| 57 | + pub fn shutdown(failure: bool) -> ! { |
| 58 | + use sbi_rt::{system_reset, NoReason, Shutdown, SystemFailure}; |
| 59 | + if !failure { |
| 60 | + system_reset(Shutdown, NoReason); |
| 61 | + } else { |
| 62 | + system_reset(Shutdown, SystemFailure); |
| 63 | + } |
| 64 | + unreachable!() |
83 | 65 | } |
84 | 66 |
|
| 67 | +这里的参数 ``failure`` 表示系统是否正常退出。更多内容可以参阅 SBI spec 的 Chapter 10。 |
| 68 | + |
| 69 | +.. note:: **sbi_rt 是如何调用 SBI 服务的** |
| 70 | + |
| 71 | + SBI spec 的 Chapter 3 介绍了服务的调用方法:只需将要调用功能的拓展 ID 和功能 ID 分别放在 ``a7`` 和 ``a6`` 寄存器中,并按照 RISC-V 调用规范将参数放置在其他寄存器中,随后执行 ``ecall`` 指令即可。这会将控制权转交给 RustSBI 并由 RustSBI 来处理请求,处理完成后会将控制权交还给内核。返回值会被保存在 ``a0`` 和 ``a1`` 寄存器中。在本书的第二章中,我们会手动编写汇编代码来实现类似的过程。 |
| 72 | + |
85 | 73 | 实现格式化输出 |
86 | 74 | ----------------------------------------------- |
87 | 75 |
|
|
213 | 201 | } else { |
214 | 202 | println!("Panicked: {}", info.message().unwrap()); |
215 | 203 | } |
216 | | - shutdown() |
| 204 | + shutdown(true) |
217 | 205 | } |
218 | 206 |
|
219 | | -我们尝试打印更加详细的信息,包括 panic 所在的源文件和代码行数。我们尝试从传入的 ``PanicInfo`` 中解析这些信息,如果解析成功的话,就和 panic 的报错信息一起打印出来。我们需要在 ``main.rs`` 开头加上 ``#![feature(panic_info_message)]`` 才能通过 ``PanicInfo::message`` 获取报错信息。当打印完毕之后,我们直接调用 ``shutdown`` 函数关机。 |
| 207 | +我们尝试打印更加详细的信息,包括 panic 所在的源文件和代码行数。我们尝试从传入的 ``PanicInfo`` 中解析这些信息,如果解析成功的话,就和 panic 的报错信息一起打印出来。我们需要在 ``main.rs`` 开头加上 ``#![feature(panic_info_message)]`` 才能通过 ``PanicInfo::message`` 获取报错信息。当打印完毕之后,我们直接调用 ``shutdown`` 函数关机,由于系统是异常 panic 关机的,参数 ``failure`` 应为 ``true`` 。 |
220 | 208 |
|
221 | 209 | 为了测试我们的实现是否正确,我们将 ``rust_main`` 改为: |
222 | 210 |
|
|
239 | 227 | Hello, world! |
240 | 228 | Panicked at src/main.rs:26 Shutdown machine! |
241 | 229 |
|
242 | | -可以看到,panic 所在的源文件和代码行数被正确报告,这将为我们后续章节的开发和调试带来很大方便。到这里,我们就实现了一个可以在Qemu模拟的计算机上运行的裸机应用程序,其具体内容就是上述的 `rust_main`函数,而其他部分,如 `entry.asm` 、 `lang_items.rs` 、`console.rs` 、 `sbi.rs` 则形成了支持裸机应用程序的寒武纪“三叶虫”操作系统 -- LibOS 。 |
| 230 | +可以看到,panic 所在的源文件和代码行数被正确报告,这将为我们后续章节的开发和调试带来很大方便。到这里,我们就实现了一个可以在Qemu模拟的计算机上运行的裸机应用程序,其具体内容就是上述的 `rust_main` 函数,而其他部分,如 `entry.asm` 、 `lang_items.rs` 、`console.rs` 、 `sbi.rs` 则形成了支持裸机应用程序的寒武纪“三叶虫”操作系统 -- LibOS 。 |
243 | 231 |
|
244 | 232 | .. note:: |
245 | 233 |
|
|
0 commit comments