Skip to content

Commit 04f4c21

Browse files
committed
fix issue #197 ch1 part
1 parent c434e44 commit 04f4c21

File tree

2 files changed

+39
-49
lines changed

2 files changed

+39
-49
lines changed

source/chapter1/6print-and-shutdown-based-on-sbi.rst

Lines changed: 37 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -8,80 +8,68 @@
88
本节导读
99
------------------------------------
1010

11-
本节我们将进行构建“三叶虫”操作系统的最后一个步骤,即基于 RustSBI 提供的服务完成在屏幕上打印 ``Hello world!`` 和关机操作。事实上,作为对我们之前提到的 :ref:`应用程序执行环境 <app-software-stack>` 的细化,RustSBI 介于底层硬件和内核之间,是我们内核的底层执行环境。本节将会提到执行环境除了为上层应用进行初始化的第二种职责:即在上层应用运行时提供服务。本节的代码涉及的汇编和 Rust 的细节较多,不必完全理解其含义,重点在于将内核成功运行起来。
11+
本节我们将进行构建“三叶虫”操作系统的最后一个步骤,即基于 RustSBI 提供的服务完成在屏幕上打印 ``Hello world!`` 和关机操作。事实上,作为对我们之前提到的 :ref:`应用程序执行环境 <app-software-stack>` 的细化,RustSBI 介于底层硬件和内核之间,是我们内核的底层执行环境。本节将会提到执行环境除了为上层应用进行初始化的第二种职责:即在上层应用运行时提供服务。
1212

1313
使用 RustSBI 提供的服务
1414
------------------------------------------
1515

16-
之前我们对 RustSBI 的了解仅限于它会在计算机启动时进行它所负责的环境初始化工作,并将计算机控制权移交给内核。但实际上作为内核的执行环境,它还有另一项职责:即在内核运行时响应内核的请求为内核提供服务。当内核发出请求时,计算机会转由 RustSBI 控制来响应内核的请求,待请求处理完毕后,计算机控制权会被交还给内核。从内存布局的角度来思考,每一层执行环境(或称软件栈)都对应到内存中的一段代码和数据,这里的控制权转移指的是 CPU 从执行一层软件的代码到执行另一层软件的代码的过程。这个过程和函数调用比较像,但是内核无法通过函数调用来请求 RustSBI 提供的服务,这是因为内核并没有和 RustSBI 链接到一起,我们仅仅使用 RustSBI 构建后的可执行文件,因此内核对于 RustSBI 的符号一无所知。事实上,内核需要通过另一种复杂的方式来“调用” RustSBI 的服务:
16+
之前我们对 RustSBI 的了解仅限于它会在计算机启动时进行它所负责的环境初始化工作,并将计算机控制权移交给内核。但实际上作为内核的执行环境,它还有另一项职责:即在内核运行时响应内核的请求为内核提供服务。当内核发出请求时,计算机会转由 RustSBI 控制来响应内核的请求,待请求处理完毕后,计算机控制权会被交还给内核。从内存布局的角度来思考,每一层执行环境(或称软件栈)都对应到内存中的一段代码和数据,这里的控制权转移指的是 CPU 从执行一层软件的代码到执行另一层软件的代码的过程。这个过程与我们使用高级语言编程时调用库函数比较类似。
1717

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 等等。
1919

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 拓展还包含若干子功能。其中:
2221

23-
// os/src/main.rs
24-
mod sbi;
22+
- Chapter 5 列出了若干 SBI 遗留接口,其中包括串口的写入(正是我们本节所需要的)和读取接口,分别位于 5.2 和 5.3 小节。在教程第九章我们自己实现串口外设驱动之前,与串口的交互都是通过这两个接口来进行的。顺带一提,第三章开始还会用到 5.1 小节介绍的 set timer 接口。
23+
- Chapter 10 包含了若干系统重启相关的接口,我们本节所需的关机接口也在其中。
2524

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:
4229
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"] }
4433
45-
``sbi.rs`` 中我们定义 RustSBI 支持的服务类型常量,它们并未被完全用到:
34+
这里需要带上 ``legacy`` 的 feature,因为我们需要用到的串口读写接口都属于 SBI 的遗留接口。
4635

47-
.. code-block:: rust
48-
:linenos:
36+
.. _term-llvm-sbicall:
4937

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 提供的接口来将输出字符:
6339

6440
.. code-block:: rust
6541
:linenos:
6642
6743
// os/src/sbi.rs
6844
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);
7047
}
7148
72-
注意我们并未使用 ``sbi_call`` 的返回值,因为它并不重要。如果同学们有兴趣的话,可以试着在 ``rust_main`` 中调用 ``console_putchar`` 来在屏幕上输出 ``OK`` 。接着在 Qemu 上运行一下,我们便可看到由我们自己输出的第一条 log 了。
49+
注意我们为了简单起见并未用到 ``sbi_call`` 的返回值,有兴趣的同学可以在 SBI spec 中查阅 SBI 服务返回值的含义。到这里,同学们可以试着在 ``rust_main`` 中调用 ``console_putchar`` 来在屏幕上输出 ``OK`` 。接着在 Qemu 上运行一下,我们便可看到由我们自己输出的第一条 log 了。
7350

74-
类似上述方式,我们还可以将关机服务 ``SBI_SHUTDOWN`` 封装成 ``shutdown`` 函数
51+
同样,我们再来实现关机功能
7552

7653
.. code-block:: rust
7754
:linenos:
7855
7956
// 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!()
8365
}
8466
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+
8573
实现格式化输出
8674
-----------------------------------------------
8775

@@ -213,10 +201,10 @@
213201
} else {
214202
println!("Panicked: {}", info.message().unwrap());
215203
}
216-
shutdown()
204+
shutdown(true)
217205
}
218206
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``
220208

221209
为了测试我们的实现是否正确,我们将 ``rust_main`` 改为:
222210

@@ -239,7 +227,7 @@
239227
Hello, world!
240228
Panicked at src/main.rs:26 Shutdown machine!
241229
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 。
243231

244232
.. note::
245233

source/chapter2/1rv-privilege.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@
4040
读者可能会好奇一共有多少种不同的特权级,在不同的指令集体系结构中特权级的数量也是不同的。x86 和 RISC-V 设计了多达 4 种特权级,而对于一般的操作系统而言,其实只要两种特权级就够了。
4141

4242

43+
.. _riscv-priv-arch:
44+
4345
RISC-V 特权级架构
4446
------------------------------------------
4547

0 commit comments

Comments
 (0)