提交 8f3b3394 编写于 作者: chyyuu1972's avatar chyyuu1972

merge main branch in 20210326

build/
.vscode/
.idea
source/_build/
......@@ -19,7 +19,15 @@ help:
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
view:
make html && firefox build/html/index.html
deploy:
@make clean
@make html
@rm -rf docs
@cp -r build/html docs
@touch docs/.nojekyll
@git add -A
@git commit -m "Deploy"
@git push
# rCore-Tutorial-Book-v3
Documentation of rCore-Tutorial version 3 in Chinese.
Deployed version can be found [here](https://rcore-os.github.io/rCore-Tutorial-Book-v3/).
If you cannot access `github.io` normally due to network problems, please visit the [synchronized version](http://wyfcyx.gitee.io/rcore-tutorial-book-v3) hosted on gitee.
## Todo List
- [x] code tree in introduction
- [ ] rust module system in chapter1
- [x] update rustsbi to 0.1.1
make clean && make html && google-chrome build/html/index.html
# Sphinx build info version 1
# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
config: 6e307cd3ba0daae86ac8cbb85a0d88aa
tags: 645f666f9bcd5a90fca523b33c5a78b7
Rust 快速入门
=============================
.. toctree::
:hidden:
:maxdepth: 4
- `Stanford 新开的一门很值得学习的 Rust 入门课程 <https://reberhardt.com/cs110l/spring-2020/>`_
- `一份简单的 Rust 入门介绍 <https://zhuanlan.zhihu.com/p/298648575>`_
- `《RustOS Guide》中的 Rust 介绍部分 <https://simonkorl.gitbook.io/r-z-rustos-guide/dai-ma-zhi-qian/ex1>`_
\ No newline at end of file
常见工具的使用方法
========================================
.. toctree::
:hidden:
:maxdepth: 4
\ No newline at end of file
应用程序运行环境与平台支持
================================================
.. toctree::
:hidden:
:maxdepth: 5
作为一切的开始,让我们使用 Cargo 工具来创建一个 Rust 项目。它看上去没有任何特别之处:
.. code-block:: console
$ cargo new os --bin
我们加上了 ``--bin`` 选项来告诉 Cargo 我们创建一个可执行项目而不是库项目。此时,项目的文件结构如下:
.. code-block:: console
$ tree os
os
├── Cargo.toml
└── src
└── main.rs
1 directory, 2 files
其中 ``Cargo.toml`` 中保存着项目的配置,包括作者的信息、联系方式以及库依赖等等。显而易见源代码保存在 ``src`` 目录下,目前为止只有 ``main.rs``
一个文件,让我们看一下里面的内容:
.. code-block:: rust
:linenos:
:caption: 最简单的 Rust 应用
fn main() {
println!("Hello, world!");
}
进入 os 项目根目录下,利用 Cargo 工具即可一条命令实现构建并运行项目:
.. code-block:: console
$ cargo run
Compiling os v0.1.0 (/home/shinbokuow/workspace/v3/rCore-Tutorial-v3/os)
Finished dev [unoptimized + debuginfo] target(s) in 1.15s
Running `target/debug/os`
Hello, world!
如我们预想的一样,我们在屏幕上看到了一行 ``Hello, world!`` 。但是,需要注意到我们所享受到的编程的方便并不是理所当然的,背后有着从硬件
到软件的多种机制的支持。
应用程序运行环境
-------------------------------
如下图所示,应用程序的运行需要下面一套运行环境栈的支持:
.. _app-software-stack:
.. figure:: app-software-stack.png
:align: center
应用程序运行环境栈:图中的白色块自上而下(越往下则越靠近底层,下层作为上层的执行环境支持上层代码的运行)表示各级运行环境,
黑色块则表示相邻两层运行环境之间的接口。
我们的应用位于最上层,它可以通过调用编程语言提供的标准库或者其他三方库对外提供的功能强大的函数接口,使得仅需少量的源代码就能完成复杂的
功能。但是这些库的功能不仅限于此,事实上它们属于应用程序的 **执行环境** (Execution Environment),在我们通常不会注意到的地方,它
们还会在执行应用之前完成一些初始化工作,并在应用程序执行的时候对它进行监控。我们在打印 ``Hello, world!`` 时使用的 ``println!``
宏正是由 Rust 标准库 std 提供的。
从内核/操作系统的角度看来,它上面的一切都属于用户态,而它自身属于内核态。无论用户态应用如何编写,是手写汇编代码,还是基于某种编程语言利用
其标准库或三方库,某些功能总要直接或间接的通过内核/操作系统提供的 **系统调用** (System Call) 来实现。因此系统调用充当了用户和内核之间
的边界。内核作为用户态的运行环境,它不仅要提供系统调用接口,还需要对用户态应用的执行进行监控和管理。
.. note::
**Hello, world! 用到了哪些系统调用?**
从之前的 ``cargo run`` 的输出可以看出之前构建的可执行文件是在 target/debug 目录下的 os 。
在 Ubuntu 系统上,可以通过 ``strace`` 工具来运行一个程序并输出程序运行过程当中向内核请求的所有的系统调用及其返回值。
我们只需输入 ``strace target/debug/os`` 即可看到一长串的系统调用。
其中,真正容易看出与 ``Hello, world!`` 相关的只有一个系统调用:
.. code-block::
write(1, "Hello, world!\n", 14) = 14
其参数的具体含义我们暂且不在这里进行解释。
其余的系统调用基本上分别用于函数库和内核两层执行环境的初始化工作和对于上层的运行期监控和管理。之后,随着应用场景的复杂化,我们
需要更强的抽象能力,也会实现这里面的一些系统调用。
从硬件的角度来看,它上面的一切都属于软件。硬件可以分为三种: 处理器 (Processor) ——它更常见的名字是中央处理单元 (CPU, Central Processing Unit),
内存 (Memory) 还有 I/O 设备。其中处理器无疑是其中最复杂同时也最关键的一个。它与软件约定一套 **指令集体系结构** (ISA, Instruction Set Architecture),
使得软件可以通过 ISA 中提供的汇编指令来访问各种硬件资源。软件当然也需要知道处理器会如何执行这些指令:最简单的话就是一条一条执行位于内存
中的指令。当然,实际的情况远比这个要复杂得多,为了适应现代应用程序的场景,处理器还需要提供很多额外的机制,而不仅仅是让数据在 CPU 寄存器、内存和 I/O 设备
三者之间流动。
.. note::
**多层执行环境都是必需的吗?**
除了最上层的应用程序和最下层的硬件平台必须存在之外,作为中间层的函数库和内核并不是必须存在的:它们都是对下层资源进行了 **抽象** (Abstraction),
并为上层提供了一套运行环境。抽象的优点在于它让上层以较小的代价获得所需的功能,并同时可以提供一些保护。但抽象同时也是一种限制,会丧失一些
应有的灵活性。比如,当你在考虑在项目中应该使用哪个函数库的时候,就常常需要这方面的权衡:过多的抽象和过少的抽象自然都是不合适的。
实际上,我们通过应用程序的特征来判断它需要什么程度的抽象。
- 如果函数库和内核都不存在,那么我们就是在手写汇编代码,这种方式具有最高的灵活性,抽象能力则最低,基本等同于硬件。我们通常用这种方式来
实现一些架构相关且仅通过编程语言无法描述的小模块或者代码片段。
- 如果仅存在函数库而不存在内核,意味着我们不需要内核提供的抽象。在嵌入式场景就常常会出现这种情况。嵌入式设备虽然也包含 CPU、内存和 I/O
设备,但是它上面通常只会同时运行一个或几个功能非常简单的小应用程序,其定位就是那种功能单一的场景,比如人脸识别打卡系统等。我们常用的
操作系统如 Windows/Linux/macOS 等的抽象都支持同时运行很多应用程序,在嵌入式场景是过抽象的。因此,常见的解决方案是仅使用函数库构建
单独的应用程序或是用专为应用场景特别裁减过的轻量级内核管理少数应用程序。
平台与目标三元组
---------------------------------------
对于一份用某种编程语言实现的源代码而言,编译器在将其通过编译、链接得到目标文件的时候需要知道程序要在哪个 **平台** (Platform) 上运行。
从上面给出的 :ref:`应用程序运行环境栈 <app-software-stack>` 可以看出:
- 如果用户态基于的内核不同,会导致系统调用接口不同或者语义不一致;
- 如果底层硬件不同,对于硬件资源的访问方式会有差异。特别是 ISA 不同的话,对上提供的指令集和寄存器都不同。
它们都会导致最终生成的目标文件有很大不同。需要指出的是,某些编译器支持同一份源代码无需修改就可编译到多个不同的目标平台并在上面运行。这种
情况下,源代码是 **跨平台** 的。而另一些编译器则已经预设好了一个固定的目标平台。
我们可以通过 **目标三元组** (Target Triplet) 来描述一个目标平台。它一般包括 CPU 架构、CPU 厂商和操作系统,它们确实都会控制目标文件的生成。
比如,我们可以尝试看一下之前的 ``Hello, world!`` 的目标平台是什么。这可以通过打印编译器 rustc 的默认配置信息:
.. code-block:: console
$ rustc --version --verbose
rustc 1.48.0-nightly (73dc675b9 2020-09-06)
binary: rustc
commit-hash: 73dc675b9437c2a51a975a9f58cc66f05463c351
commit-date: 2020-09-06
host: x86_64-unknown-linux-gnu
release: 1.48.0-nightly
LLVM version: 11.0
从其中的 host 一项可以看出默认的目标平台是 ``x86_64-unknown-linux-gnu``,其中 CPU 架构是 x86_64,CPU 厂商是 unknown,操作系统是 linux-gnu。
这种无论编译器还是其目标文件都在我们当前所处的平台运行是一种最简单也最普遍的情况。但是很快我们就将遇到另外一种情况。
讲了这么多,终于该介绍我们的主线任务了。我们希望能够在另一个平台上运行 ``Hello, world!``,而与之前的默认平台不同的地方在于,我们将 CPU 架构从
x86_64 换成 RISC-V。
.. note::
为何基于 RISC-V 架构而非 x86 系列架构?
x86 架构为了在升级换代的同时保持对基于旧版架构应用程序/内核的兼容性,存在大量的历史包袱,也就是一些对于目前的应用场景没有任何意义,但又必须
花大量时间正确设置才能正常使用 CPU 的奇怪设定。为了建立并维护架构的应用生态,这确实是必不可少的,但站在教学的角度几乎完全是在浪费时间。而
新生的 RISC-V 架构十分简洁,架构文档需要阅读的核心部分不足百页,且这些功能已经足以用来构造一个具有相当抽象能力的内核了。
可以看一下目前 Rust 编译器支持哪些基于 RISC-V 的平台:
.. code-block:: console
$ rustc --print target-list | grep riscv
riscv32i-unknown-none-elf
riscv32imac-unknown-none-elf
riscv32imc-unknown-none-elf
riscv64gc-unknown-linux-gnu
riscv64gc-unknown-none-elf
riscv64imac-unknown-none-elf
这里我们选择的是 ``riscv64gc-unknown-none-elf``,目标三元组中的操作系统是 none-elf,表明没有任何系统调用支持。这里我们之所以不选择有
linux-gnu 系统调用支持的版本 ``riscv64gc-unknown-linux-gnu``,是因为我们只是想跑一个 ``Hello, world!``,没有必要使用操作系统所提供的
那么高级的抽象。而且我们很清楚后续我们要开发的是一个内核,如果仅仅基于已有操作系统提供的系统调用的话,它自身的抽象能力会受到很大限制。所以它必须
直面底层硬件来解锁更大的抽象能力上限。
.. note::
**RISC-V 指令集拓展**
由于基于 RISC-V 架构的处理器可能用于嵌入式场景或是通用计算场景,因此指令集规范将指令集划分为最基本的 RV32/64I 以及若干标准指令集拓展。
每款处理器只需按照其实际应用场景按需实现指令集拓展即可。
- RV32/64I:每款处理器都必须实现的基本整数指令集。在 RV32I 中,每个通用寄存器的位宽为 32 位;在 RV64I 中则为 64 位。它可以用来模拟
绝大多数标准指令集拓展中的指令,除了比较特殊的 A 拓展,因为它需要特别的硬件支持。
- M 拓展:提供整数乘除法相关指令。
- A 拓展:提供原子指令和一些相关的内存同步机制,这个后面会展开。
- F/D 拓展:提供单/双精度浮点数运算支持。
- C 拓展:提供压缩指令拓展。
G 拓展是基本整数指令集 I 再加上标准指令集拓展 MAFD 的总称,因此 riscv64gc 也就等同于 riscv64imafdc。我们剩下的内容都基于该处理器
架构完成。除此之外 RISC-V 架构还有很多标准指令集拓展,有一些还在持续更新中尚未稳定,有兴趣的读者可以浏览最新版的 RISC-V 指令集规范。
Rust 标准库与核心库
----------------------------------
我们尝试一下将当前的 ``Hello, world!`` 程序的目标平台换成 riscv64gc-unknown-none-elf 看看会发生什么事情:
.. code-block:: console
$ cargo run --target riscv64gc-unknown-none-elf
Compiling os v0.1.0 (/home/shinbokuow/workspace/v3/rCore-Tutorial-v3/os)
error[E0463]: can't find crate for `std`
|
= note: the `riscv64gc-unknown-none-elf` target may not be installed
在之前的环境配置中,我们已经在 rustup 工具链中安装了这个目标平台支持,因此并不是该目标平台未安装的问题。因此只是单纯的在这个目标平台上找不到
Rust 标准库 std。我们之前曾经提到过,编程语言的标准库或三方库的某些功能会直接或间接的用到操作系统提供的系统调用。但目前我们所选的目标平台不存在
任何操作系统支持,于是 Rust 并没有为这个目标平台支持完整的标准库 std。类似这样的平台通常被我们称为 **裸机平台** (bare-metal)。
幸运的是,Rust 有一个对 std 裁剪过后的核心库 core,这个库是不需要任何操作系统支持的,相对的它的功能也比较受限,但是也包含了 Rust 语言
相当一部分的核心机制,可以满足我们的大部分需求。在 Rust 语言生态中,有很多三方库也不依赖标准库 std 而仅仅依赖核心库 core,它们也可以很大
程度上减轻我们的编程负担。它们是我们能够在裸机平台挣扎求生的最主要倚仗。
于是,我们知道在裸机平台上我们要将对于标准库 std 的引用换成核心库 core。但是做起来其实并没有那么容易。
\ No newline at end of file
移除标准库依赖
==========================
.. toctree::
:hidden:
:maxdepth: 5
本节我们尝试移除之前的 ``Hello world!`` 程序对于标准库的依赖,使得它能够编译到裸机平台 RV64GC 上。
我们首先在 ``os`` 目录下新建 ``.cargo`` 目录,并在这个目录下创建 ``config`` 文件,并在里面输入如下内容:
.. code-block::
// os/.cargo/config
[build]
target = "riscv64gc-unknown-none-elf"
这会对于 Cargo 工具在 os 目录下的行为进行调整:现在默认会使用 riscv64gc 作为目标平台而不是原先的默认 x86_64-unknown-linux-gnu。
事实上,这是一种编译器运行所在的平台与编译器生成可执行文件的目标平台不同(分别是后者和前者)的情况。这是一种 **交叉编译** (Cross Compile)。
当然,这只是使得我们之后在 ``cargo build`` 的时候不必再加上 ``--target`` 参数的一个小 trick。如果我们现在 ``cargo build`` ,还是会和
上一小节一样出现找不到标准库 std 的错误。于是我们开始着手移除标准库。当然,这会产生一些副作用。
移除 println! 宏
----------------------------------
我们在 ``main.rs`` 的开头加上一行 ``#![no_std]`` 来告诉 Rust 编译器不使用 Rust 标准库 std 转而使用核心库 core。编译器报出如下错误:
.. error::
.. code-block:: console
$ cargo build
Compiling os v0.1.0 (/home/shinbokuow/workspace/v3/rCore-Tutorial-v3/os)
error: cannot find macro `println` in this scope
--> src/main.rs:4:5
|
4 | println!("Hello, world!");
| ^^^^^^^
我们之前提到过, println! 宏是由标准库 std 提供的,且会使用到一个名为 write 的系统调用。现在我们的条件还不足以自己实现一个 println! 宏,由于
使用了系统调用也不能在核心库 core 中找到它。我们目前先通过将它注释掉来绕过它。
提供语义项 panic_handler
----------------------------------------------------
.. error::
.. code-block:: console
$ cargo build
Compiling os v0.1.0 (/home/shinbokuow/workspace/v3/rCore-Tutorial-v3/os)
error: `#[panic_handler]` function required, but not found
在使用 Rust 编写应用程序的时候,我们常常在遇到了一些无法恢复的致命错误导致程序无法继续向下运行的时候手动或自动调用 panic! 宏来并打印出错的
位置让我们能够意识到它的存在,并进行一些后续处理。panic! 宏最典型的应用场景包括断言宏 assert! 失败或者对 ``Option::None/Result::Err``
进行 ``unwrap`` 操作。
在标准库 std 中提供了 panic 的处理函数 ``#[panic_handler]``,其大致功能是打印出错位置和原因并杀死当前应用。可惜的是在核心库 core 中并没有提供,
因此我们需要自己实现 panic 处理函数。
.. note::
**Rust 语义项 lang_items**
Rust 编译器内部的某些功能的实现并不是硬编码在语言内部的,而是以一种可插入的形式在库中提供。库只需要通过某种方式告诉编译器它的某个方法实现了
编译器内部的哪些功能,编译器就会采用库提供的方法来实现它内部对应的功能。通常只需要在库的方法前面加上一个标记即可。
我们开一个新的子模块 ``lang_items.rs`` 保存这些语义项,在里面提供 panic 处理函数的实现并通过标记通知编译器采用我们的实现:
.. code-block:: rust
// os/src/lang_items.rs
use core::panic::PanicInfo;
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
注意,panic 处理函数的函数签名需要一个 ``PanicInfo`` 的不可变借用作为输入参数,它在核心库中得以保留,这也是我们第一次与核心库打交道。之后我们
会从 ``PanicInfo`` 解析出错位置并打印出来,然后杀死应用程序。但目前我们什么都不做只是在原地 loop。
移除 main 函数
-----------------------------
.. error::
.. code-block::
$ cargo build
Compiling os v0.1.0 (/home/shinbokuow/workspace/v3/rCore-Tutorial-v3/os)
error: requires `start` lang_item
编译器提醒我们缺少一个名为 ``start`` 的语义项。我们回忆一下,之前提到语言标准库和三方库作为应用程序的执行环境,需要负责在执行应用程序之前进行
一些初始化工作,然后才跳转到应用程序的入口点(也就是跳转到我们编写的 ``main`` 函数)开始执行。事实上 ``start`` 语义项正代表着标准库 std 在
执行应用程序之前需要进行的一些初始化工作。由于我们禁用了标准库,编译器也就找不到这项功能的实现了。
最简单的解决方案就是压根不让编译器使用这项功能。我们在 ``main.rs`` 的开头加入设置 ``#![no_main]`` 告诉编译器我们没有一般意义上的 ``main`` 函数,
并将原来的 ``main`` 函数删除。在失去了 ``main`` 函数的情况下,编译器也就不需要完成所谓的初始化工作了。
至此,我们成功移除了标准库的依赖并完成裸机平台上的构建。
.. code-block:: console
$ cargo build
Compiling os v0.1.0 (/home/shinbokuow/workspace/v3/rCore-Tutorial-v3/os)
Finished dev [unoptimized + debuginfo] target(s) in 0.06s
目前的代码如下:
.. code-block:: rust
// os/src/main.rs
#![no_std]
#![no_main]
mod lang_items;
// os/src/lang_items.rs
use core::panic::PanicInfo;
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
本小节我们固然脱离了标准库,通过了编译器的检验,但也是伤筋动骨,将原有的很多功能弱化甚至直接删除,看起来距离在 RV64GC 平台上打印
``Hello world!`` 相去甚远了(我们甚至连 println! 和 ``main`` 函数都删除了)。不要着急,接下来我们会以自己的方式来重塑这些
功能,并最终完成我们的目标。
.. note::
**在 x86_64 平台上移除标准库依赖**
有兴趣的同学可以将目标平台换回之前默认的 ``x86_64-unknown-linux-gnu`` 并重复本小节所做的事情,比较两个平台从 ISA 到操作系统
的差异。可以参考 `BlogOS 的相关内容 <https://os.phil-opp.com/freestanding-rust-binary/>`_ 。
\ No newline at end of file
此差异已折叠。
手动加载、运行应用程序
==================================
.. toctree::
:hidden:
:maxdepth: 5
在上一节中我们自己实现了一套运行时来代替标准库,并完整的构建了最终的可执行文件。但是它现在只是放在磁盘上的一个文件,若想将它运行起来的话,
就需要将它加载到内存中,在大多数情况下这是操作系统的任务。
让我们先来看看最终可执行文件的格式:
.. code-block:: console
$ file os/target/riscv64gc-unknown-none-elf/release/os
os/target/riscv64gc-unknown-none-elf/release/os: ELF 64-bit LSB executable,
UCB RISC-V, version 1 (SYSV), statically linked, not stripped
从中可以看出可执行文件的格式为 **可执行和链接格式** (Executable and Linkable Format, ELF),硬件平台是 RV64 。在 ELF 文件中,
除了程序必要的代码、数据段(它们本身都只是一些二进制的数据)之外,还有一些 **元数据** (Metadata) 描述这些段在地址空间中的位置和在
文件中的位置以及一些权限控制信息,这些元数据只能放在代码、数据段的外面。
我们可以通过二进制工具 ``readelf`` 来看看 ELF 文件中究竟包含什么内容,输入命令:
.. code-block:: console
$ riscv64-unknown-elf-readelf os/target/riscv64gc-unknown-none-elf/release/os -a
首先可以看到一个 ELF header,它位于 ELF 文件的开头:
.. code-block:: objdump
:linenos:
:emphasize-lines: 2,11,12,13,17,19
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: RISC-V
Version: 0x1
Entry point address: 0x80020000
Start of program headers: 64 (bytes into file)
Start of section headers: 9016 (bytes into file)
Flags: 0x1, RVC, soft-float ABI
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 3
Size of section headers: 64 (bytes)
Number of section headers: 8
Section header string table index: 6
- 第 2 行是一个称之为 **魔数** (Magic) 独特的常数,存放在 ELF header 的一个固定位置。当加载器将 ELF 文件加载到内存之前,通常会查看
该位置的值是否正确,来快速确认被加载的文件是不是一个 ELF 。
- 第 11 行给出了可执行文件的入口点为 ``0x80020000`` ,这正是我们上一节所做的事情。
- 从 12/13/17/19 行中,我们可以知道除了 ELF header 之外,还有另外两种不同的 header,分别称为 program header 和 section header,
它们都有多个。ELF header 中给出了三种 header 的大小、在文件中的位置以及数目。
一共有 3 个不同的 program header,它们从文件的 64 字节开始,每个 56 字节:
.. code-block:: objdump
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000001000 0x0000000080020000 0x0000000080020000
0x000000000000001a 0x000000000000001a R E 0x1000
LOAD 0x0000000000002000 0x0000000080021000 0x0000000080021000
0x0000000000000000 0x0000000000010000 RW 0x1000
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x0
每个 program header 指向一个在加载的时候可以连续加载的区域。
一共有 8 个不同的 section header,它们从文件的 9016 字节开始,每个 64 字节:
.. code-block:: objdump
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000080020000 00001000
000000000000001a 0000000000000000 AX 0 0 2
[ 2] .bss NOBITS 0000000080021000 00002000
0000000000010000 0000000000000000 WA 0 0 1
[ 3] .riscv.attributes RISCV_ATTRIBUTE 0000000000000000 00002000
000000000000006a 0000000000000000 0 0 1
[ 4] .comment PROGBITS 0000000000000000 0000206a
0000000000000013 0000000000000001 MS 0 0 1
[ 5] .symtab SYMTAB 0000000000000000 00002080
00000000000001c8 0000000000000018 7 4 8
[ 6] .shstrtab STRTAB 0000000000000000 00002248
0000000000000041 0000000000000000 0 0 1
[ 7] .strtab STRTAB 0000000000000000 00002289
00000000000000ab 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
p (processor specific)
There are no section groups in this file.
每个 section header 则描述一个段的元数据。
其中,我们看到了代码段 ``.text`` 被放在可执行文件的 4096 字节处,大小 0x1a=26 字节,需要被加载到地址 ``0x80020000``。
它们分别由元数据的字段 Offset、 Size 和 Address 给出。同理,我们自己预留的应用程序函数调用栈在 ``.bss`` 段中,大小为 :math:`64\text{KiB}`
,需要被加载到地址 ``0x80021000`` 处。我们没有看到 ``.data/.rodata`` 等段,因为目前的 ``rust_main`` 里面没有任何东西。
我们还能够看到 ``.symtab`` 段中给出的符号表:
.. code-block::
Symbol table '.symtab' contains 19 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS os.78wp4f2l-cgu.0
2: 0000000000000000 0 FILE LOCAL DEFAULT ABS os.78wp4f2l-cgu.1
3: 0000000080020000 0 NOTYPE LOCAL DEFAULT 1 .Lpcrel_hi0
4: 0000000080020000 0 NOTYPE GLOBAL DEFAULT 1 _start
5: 0000000080021000 0 NOTYPE GLOBAL DEFAULT 2 boot_stack
6: 0000000080031000 0 NOTYPE GLOBAL DEFAULT 2 boot_stack_top
7: 0000000080020010 10 FUNC GLOBAL DEFAULT 1 rust_main
8: 0000000080020000 0 NOTYPE GLOBAL DEFAULT ABS BASE_ADDRESS
9: 0000000080020000 0 NOTYPE GLOBAL DEFAULT 1 skernel
10: 0000000080020000 0 NOTYPE GLOBAL DEFAULT 1 stext
11: 0000000080021000 0 NOTYPE GLOBAL DEFAULT 1 etext
12: 0000000080021000 0 NOTYPE GLOBAL DEFAULT 1 srodata
13: 0000000080021000 0 NOTYPE GLOBAL DEFAULT 1 erodata
14: 0000000080021000 0 NOTYPE GLOBAL DEFAULT 1 sdata
15: 0000000080021000 0 NOTYPE GLOBAL DEFAULT 1 edata
16: 0000000080031000 0 NOTYPE GLOBAL DEFAULT 2 sbss
17: 0000000080031000 0 NOTYPE GLOBAL DEFAULT 2 ebss
18: 0000000080031000 0 NOTYPE GLOBAL DEFAULT 2 ekernel
里面包括了栈顶、栈底、rust_main 的地址以及我们在 ``linker.ld`` 中定义的各个段开始和结束地址。
因此,从 ELF header 中可以看出,ELF 中的内容按顺序应该是:
- ELF header
- 若干个 program header
- 程序各个段的实际数据
- 若干的 section header
当将程序加载到内存的时候,对于每个 program header 所指向的区域,我们需要将对应的数据从文件复制到内存中。这就需要解析 ELF 的元数据
才能知道数据在文件中的位置以及即将被加载到内存中的位置。但目前,我们不需要从 ELF 中解析元数据就知道程序的内存布局
(这个内存布局是我们按照需求自己指定的),我们可以手动完成加载任务。
具体的做法是利用 ``rust-objcopy`` 工具删除掉 ELF 文件中的
所有 header 只保留各个段的实际数据得到一个没有任何符号的纯二进制镜像文件,由于缺少了必要的元数据,我们的二进制工具也没有办法
对它完成解析了。而后,我们直接将这个二进制镜像文件手动载入到内存中合适位置即可。在这里,我们知道在镜像文件中,仍然是代码段 ``.text``
作为起始,因此我们要将这个代码段载入到 ``0x80020000`` 才能和上一级 bootloader 对接上。因此,我们只要把整个镜像文件手动载入到
内存的地址 ``0x80020000`` 处即可。在不同的硬件平台上,手动加载的方式是不同的。
qemu 平台
-------------------------
首先我们还原一下可执行文件和二进制镜像的生成流程:
.. code-block:: makefile
# os/Makefile
TARGET := riscv64gc-unknown-none-elf
MODE := release
KERNEL_ELF := target/$(TARGET)/$(MODE)/os
KERNEL_BIN := $(KERNEL_ELF).bin
$(KERNEL_BIN): kernel
@$(OBJCOPY) $(KERNEL_ELF) --strip-all -O binary $@
kernel:
@cargo build --release
这里可以看出 ``KERNEL_ELF`` 保存最终可执行文件 ``os`` 的路径,而 ``KERNEL_BIN`` 保存只保留各个段数据的二进制镜像文件 ``os.bin``
的路径。目标 ``kernel`` 直接通过 ``cargo build`` 以 release 模式最终可执行文件,目标 ``KERNEL_BIN`` 依赖于目标 ``kernel``,将
可执行文件通过 ``rust-objcopy`` 工具加上适当的配置移除所有的 header 和符号得到二进制镜像。
我们可以通过 ``make run`` 直接在 qemu 上运行我们的应用程序,qemu 是一个虚拟机,它完整的模拟了一整套硬件平台,就像是一台真正的计算机
一样,我们来看运行 qemu 的具体命令:
.. code-block:: makefile
:linenos:
:emphasize-lines: 11,12,13,14,15
KERNEL_ENTRY_PA := 0x80020000
BOARD ?= qemu
SBI ?= rustsbi
BOOTLOADER := ../bootloader/$(SBI)-$(BOARD).bin
run: run-inner
run-inner: build
ifeq ($(BOARD),qemu)
@qemu-system-riscv64 \
-machine virt \
-nographic \
-bios $(BOOTLOADER) \
-device loader,file=$(KERNEL_BIN),addr=$(KERNEL_ENTRY_PA)
else
@cp $(BOOTLOADER) $(BOOTLOADER).copy
@dd if=$(KERNEL_BIN) of=$(BOOTLOADER).copy bs=128K seek=1
@mv $(BOOTLOADER).copy $(KERNEL_BIN)
@sudo chmod 777 $(K210-SERIALPORT)
python3 $(K210-BURNER) -p $(K210-SERIALPORT) -b 1500000 $(KERNEL_BIN)
miniterm --eol LF --dtr 0 --rts 0 --filter direct $(K210-SERIALPORT) 115200
endif
注意其中高亮部分给出了传给 qemu 的参数。
- ``-machine`` 告诉 qemu 使用预设的硬件配置。在整个项目中我们将一直沿用该配置。
- ``-bios`` 告诉 qemu 使用我们放在 ``bootloader`` 目录下的预编译版本作为 bootloader。
- ``-device`` 则告诉 qemu 将二进制镜像加载到内存指定的位置。
可以先输入 Ctrl+A ,再输入 X 来退出 qemu 终端。
.. warning::
**FIXME: 使用 GDB 跟踪 qemu 的运行状态**
k210 平台
------------------------
对于 k210 平台来说,只需要将 maix 系列开发板通过数据线连接到 PC,然后 ``make run BOARD=k210`` 即可。从 Makefile 中来看:
.. code-block:: makefile
:linenos:
:emphasize-lines: 13,16,17
K210-SERIALPORT = /dev/ttyUSB0
K210-BURNER = ../tools/kflash.py
run-inner: build
ifeq ($(BOARD),qemu)
@qemu-system-riscv64 \
-machine virt \
-nographic \
-bios $(BOOTLOADER) \
-device loader,file=$(KERNEL_BIN),addr=$(KERNEL_ENTRY_PA)
else
@cp $(BOOTLOADER) $(BOOTLOADER).copy
@dd if=$(KERNEL_BIN) of=$(BOOTLOADER).copy bs=128K seek=1
@mv $(BOOTLOADER).copy $(KERNEL_BIN)
@sudo chmod 777 $(K210-SERIALPORT)
python3 $(K210-BURNER) -p $(K210-SERIALPORT) -b 1500000 $(KERNEL_BIN)
miniterm --eol LF --dtr 0 --rts 0 --filter direct $(K210-SERIALPORT) 115200
endif
在构建目标 ``run-inner`` 的时候,根据平台 ``BOARD`` 的不同,启动运行的指令也不同。当我们传入命令行参数 ``BOARD=k210`` 时,就会进入下面
的分支。
- 第 13 行我们使用 ``dd`` 工具将 bootloader 和二进制镜像拼接到一起,这是因为 k210 平台的写入工具每次只支持写入一个文件,所以我们只能
将二者合并到一起一并写入 k210 的内存上。这样的参数设置可以保证 bootloader 在合并后文件的开头,而二进制镜像在文件偏移量 0x20000 的
位置处。有兴趣的读者可以输入命令 ``man dd`` 查看关于工具 ``dd`` 的更多信息。
- 第 16 行我们使用烧写工具 ``K210-BURNER`` 将合并后的镜像烧写到 k210 开发板的内存的 ``0x80000000`` 地址上。
参数 ``K210-SERIALPORT`` 表示当前 OS 识别到的 k210 开发板的串口设备名。在 Ubuntu 平台上一般为 ``/dev/ttyUSB0``。
- 第 17 行我们打开串口终端和 k210 开发板进行通信,可以通过键盘向 k210 开发板发送字符并在屏幕上看到 k210 开发板的字符输出。
可以输入 Ctrl+] 退出 miniterm。
手动清空 .bss 段
----------------------------------
由于 ``.bss`` 段需要在程序正式开始运行之前被固定初始化为零,因此在 ELF 文件中,为了节省磁盘空间,只会记录 ``.bss`` 段的位置而并不是
有一块长度相等的全为零的数据。在内核将可执行文件加载到内存的时候,它需要负责将 ``.bss`` 所分配到的内存区域全部清零。而我们这里需要在
应用程序 ``rust_main`` 中,在访问任何 ``.bss`` 段的全局数据之前手动将其清零。
.. code-block:: rust
:linenos:
// os/src/main.rs
fn clear_bss() {
extern "C" {
fn sbss();
fn ebss();
}
(sbss as usize..ebss as usize).for_each(|a| {
unsafe { (a as *mut u8).write_volatile(0) }
});
}
在程序内自己进行清零的时候,我们就不用去解析 ELF(此时也没有 ELF 可供解析)了,而是通过链接脚本 ``linker.ld`` 中给出的全局符号
``sbss`` 和 ``ebss`` 来确定 ``.bss`` 段的位置。
.. note::
**Rust 小知识:外部符号引用**
extern "C" 可以引用一个外部的 C 函数接口(这意味着调用它的时候要遵从目标平台的 C 语言调用规范)。但我们这里只是引用位置标志
并将其转成 usize 获取它的地址。由此可以知道 ``.bss`` 段两端的地址。
**Rust 小知识:迭代器与闭包**
代码第 7 行用到了 Rust 的迭代器与闭包的语法,它们在很多情况下能够提高开发效率。如读者感兴趣的话也可以将其改写为等价的 for
循环实现。
.. warning::
**Rust Unsafe**
代码第 8 行,我们将 ``.bss`` 段内的一个地址转化为一个 **裸指针** (Raw Pointer),并将它指向的值修改为 0。这在 C 语言中是
一种司空见惯的操作,但在 Rust 中我们需要将他包裹在 unsafe 块中。这是因为,Rust 认为对于裸指针的 **解引用** (Dereference)
是一种 unsafe 行为。
相比 C 语言,Rust 进行了更多的语义约束来保证安全性(内存安全/类型安全/并发安全),这在编译期和运行期都有所体现。但在某些时候,
尤其是与底层硬件打交道的时候,在 Rust 的语义约束之内没法满足我们的需求,这个时候我们就需要将超出了 Rust 语义约束的行为包裹
在 unsafe 块中,告知编译器不需要对它进行完整的约束检查,而是由程序员自己负责保证它的安全性。当代码不能正常运行的时候,我们往往也是
最先去检查 unsafe 块中的代码,因为它没有受到编译器的保护,出错的概率更大。
C 语言中的指针相当于 Rust 中的裸指针,它无所不能但又太过于灵活,程序员对其不谨慎的使用常常会引起很多内存不安全问题,最常见的如
悬垂指针和多次回收的问题,Rust 编译器没法确认程序员对它的使用是否安全,因此将其划到 unsafe Rust 的领域。在 safe Rust 中,我们
有引用 ``&/&mut`` 以及各种功能各异的智能指针 ``Box<T>/RefCell<T>/Rc<T>`` 可以使用,只要按照 Rust 的规则来使用它们便可借助
编译器在编译期就解决很多潜在的内存不安全问题。
格式化输出
=====================
.. toctree::
:hidden:
:maxdepth: 5
这一小节我们来自己实现 ``println!`` 的功能。 我们这里只是给出一些函数之间的调用关系,而不在这里进行一些实现细节上的展开。有兴趣的读者
可以自行参考代码提供的注释。
在屏幕上打印一个字符是最基础的功能,它已经由 bootloader (也就是放在 ``bootloader`` 目录下的预编译版本)提供,具体的调用方法可以参考
``sbi.rs`` 中的 ``console_putchar`` 函数。
随后我们在 ``console.rs`` 中利用 ``console_putchar`` 来实现 ``print!`` 和 ``println!`` 两个宏。有兴趣的读者可以去代码注释中
参考有关 Rust ``core::fmt`` 库和宏编写的相关知识。在 ``main.rs`` 声明子模块 ``mod console`` 的时候加上 ``#[macro_use]`` 来让
整个引用都可以使用到该模块里面定义的宏。
接着我们在 ``lang_items.rs`` 中修改 panic 时的行为:
.. code-block:: rust
// os/src/lang_items.rs
use crate::sbi::shutdown;
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
if let Some(location) = info.location() {
println!("Panicked at {}:{} {}", location.file(), location.line(), info.message().unwrap());
} else {
println!("Panicked: {}", info.message().unwrap());
}
shutdown()
}
我们尝试从传入的 ``PanicInfo`` 中解析 panic 发生的文件和行数。如果解析成功的话,就和 panic 的报错信息一起打印出来。我们需要在
``main.rs`` 开头加上 ``#![feature(panic_info_message)]`` 才能通过 ``PanicInfo::message`` 获取报错信息。
.. note::
**Rust 小知识: 错误处理**
Rust 中常利用 ``Option<T>`` 和 ``Result<T, E>`` 进行方便的错误处理。它们都属于枚举结构:
- ``Option<T>`` 既可以有值 ``Option::Some<T>`` ,也有可能没有值 ``Option::None``;
- ``Result<T, E>`` 既可以保存某个操作的返回值 ``Result::Ok<T>`` ,也可以表明操作过程中出现了错误 ``Result::Err<E>`` 。
我们可以使用 ``Option/Result`` 来保存一个不能确定存在/不存在或是成功/失败的值。之后可以通过匹配 ``if let`` 或是在能够确定
的场合直接通过 ``unwrap`` 将里面的值取出。详细的内容可以参考 Rust 官方文档。
此外,我们还使用 bootloader 中提供的另一个接口 ``shutdown`` 关闭机器。
最终我们的应用程序 ``rust_main`` 如下:
.. code-block:: rust
// os/src/main.rs
#[no_mangle]
pub fn rust_main() -> ! {
extern "C" {
fn stext();
fn etext();
fn srodata();
fn erodata();
fn sdata();
fn edata();
fn sbss();
fn ebss();
fn boot_stack();
fn boot_stack_top();
};
clear_bss();
println!("Hello, world!");
println!(".text [{:#x}, {:#x})", stext as usize, etext as usize);
println!(".rodata [{:#x}, {:#x})", srodata as usize, erodata as usize);
println!(".data [{:#x}, {:#x})", sdata as usize, edata as usize);
println!("boot_stack [{:#x}, {:#x})", boot_stack as usize, boot_stack_top as usize);
println!(".bss [{:#x}, {:#x})", sbss as usize, ebss as usize);
panic!("Shutdown machine!");
}
当我们在 qemu 平台上运行的时候能够看到如下的运行结果:
.. code-block::
:linenos:
[rustsbi] Version 0.1.0
.______ __ __ _______.___________. _______..______ __
| _ \ | | | | / | | / || _ \ | |
| |_) | | | | | | (----`---| |----`| (----`| |_) || |
| / | | | | \ \ | | \ \ | _ < | |
| |\ \----.| `--' |.----) | | | .----) | | |_) || |
| _| `._____| \______/ |_______/ |__| |_______/ |______/ |__|
[rustsbi] Platform: QEMU
[rustsbi] misa: RV64ACDFIMSU
[rustsbi] mideleg: 0x222
[rustsbi] medeleg: 0xb109
[rustsbi] Kernel entry: 0x80020000
Hello, world!
.text [0x80020000, 0x80022000)
.rodata [0x80022000, 0x80023000)
.data [0x80023000, 0x80023000)
boot_stack [0x80023000, 0x80033000)
.bss [0x80033000, 0x80033000)
Panicked at src/main.rs:46 Shutdown machine!
其中前 13 行是 bootloader 的输出,剩下的部分是我们的应用程序的输出,打印了 ``Hello, world!``,输出了程序内部各个段的地址区间,
还展示了 panic 相关信息。
\ No newline at end of file
练习一
==============
.. toctree::
:hidden:
:maxdepth: 5
编程练习一:backtrace
---------------------------------------------
仔细阅读 :ref:`函数调用与栈 <function-call-and-stack>` 小节的内容,特别是函数栈帧中的 ra 寄存器和 prev fp 的位置以及它们的作用。
编程实现:在 ``rust_main`` 中多层嵌套调用函数,然后在最深层按照层数由深到浅打印函数调用链,也就是每一层函数栈帧中保存的 ra 寄存器的值,
由此我们可以依次知道每个函数的调用语句所在的地址,也就能跟踪一整条函数调用链。
拓展:寻找/改写 Rust 库或者自己实现,能够通过 ra 寄存器的值得到其所在的源文件/函数/行数,从而更直观的看到函数调用链。或者也可以通过
addr2line 工具在运行结束之后手动去可执行文件中查找每个地址对应的信息。
\ No newline at end of file
第一章:RV64 裸机应用
==============================================
.. toctree::
:hidden:
:maxdepth: 4
1app-ee-platform
2remove-std
3minimal-rt
4load-manually
5sbi-print
6practice
大多数程序员的第一行代码都从 ``Hello, world!`` 开始,当我们满怀着好奇心在编辑器内键入仅仅数个字节,再经过几行命令编译、运行,终于
在黑洞洞的终端窗口中看到期望中的结果的时候,一扇通往编程世界的大门已经打开。时至今日,我们能够隐约意识到编程工作能够如此方便简洁并不是
理所当然的,实际上有着多层硬件、软件隐藏在它背后,才让我们不必付出那么多努力就能够创造出功能强大的应用程序。
本章我们的目标仍然只是输出 ``Hello, world!`` ,但这一次,我们将离开舒适区,基于一个几乎空无一物的平台从零开始搭建我们自己的高楼大厦,
而不是仅仅通过一行语句就完成任务。
获取本章代码:
.. code-block:: console
$ git clone https://github.com/rcore-os/rCore-Tutorial-v3.git
$ cd rCore-Tutorial-v3
$ git checkout ch1
在 qemu 模拟器上运行本章代码:
.. code-block:: console
$ cd os
$ make run
将 Maix 系列开发版连接到 PC,并在上面运行本章代码:
.. code-block:: console
$ cd os
$ make run BOARD=k210
.. warning::
**FIXME: 提供 wsl/macOS 等更多平台支持**
如果顺利的话,以 qemu 平台为例,将输出:
.. code-block::
[rustsbi] Version 0.1.0
.______ __ __ _______.___________. _______..______ __
| _ \ | | | | / | | / || _ \ | |
| |_) | | | | | | (----`---| |----`| (----`| |_) || |
| / | | | | \ \ | | \ \ | _ < | |
| |\ \----.| `--' |.----) | | | .----) | | |_) || |
| _| `._____| \______/ |_______/ |__| |_______/ |______/ |__|
[rustsbi] Platform: QEMU
[rustsbi] misa: RV64ACDFIMSU
[rustsbi] mideleg: 0x222
[rustsbi] medeleg: 0xb109
[rustsbi] Kernel entry: 0x80020000
Hello, world!
.text [0x80020000, 0x80022000)
.rodata [0x80022000, 0x80023000)
.data [0x80023000, 0x80023000)
boot_stack [0x80023000, 0x80033000)
.bss [0x80033000, 0x80033000)
Panicked at src/main.rs:46 Shutdown machine!
除了 ``Hello, world!`` 之外还有一些额外的信息,最后关机。
\ No newline at end of file
RISC-V 特权级架构
=====================================
.. toctree::
:hidden:
:maxdepth: 5
为了保护我们的批处理系统不受到出错应用程序的影响并全程稳定工作,单凭软件实现是很难做到的,而是需要 CPU 提供一种特权级隔离机制,使得它在执行
应用程序和内核代码的时候处于不同的特权级。特权级可以看成 CPU 随时间变化而处于的不同的工作模式。
RISC-V 架构中一共定义了 4 种特权级:
.. list-table:: RISC-V 特权级
:widths: 30 30 60
:header-rows: 1
:align: center
* - 级别
- 编码
- 名称
* - 0
- 00
- 机器模式 (M, Machine)
* - 1
- 01
- 监督模式 (S, Supervisor)
* - 2
- 10
- H, Hypervisor
* - 3
- 11
- 用户/应用模式 (U, User/Application)
其中,级别的数值越小,特权级越高,掌控硬件的能力越强。从表中可以看出, M 模式处在最高的特权级,而 U 模式处于最低的特权级。
之前我们给出过支持应用程序运行的一套 :ref:`执行环境栈 <app-software-stack>` ,现在我们站在特权级架构的角度去重新看待它:
.. image:: PrivilegeStack.png
:align: center
:name: PrivilegeStack
和之前一样,白色块表示一层执行环境,黑色块表示相邻两层执行环境之间的接口。这张图片给出了能够支持运行 Unix 这类复杂系统的软件栈。其中
内核代码运行在 S 模式上;应用程序运行在 U 模式上。运行在 M 模式上的软件被称为 **监督模式执行环境** (SEE, Supervisor Execution Environment)
,这是站在运行在 S 模式上的软件的视角来看,它的下面也需要一层执行环境支撑,因此被命名为 SEE,它需要在相比 S 模式更高的特权级下运行,
一般情况下在 M 模式上运行。
.. note::
**按需实现 RISC-V 特权级**
RISC-V 架构中,只有 M 模式是必须实现的,剩下的特权级则可以根据跑在 CPU 上应用的实际需求进行调整:
- 简单的嵌入式应用只需要实现 M 模式;
- 带有一定保护能力的嵌入式系统需要实现 M/U 模式;
- 复杂的多任务系统则需要实现 M/S/U 模式。
之前我们提到过,执行环境的其中一种功能是在执行它支持的上层软件之前进行一些初始化工作。我们之前提到的引导加载程序会在加电后对整个系统进行
初始化,它实际上是 SEE 功能的一部分,也就是说在 RISC-V 架构上引导加载程序一般运行在 M 模式上。此外,编程语言的标准库也会在执行程序员
编写的逻辑之前进行一些初始化工作,但是在这张图中我们并没有将其展开,而是统一归类到 U 模式软件,也就是应用程序中。
执行环境的另一种功能是对上层软件的执行进行监控管理。监控管理可以理解为,当上层软件执行的时候出现了一些情况导致需要用到执行环境中提供的功能,
因此需要暂停上层软件的执行,转而运行执行环境的代码。由于上层软件和执行环境被设计为运行在不同的特权级,这个过程也往往(而 **不一定** )
伴随着 CPU 的 **特权级切换** 。当执行环境的代码运行结束后,我们需要回到上层软件暂停的位置继续执行。在 RISC-V 架构中,这种与常规控制流
(顺序、循环、分支、函数调用)不同的 **异常控制流** (ECF, Exception Control Flow) 被称为 **陷入** (Trap) 。
触发 Trap 的原因总体上可以分为两种: **中断** (Interrupt) 和 **异常** (Exception) 。本章我们只会用到异常,因此暂且略过中断。异常
就是指上层软件需要执行环境功能的原因确切的与上层软件的 **某一条指令的执行** 相关。下表中我们给出了 RISC-V 特权级定义的一些异常:
.. list-table:: RISC-V 异常一览表
:align: center
:header-rows: 1
:widths: 30 30 60
* - Interrupt
- Exception Code
- Description
* - 0
- 0
- Instruction address misaligned
* - 0
- 1
- Instruction access fault
* - 0
- 2
- Illegal instruction
* - 0
- 3
- Breakpoint
* - 0
- 4
- Load address misaligned
* - 0
- 5
- Load access fault
* - 0
- 6
- Store/AMO address misaligned
* - 0
- 7
- Store/AMO access fault
* - 0
- 8
- Environment call from U-mode
* - 0
- 9
- Environment call from S-mode
* - 0
- 11
- Environment call from M-mode
* - 0
- 12
- Instruction page fault
* - 0
- 13
- Load page fault
* - 0
- 14
- Store/AMO page fault
其中断点异常 (Breakpoint) 和执行环境调用 (Environment call) 两个异常是通过在上层软件中执行一条特定的指令触发的:当执行 ``ebreak``
这条指令的之后就会触发断点异常;而执行 ``ecall`` 这条指令的时候则会随着 CPU 当前所处特权级而触发不同的异常。从表中可以看出,当 CPU 分别
处于 M/S/U 三种特权级时执行 ``ecall`` 这条指令会触发三种异常。
在这里我们需要说明一下执行环境调用,这是一种很特殊的异常, :ref:`上图 <PrivilegeStack>` 中相邻两特权级软件之间的接口正是基于这种异常
机制实现的。M 模式软件 SEE 和 S 模式的内核之间的接口被称为 **监督模式二进制接口** (SBI, Supervisor Binary Interface),而内核和
U 模式的应用程序之间的接口被称为 **应用程序二进制接口** (Application Binary Interface),当然它有一个更加通俗的名字—— **系统调用**
(syscall, System Call) 。而之所以叫做二进制接口,是因为它和在同一种编程语言内部调用接口不同,是汇编指令级的一种接口。事实上 M/S/U
三个特权级的软件可能分别由不同的编程语言实现,即使是用同一种编程语言实现的,其调用也并不是普通的函数调用执行流,而是陷入,在该过程中有可能
切换 CPU 特权级。因此只有将接口下降到汇编指令级才能够满足其通用性。
可以看到,在这样的架构之下,每层特权级的软件都只能做高特权级软件允许它做的、且对于高特权级软件不会产生什么撼动的事情,一旦超出了能力范围,
就必须寻求高特权级软件的帮助。因此,在一条执行流中我们经常能够看到特权级切换。如下图所示:
.. image:: EnvironmentCallFlow.png
:align: center
其他的异常则一般是在执行某一条指令的时候发生了错误,需要将控制转交给高特权级软件:当错误可恢复的时候,则处理错误并重新回到上层软件的执行;
否则,一般会将上层软件杀死以避免破坏执行环境。
第一章只是一个简单的嵌入式应用,它全程运行在 M 模式下。而在后续的章节中,我们会用到 M/S/U 三种特权级:其中我们的内核运行在 S 模式下
(在本章表现为一个简单的批处理系统),应用程序运行在 U 特权级下,第一章提到的预编译的 bootloader 实际上是运行在 M 模式下的 SEE。
整个系统就由这三层运行在不同特权级下的不同软件组成。在特权级相关机制方面,本书正文中我们重点关心 S/U 特权级, M 特权级的机制细节则
是作为可选内容在附录 :doc:`/appendix-c/index` 中讲解,有兴趣的读者可以参考。
..
随着特权级的逐渐降低,硬件的能力受到限制,
从每一个特权级看来,比它特权级更低的部分都可以看成是它的应用。(这个好像没啥用?)
M 模式是每个 RISC-V CPU 都需要实现的模式,而剩下的模式都是可选的。常见的模式组合:普通嵌入式应用只需要在 M 模式上运行;追求安全的
嵌入式应用需要在 M/U 模式上运行;像 Unix 这样比较复杂的系统这需要 M/S/U 三种模式。
RISC-V 特权级规范中给出了一些特权寄存器和特权指令...
重要的是保护,也就是特权级的切换。当 CPU 处于低特权级的时候,如果发生了错误或者一些需要处理的情况,CPU 会切换到高特权级进行处理。这个
就是所谓的 Trap 机制。
RISC-V 架构规范分为两部分: `RISC-V 无特权级规范 <https://github.com/riscv/riscv-isa-manual/releases/download/Ratified-IMAFDQC/riscv-spec-20191213.pdf>`_
和 `RISC-V 特权级规范 <https://github.com/riscv/riscv-isa-manual/releases/download/Ratified-IMFDQC-and-Priv-v1.11/riscv-privileged-20190608.pdf>`_ 。
RISC-V 无特权级规范中给出的指令和寄存器无论在 CPU 处于哪个特权级下都可以使用。
实现应用程序
===========================
.. toctree::
:hidden:
:maxdepth: 5
本节我们来实现被批处理系统逐个加载并运行的应用程序,它们是在认为自己会在 U 模式运行的前提下而设计、编写的,但实际上它们完全可能在其他特权级
运行。事实上,保证应用程序的代码在 U 模式运行是我们接下来将实现的批处理系统的任务。
应用程序的实现放在项目根目录的 ``user`` 目录下,它和第一章的嵌入式应用不同之处在于以下几点。
项目结构
------------------
我们看到 ``user/src`` 目录下面多出了一个 ``bin`` 目录。``bin`` 里面有多个文件,每个文件都是一个用户程序,目前里面有三个程序,分别是:
- ``00hello_world``:在屏幕上打印一行 ``Hello, world!``;
- ``01store_fault``:访问一个非法的物理地址,测试批处理系统是否会被该错误影响;
- ``02power``:一个略微复杂的、行为不断在计算和打印字符串间切换的程序。
批处理系统会按照文件名开头的编号从小到大的顺序加载并运行它们。
打开其中任意一个文件,会看到里面只有一个 ``main`` 函数,因此这很像是我们日常利用高级语言编程,只需要在单个文件中给出主逻辑的实现即可。
我们还能够看到代码中尝试引入了外部库:
.. code-block:: rust
#[macro_use]
extern crate user_lib;
这个外部库其实就是 ``user`` 目录下的 ``lib.rs`` 以及它引用的若干子模块中。至于这个外部库为何叫 ``user_lib`` 而不叫 ``lib.rs``
所在的目录的名字 ``user`` ,是因为在 ``user/Cargo.toml`` 中我们对于库的名字进行了设置: ``name = "user_lib"`` 。它作为
``bin`` 目录下的源程序所依赖的用户库,等价于其他编程语言提供的标准库。
在 ``lib.rs`` 中我们定义了用户库的入口点 ``_start`` :
.. code-block:: rust
:linenos:
#[no_mangle]
#[link_section = ".text.entry"]
pub extern "C" fn _start() -> ! {
clear_bss();
syscall::sys_exit(main());
panic!("unreachable after sys_exit!");
}
第 2 行使用 Rust 的宏将 ``_start`` 这段代码编译后的汇编代码中放在一个名为 ``.text.entry`` 的代码段中,方便我们在后续链接的时候
调整它的位置使得它能够作为用户库的入口。
而从第 4 行开始我们能够看到进入用户库入口之后,首先和第一章一样手动清空需要被零初始化 ``.bss`` 段(很遗憾到目前为止底层的批处理系统还
没有这个能力,所以我们只能在用户库中完成),然后是调用 ``main`` 函数得到一个类型为 ``i32`` 的返回值,最后是使用接下来会提到的系统调用
退出应用程序并将这个返回值告知批处理系统。
我们还在 ``lib.rs`` 中看到了另一个 ``main`` :
.. code-block:: rust
:linenos:
#[linkage = "weak"]
#[no_mangle]
fn main() -> i32 {
panic!("Cannot find main!");
}
第 1 行,我们使用 Rust 的宏将其函数符号 ``main`` 标志为弱链接。这样在最后链接的时候,虽然在 ``lib.rs`` 和 ``bin`` 目录下的某个
应用程序都有 ``main`` 符号,但由于 ``lib.rs`` 中的 ``main`` 符号是弱链接,链接器会使用 ``bin`` 目录下的应用主逻辑作为 ``main`` 。
这里我们主要是进行某种程度上的保护,如果在 ``bin`` 目录下找不到任何 ``main`` ,那么编译也能够通过,并会在运行时报错。
为了上述这些链接操作,我们需要在 ``lib.rs`` 的开头加入:
.. code-block:: rust
#![feature(linkage)]
内存布局
-------------------
在 ``user/.cargo/config`` 中,我们和第一章一样设置链接时使用链接脚本 ``user/src/linker.ld`` 。在其中我们做的重要的事情是:
- 将程序的起始物理地址调整为 ``0x80040000`` ,三个应用程序都会被加载到这个物理地址上运行;
- 将 ``_start`` 所在的 ``.text.entry`` 放在整个程序的开头,也就是说批处理系统只要在加载之后跳转到 ``0x80040000`` 就已经进入了
用户库的入口点,并会在初始化之后跳转到应用程序主逻辑;
- 提供了最终生成可执行文件的 ``.bss`` 段的起始和终止地址,方便 ``clear_bss`` 函数使用。
其余的部分和第一章基本相同。
系统调用
---------------------
在子模块 ``syscall`` 中我们作为应用程序来通过 ``ecall`` 调用批处理系统提供的接口,由于应用程序运行在 U 模式, ``ecall`` 指令会触发
名为 ``Environment call from U-mode`` 的异常,并 Trap 进入 S 模式执行批处理系统针对这个异常特别提供的服务代码。由于这个接口处于
S 模式的批处理系统和 U 模式的应用程序之间,从上一节我们可以知道,这个接口可以被称为 ABI 或者系统调用。现在我们不关心底层的批处理系统如何
提供应用程序所需的功能,只是站在应用程序的角度去使用即可。
在本章中,应用程序和批处理系统之间约定如下两个系统调用:
.. code-block:: rust
:caption: 系统调用一
/// 功能:将内存中缓冲区中的数据写入文件。
/// 参数:`fd` 表示待写入文件的文件描述符;
/// `buf` 表示内存中缓冲区的起始地址;
/// `len` 表示内存中缓冲区的长度。
/// 返回值:返回成功写入的长度。
/// syscall ID:64
fn sys_write(fd: usize, buf: *const u8, len: usize) -> isize;
/// 功能:退出应用程序并将返回值告知批处理系统。
/// 参数:`xstate` 表示应用程序的返回值。
/// 返回值:该系统调用不应该返回。
/// syscall ID:93
fn sys_exit(xstate: usize) -> !;
我们知道系统调用实际上是汇编指令级的二进制接口,因此这里给出的只是使用 Rust 语言描述的版本。在实际调用的时候,我们需要按照 RISC-V 调用
规范在合适的寄存器中放置系统调用的参数,然后执行 ``ecall`` 指令触发 Trap。在 Trap 回到 U 模式的应用程序代码之后,会从 ``ecall`` 的
下一条指令继续执行,同时我们能够按照调用规范在合适的寄存器中读取返回值。
在 RISC-V 调用规范中,和函数调用的情形类似,约定寄存器 ``a0~a6`` 保存系统调用的参数, ``a0~a1`` 保存系统调用的返回值。有些许不同的是
寄存器 ``a7`` 用来传递 syscall ID,这是因为所有的 syscall 都是通过 ``ecall`` 指令触发的,除了各输入参数之外我们还额外需要一个寄存器
来保存要请求哪个系统调用。由于这超出了 Rust 语言的表达能力,我们需要在代码中使用内嵌汇编来完成参数/返回值绑定和 ``ecall`` 指令的插入:
.. code-block:: rust
:linenos:
fn syscall(id: usize, args: [usize; 3]) -> isize {
let mut ret: isize;
unsafe {
llvm_asm!("ecall"
: "={x10}" (ret)
: "{x10}" (args[0]), "{x11}" (args[1]), "{x12}" (args[2]), "{x17}" (id)
: "memory"
: "volatile"
);
}
ret
}
第 1 行,我们将所有的系统调用都封装成 ``syscall`` 函数,可以看到它支持传入 syscall ID 和 3 个参数。
第 4 行开始,我们使用 Rust 提供的 ``llvm_asm!`` 宏在代码中内嵌汇编,在本行也给出了具体要插入的汇编指令,也就是 ``ecall``,但这并不是
全部,后面我们还需要进行一些相关设置。这个宏在 Rust 中还不稳定,因此我们需要在 ``lib.rs`` 开头加入 ``#![feature(llvm_asm)]`` 。
此外,编译器无法判定插入汇编代码这个行为的安全性,所以我们需要将其包裹在 unsafe 块中自己来对它负责。
Rust 中的 ``llvm_asm!`` 宏的完整格式如下:
.. code-block:: rust
llvm_asm!(assembly template
: output operands
: input operands
: clobbers
: options
);
下面逐行进行说明。
第 5 行指定输出操作数。这里由于我们的系统调用返回值只有一个 ``isize`` ,根据调用规范它会被保存在 ``a0`` 寄存器中。在双引号内,我们
可以对于使用的操作数进行限制,由于是输出部分,限制的开头必须是一个 ``=`` 。我们可以在限制内使用一对花括号再加上一个寄存器的名字告诉
编译器汇编的输出结果会保存在这个寄存器中。我们将声明出来用来保存系统调用返回值的变量 ``ret`` 包在一对普通括号里面放在操作数限制的
后面,这样可以把变量和寄存器建立联系。于是,在系统调用返回之后我们就能在变量 ``ret`` 中看到返回值了。注意,变量 ``ret`` 必须为可变
绑定,否则无法通过编译,这也说明在 unsafe 块内编译器还是会进行力所能及的安全检查。
第 6 行指定输入操作数。由于是输入部分,限制的开头不用加上 ``=`` 。同时在限制中设置使用寄存器 ``a0~a2`` 来保存系统调用的参数,以及
寄存器 ``a7`` 保存 syscall ID ,而它们分别 ``syscall`` 的参数变量 ``args`` 和 ``id`` 绑定。
第 7 行用于告知编译器插入的汇编代码会造成的一些影响以防止编译器在不知情的情况下误优化。常用的使用方法是告知编译器某个寄存器在执行嵌入
的汇编代码中的过程中会发生变化。我们这里则是告诉编译器在执行嵌入汇编代码中的时候会修改内存。这能给编译器提供更多信息。
第 8 行用于告知编译器将我们在程序中给出的嵌入汇编代码保持原样放到最终构建的可执行文件中。如果不这样做的话,编译器可能会把它和其他代码
一视同仁并放在一起进行一些我们期望之外的优化。为了保证语义的正确性,一些比较关键的汇编代码需要加上该选项。
第一章中的输出到屏幕的操作也同样是使用内联汇编调用 SEE 提供的 SBI 接口来实现的。有兴趣的读者可以回顾第一章的 ``console.rs`` 和
``sbi.rs`` 。
.. note::
**Rust 中的内联汇编**
我们这里使用的 ``llvm_asm!`` 宏是将 Rust 底层 IR LLVM 中提供的内联汇编包装成的,更多信息可以参考 `llvm_asm 文档 <https://doc.rust-lang.org/unstable-book/library-features/llvm-asm.html>`_ 。
在未来的 Rust 版本推荐使用功能更加强大且方便易用的 ``asm!`` 宏,但是目前还未稳定,可以查看 `inline-asm RFC <https://doc.rust-lang.org/beta/unstable-book/library-features/asm.html>`_ 了解最新进展。
于是 ``sys_write`` 和 ``sys_exit`` 只需将 ``syscall`` 进行包装:
.. code-block:: rust
:linenos:
const SYSCALL_WRITE: usize = 64;
const SYSCALL_EXIT: usize = 93;
pub fn sys_write(fd: usize, buffer: &[u8]) -> isize {
syscall(SYSCALL_WRITE, [fd, buffer.as_ptr() as usize, buffer.len()])
}
pub fn sys_exit(xstate: i32) -> isize {
syscall(SYSCALL_EXIT, [xstate as usize, 0, 0])
}
注意 ``sys_write`` 使用一个 ``&[u8]`` 切片类型来描述缓冲区,这是一个 **胖指针** (Fat Pointer),里面既包含缓冲区的起始地址,还
包含缓冲区的长度。我们可以分别通过 ``as_ptr`` 和 ``len`` 方法取出它们并独立的作为实际的系统调用参数。
我们把 ``console`` 子模块中 ``Stdout::write_str`` 改成基于 ``sys_write`` 的实现,且传入的 ``fd`` 参数设置为 1,它代表标准输出,
也就是输出到屏幕。目前我们不需要考虑其他的 ``fd`` 选取情况。这样,应用程序的 ``println!`` 宏借助系统调用变得可用了。
``sys_exit`` 则在用户库中的 ``_start`` 内使用,当应用程序主逻辑 ``main`` 返回之后,使用该系统调用退出应用程序并将返回值告知
底层的批处理系统。
自动构建
-----------------------
这里简要介绍一下应用程序的自动构建。只需要在 ``user`` 目录下 ``make build`` 即可:
1. 对于 ``src/bin`` 下的每个应用程序,在 ``target/riscv64gc-unknown-none-elf/release`` 目录下生成一个同名的 ELF 可执行文件;
2. 使用 objcopy 二进制工具将上一步中生成的 ELF 文件删除所有 ELF header 和符号得到 ``.bin`` 后缀的纯二进制镜像文件。它们将被链接
进内核并由内核在合适的时机加载到内存。
\ No newline at end of file
实现批处理系统
==============================
.. toctree::
:hidden:
:maxdepth: 5
将应用程序链接到内核
--------------------------------------------
在本章中,我们把应用程序的二进制镜像文件作为内核的数据段链接到内核里面,因此内核需要知道内含的应用程序的数量和它们的位置,这样才能够在运行时
对它们进行管理并能够加载到物理内存。
在 ``os/src/main.rs`` 中能够找到这样一行:
.. code-block:: rust
global_asm!(include_str!("link_app.S"));
这里我们引入了一段汇编代码 ``link_app.S`` ,它一开始并不存在,而是在构建的时候自动生成的。当我们使用 ``make run`` 让系统成功运行起来
之后,我们可以先来看一看里面的内容:
.. code-block:: asm
:linenos:
# os/src/link_app.S
.align 4
.section .data
.global _num_app
_num_app:
.quad 3
.quad app_0_start
.quad app_1_start
.quad app_2_start
.quad app_2_end
.section .data
.global app_0_start
.global app_0_end
app_0_start:
.incbin "../user/target/riscv64gc-unknown-none-elf/release/00hello_world.bin"
app_0_end:
.section .data
.global app_1_start
.global app_1_end
app_1_start:
.incbin "../user/target/riscv64gc-unknown-none-elf/release/01store_fault.bin"
app_1_end:
.section .data
.global app_2_start
.global app_2_end
app_2_start:
.incbin "../user/target/riscv64gc-unknown-none-elf/release/02power.bin"
app_2_end:
可以看到第 13 行开始的三个数据段分别插入了三个应用程序的二进制镜像,并且各自有一对全局符号 ``app_*_start, app_*_end`` 指示它们的
开始和结束位置。而第 3 行开始的另一个数据段相当于一个 64 位整数数组。数组中的第一个元素表示应用程序的数量,后面则按照顺序放置每个应用
程序的起始地址,最后一个元素放置最后一个应用程序的结束位置。这样每个应用程序的位置都能从该数组中相邻两个元素中得知。这个数组所在的位置
同样也由全局符号 ``_num_app`` 所指示。
这个文件是在 ``cargo build`` 的时候,由脚本 ``os/build.rs`` 控制生成的。有兴趣的读者可以参考其代码。
应用管理器
--------------------------
我们在 ``os`` 的 ``batch`` 子模块中实现一个应用管理器,它的功能是:保存应用数量和各自的位置信息,以及当前执行到第几个应用了。结构体定义
如下:
.. code-block:: rust
struct AppManager {
inner: RefCell<AppManagerInner>,
}
struct AppManagerInner {
num_app: usize,
current_app: usize,
app_start: [usize; MAX_APP_NUM + 1],
}
这里我们可以看出,上面提到的应用管理器需要保存和维护的信息都在 ``AppManagerInner`` 里面,而结构体 ``AppManager`` 里面只是保存了
一个指向 ``AppManagerInner`` 的 ``RefCell`` 智能指针。这样设计的原因在于:我们希望将 ``AppManager`` 实例化为一个全局变量使得
任何函数都可以直接访问,但是里面的 ``current_app`` 字段表示当前执行到了第几个应用,它会在系统运行期间发生变化。因此在声明全局变量
的时候一种自然的方法是利用 ``static mut``。但是在 Rust 中,任何对于 ``static mut`` 变量的访问都是 unsafe 的,而我们要尽可能
减少 unsafe 的使用来更多的让编译器负责安全性检查。
于是,我们利用 ``RefCell`` 来提供内部可变性,所谓的内部可变性就是指在我们只能拿到 ``AppManager`` 的不可变借用,意味着同样也只能
拿到 ``AppManagerInner`` 的不可变借用的情况下依然可以修改 ``AppManagerInner`` 里面的字段。
使用 ``RefCell::borrow/RefCell::borrow_mut`` 分别可以拿到 ``RefCell`` 里面内容的不可变借用/可变借用,
``RefCell`` 内部会运行时维护当前已有的借用状态并进行借用检查。于是 ``RefCell::borrow_mut`` 就是我们实现内部可变性的关键。
我们这样初始化 ``AppManager`` 的全局实例:
.. code-block:: rust
lazy_static! {
static ref APP_MANAGER: AppManager = AppManager {
inner: RefCell::new({
extern "C" { fn _num_app(); }
let num_app_ptr = _num_app as usize as *const usize;
let num_app = unsafe { num_app_ptr.read_volatile() };
let mut app_start: [usize; MAX_APP_NUM + 1] = [0; MAX_APP_NUM + 1];
let app_start_raw: &[usize] = unsafe {
core::slice::from_raw_parts(num_app_ptr.add(1), num_app + 1)
};
app_start[..=num_app].copy_from_slice(app_start_raw);
AppManagerInner {
num_app,
current_app: 0,
app_start,
}
}),
};
}
这里我们使用了外部库 ``lazy_static`` 提供的 ``lazy_static!`` 宏。要引入这个外部库,我们需要加入依赖:
.. code-block:: toml
# os/Cargo.toml
[dependencies]
lazy_static = { version = "1.4.0", features = ["spin_no_std"] }
``lazy_static!`` 宏提供了全局变量的运行时初始化功能。一般情况下,全局变量必须在编译期设置一个初始值,但是有些全局变量依赖于运行期间
才能得到的数据作为初始值。这导致这些全局变量需要在运行时发生变化,也即重新设置初始值之后才能使用。如果我们手动实现的话有诸多不便之处,
比如又需要把这种全局变量声明为 ``static mut`` 并衍生出很多 unsafe 。这种情况下我们可以使用 ``lazy_static!`` 宏来帮助我们解决
这个问题。这里我们借助 ``lazy_static!`` 声明了一个名为 ``APP_MANAGER`` 的 ``AppManager`` 全局实例,且只有在它第一次被使用到
的时候才会实际进行初始化工作。
初始化的逻辑很简单,就是找到 ``link_app.S`` 中提供的符号 ``_num_app`` ,并从这里开始解析出应用数量以及各个应用的开头地址。注意其中
对于切片类型的使用能够很大程度上简化编程。
因此,借助 Rust 核心库提供的 ``RefCell`` 和外部库 ``lazy_static!``,我们就能在避免 ``static mut`` 声明的情况下以更加 Rust 的
方式使用全局变量。
``AppManagerInner`` 的方法中, ``print_app_info/get_current_app/move_to_next_app`` 都相当简单直接,需要说明的是 ``load_app``:
.. code-block:: rust
:linenos:
unsafe fn load_app(&self, app_id: usize) {
if app_id >= self.num_app {
panic!("All applications completed!");
}
println!("[kernel] Loading app_{}", app_id);
// clear icache
llvm_asm!("fence.i" :::: "volatile");
// clear app area
(APP_BASE_ADDRESS..APP_BASE_ADDRESS + APP_SIZE_LIMIT).for_each(|addr| {
(addr as *mut u8).write_volatile(0);
});
let app_src = core::slice::from_raw_parts(
self.app_start[app_id] as *const u8,
self.app_start[app_id + 1] - self.app_start[app_id]
);
let app_dst = core::slice::from_raw_parts_mut(
APP_BASE_ADDRESS as *mut u8,
app_src.len()
);
app_dst.copy_from_slice(app_src);
}
这个方法负责将参数 ``app_id`` 对应的应用程序的二进制镜像加载到物理内存以 ``0x80040000`` 开头的位置,这个位置是批处理系统和应用程序
之间约定的常数,回忆上一小节中,我们也调整应用程序的内存布局以同一个地址开头。第 8 行开始,我们首先将一块内存清空,然后找到待加载应用
二进制镜像的位置,并将它复制到正确的位置。它本质上是数据从一块内存复制到另一块内存,从批处理系统的角度来看是将它数据段的一部分复制到了它
程序之外未知的地方。
注意第 7 行我们插入了一条奇怪的汇编指令 ``fence.i`` ,它是用来清理 i-cache 的。我们知道缓存是存储层级结构中提高访存速度的很重要一环。
而 CPU 对物理内存所做的缓存又分成 **数据缓存** (d-cache) 和 **指令缓存** (i-cache) 两部分,分别在 CPU 访存和取指的时候使用。在取指
的时候,对于一个指令地址, CPU 会先去 i-cache 里面看一下它是否在某个已缓存的缓存行内,如果在的话它就会直接从高速缓存中拿到指令而不是通过
总线和内存通信。通常情况下, CPU 会认为程序的代码段不会发生变化,因此 i-cache 是一种只读缓存。但在这里,我们会修改会被 CPU 取指的内存
区域,这会使得 i-cache 中含有与内存中不一致的内容。因此我们这里必须使用 ``fence.i`` 指令手动清空 i-cache ,让里面所有的内容全部失效,
才能够保证正确性。
``batch`` 子模块对外暴露出如下接口:
- ``init`` :调用 ``print_app_info`` 的时候第一次用到了全局变量 ``APP_MANAGER`` ,它也是在这个时候完成初始化;
- ``run_next_app`` :批处理系统的核心操作,即加载并运行下一个应用程序。当批处理系统完成初始化或者一个应用程序运行结束或出错之后会调用
该函数。我们下节再介绍其具体实现。
\ No newline at end of file
处理 Trap
=======================
.. toctree::
:hidden:
:maxdepth: 5
项目协作
==================
.. toctree::
:hidden:
:maxdepth: 4
1. 参考 `这里 <https://www.sphinx-doc.org/en/master/usage/installation.html>`_ 安装 Sphinx。
2. ``pip install sphinx_rtd_theme`` 安装 Read The Docs 主题。
3. ``pip install jieba`` 安装中文分词。
4. ``pip install sphinx-comments`` 安装 Sphinx 讨论区插件。
5. :doc:`/rest-example` 是 ReST 的一些基本语法,也可以参考已完成的文档。
6. 修改之后,在项目根目录下 ``make clean && make html`` 即可在 ``build/html/index.html`` 查看本地构建的主页。请注意在修改
章节目录结构之后需要 ``make clean`` 一下,不然可能无法正常更新。
7. 确认修改无误之后,在项目根目录下 ``make deploy`` 然后即可 ``git add -A && git commit -m && git push`` 上传到远程仓库。
如果出现冲突的话,请删除掉 ``docs`` 目录再进行 merge。
.. rCore-Tutorial-Book-v3 documentation master file, created by
sphinx-quickstart on Thu Oct 29 22:25:54 2020.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
rCore-Tutorial-Book 第三版
==================================================
.. toctree::
:maxdepth: 2
:caption: 正文
:hidden:
quickstart
chapter1/index
chapter2/index
.. toctree::
:maxdepth: 2
:caption: 附录
:hidden:
appendix-a/index
appendix-b/index
appendix-c/index
.. toctree::
:maxdepth: 2
:caption: 开发注记
:hidden:
rest-example
collaboration
欢迎来到 rCore-Tutorial-Book 第三版!
读者须知
---------------------
请先按照 :doc:`/quickstart` 中的说明完成环境配置,再从第一章开始阅读正文。
项目协作
----------------------
请参考 :doc:`/collaboration` 了解如何进行项目协作。
快速上手
============
.. toctree::
:hidden:
:maxdepth: 4
本节我们将完成环境配置并成功运行 rCore-Tutorial。
首先,请参考 `环境部署 <https://rcore-os.github.io/rCore-Tutorial-deploy/docs/pre-lab/env.html>`_ 安装 qemu 模拟器
和 rust。有一些小的变更如下:
- 将 ``riscv64imac-unknown-none-elf`` 改成 ``riscv64gc-unknown-none-elf``;
- 在使用文档中提供的链接下载 qemu 源码的时候,点击下载之后需要将链接中的 ``localhost`` 替换为 ``42.194.184.212:5212``。若仍然
不行的话,可以在 `SiFive 官网 <https://www.sifive.com/software>`_ 下载预编译的 qemu,比如
`Ubuntu 版本 qemu <https://static.dev.sifive.com/dev-tools/riscv-qemu-4.2.0-2020.04.0-x86_64-linux-ubuntu14.tar.gz>`_ 。
此外:
- 下载安装 `macOS 平台 <https://static.dev.sifive.com/dev-tools/riscv64-unknown-elf-gcc-8.3.0-2020.04.0-x86_64-apple-darwin.tar.gz?_ga=2.230260892.1021855761.1603335606-1708912445.1603335606>`_
或 `Ubuntu 平台 <https://static.dev.sifive.com/dev-tools/riscv64-unknown-elf-gcc-8.3.0-2020.04.0-x86_64-linux-ubuntu14.tar.gz?_ga=2.230260892.1021855761.1603335606-1708912445.1603335606>`_
的预编译版本 ``riscv64-unknown-elf-*`` 工具链,并添加到环境变量。可以在提示找不到的情况下再进行下载。
- 下载安装 `Linux 平台 <https://musl.cc/riscv64-linux-musl-cross.tgz>`_ 预编译版本的 ``riscv64-linux-musl-*`` 工具链,并
添加到环境变量。可以在提示找不到的情况下再进行下载。
- 如果想在 Maix 系列开发板上运行,需要安装 python 包 ``pyserial`` 和串口终端 miniterm 。
.. warning::
**FIXME: 提供一套开箱即用的 Docker 环境**
reStructuredText 基本语法
=====================================================
.. toctree::
:hidden:
:maxdepth: 4
.. note::
下面是一个注记。
`这里 <https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#hyperlinks>`_ 给出了在 Sphinx 中
外部链接的引入方法。注意,链接的名字和用一对尖括号包裹起来的链接地址之间必须有一个空格。链接最后的下划线和片段的后续内容之间也需要
有一个空格。
接下来是一个文档内部引用的例子。比如,戳 :doc:`/quickstart` 可以进入快速上手环节。
.. warning::
下面是一个警告。
.. code-block:: rust
:linenos:
:caption: 一段示例 Rust 代码
// 我们甚至可以插入一段 Rust 代码!
fn add(a: i32, b: i32) -> i32 { a + b }
下面继续我们的警告。
.. error::
下面是一个错误。
这里是一行数学公式 :math:`\sin(\alpha+\beta)=\sin\alpha\cos\beta+\cos\alpha\sin\beta`。
基本的文本样式:这是 *斜体* ,这是 **加粗** ,接下来的则是行间公式 ``a0`` 。它们的前后都需要有一个空格隔开其他内容,这个让人挺不爽的...
`这是 <https://docs.readthedocs.io/en/stable/guides/cross-referencing-with-sphinx.html#the-doc-role>`_ 一个全面展示
章节分布的例子,来自于 ReadTheDocs 的官方文档。事实上,现在我们也采用 ReadTheDocs 主题了,它非常美观大方。
下面是一个测试 gif。
.. image:: test.gif
接下来是一个表格的例子。
.. list-table:: RISC-V 函数调用跳转指令
:widths: 20 30
:header-rows: 1
:align: center
* - 指令
- 指令功能
* - :math:`\text{jal}\ \text{rd},\ \text{imm}[20:1]`
- :math:`\text{rd}\leftarrow\text{pc}+4`
:math:`\text{pc}\leftarrow\text{pc}+\text{imm}`
* - :math:`\text{jalr}\ \text{rd},\ (\text{imm}[11:0])\text{rs}`
- :math:`\text{rd}\leftarrow\text{pc}+4`
:math:`\text{pc}\leftarrow\text{rs}+\text{imm}`
\ No newline at end of file
/*
* basic.css
* ~~~~~~~~~
*
* Sphinx stylesheet -- basic theme.
*
* :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/
/* -- main layout ----------------------------------------------------------- */
div.clearer {
clear: both;
}
div.section::after {
display: block;
content: '';
clear: left;
}
/* -- relbar ---------------------------------------------------------------- */
div.related {
width: 100%;
font-size: 90%;
}
div.related h3 {
display: none;
}
div.related ul {
margin: 0;
padding: 0 0 0 10px;
list-style: none;
}
div.related li {
display: inline;
}
div.related li.right {
float: right;
margin-right: 5px;
}
/* -- sidebar --------------------------------------------------------------- */
div.sphinxsidebarwrapper {
padding: 10px 5px 0 10px;
}
div.sphinxsidebar {
float: left;
width: 230px;
margin-left: -100%;
font-size: 90%;
word-wrap: break-word;
overflow-wrap : break-word;
}
div.sphinxsidebar ul {
list-style: none;
}
div.sphinxsidebar ul ul,
div.sphinxsidebar ul.want-points {
margin-left: 20px;
list-style: square;
}
div.sphinxsidebar ul ul {
margin-top: 0;
margin-bottom: 0;
}
div.sphinxsidebar form {
margin-top: 10px;
}
div.sphinxsidebar input {
border: 1px solid #98dbcc;
font-family: sans-serif;
font-size: 1em;
}
div.sphinxsidebar #searchbox form.search {
overflow: hidden;
}
div.sphinxsidebar #searchbox input[type="text"] {
float: left;
width: 80%;
padding: 0.25em;
box-sizing: border-box;
}
div.sphinxsidebar #searchbox input[type="submit"] {
float: left;
width: 20%;
border-left: none;
padding: 0.25em;
box-sizing: border-box;
}
img {
border: 0;
max-width: 100%;
}
/* -- search page ----------------------------------------------------------- */
ul.search {
margin: 10px 0 0 20px;
padding: 0;
}
ul.search li {
padding: 5px 0 5px 20px;
background-image: url(file.png);
background-repeat: no-repeat;
background-position: 0 7px;
}
ul.search li a {
font-weight: bold;
}
ul.search li div.context {
color: #888;
margin: 2px 0 0 30px;
text-align: left;
}
ul.keywordmatches li.goodmatch a {
font-weight: bold;
}
/* -- index page ------------------------------------------------------------ */
table.contentstable {
width: 90%;
margin-left: auto;
margin-right: auto;
}
table.contentstable p.biglink {
line-height: 150%;
}
a.biglink {
font-size: 1.3em;
}
span.linkdescr {
font-style: italic;
padding-top: 5px;
font-size: 90%;
}
/* -- general index --------------------------------------------------------- */
table.indextable {
width: 100%;
}
table.indextable td {
text-align: left;
vertical-align: top;
}
table.indextable ul {
margin-top: 0;
margin-bottom: 0;
list-style-type: none;
}
table.indextable > tbody > tr > td > ul {
padding-left: 0em;
}
table.indextable tr.pcap {
height: 10px;
}
table.indextable tr.cap {
margin-top: 10px;
background-color: #f2f2f2;
}
img.toggler {
margin-right: 3px;
margin-top: 3px;
cursor: pointer;
}
div.modindex-jumpbox {
border-top: 1px solid #ddd;
border-bottom: 1px solid #ddd;
margin: 1em 0 1em 0;
padding: 0.4em;
}
div.genindex-jumpbox {
border-top: 1px solid #ddd;
border-bottom: 1px solid #ddd;
margin: 1em 0 1em 0;
padding: 0.4em;
}
/* -- domain module index --------------------------------------------------- */
table.modindextable td {
padding: 2px;
border-collapse: collapse;
}
/* -- general body styles --------------------------------------------------- */
div.body {
min-width: 450px;
max-width: 800px;
}
div.body p, div.body dd, div.body li, div.body blockquote {
-moz-hyphens: auto;
-ms-hyphens: auto;
-webkit-hyphens: auto;
hyphens: auto;
}
a.headerlink {
visibility: hidden;
}
a.brackets:before,
span.brackets > a:before{
content: "[";
}
a.brackets:after,
span.brackets > a:after {
content: "]";
}
h1:hover > a.headerlink,
h2:hover > a.headerlink,
h3:hover > a.headerlink,
h4:hover > a.headerlink,
h5:hover > a.headerlink,
h6:hover > a.headerlink,
dt:hover > a.headerlink,
caption:hover > a.headerlink,
p.caption:hover > a.headerlink,
div.code-block-caption:hover > a.headerlink {
visibility: visible;
}
div.body p.caption {
text-align: inherit;
}
div.body td {
text-align: left;
}
.first {
margin-top: 0 !important;
}
p.rubric {
margin-top: 30px;
font-weight: bold;
}
img.align-left, .figure.align-left, object.align-left {
clear: left;
float: left;
margin-right: 1em;
}
img.align-right, .figure.align-right, object.align-right {
clear: right;
float: right;
margin-left: 1em;
}
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
img.align-default, .figure.align-default {
display: block;
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left;
}
.align-center {
text-align: center;
}
.align-default {
text-align: center;
}
.align-right {
text-align: right;
}
/* -- sidebars -------------------------------------------------------------- */
div.sidebar {
margin: 0 0 0.5em 1em;
border: 1px solid #ddb;
padding: 7px;
background-color: #ffe;
width: 40%;
float: right;
clear: right;
overflow-x: auto;
}
p.sidebar-title {
font-weight: bold;
}
div.admonition, div.topic, blockquote {
clear: left;
}
/* -- topics ---------------------------------------------------------------- */
div.topic {
border: 1px solid #ccc;
padding: 7px;
margin: 10px 0 10px 0;
}
p.topic-title {
font-size: 1.1em;
font-weight: bold;
margin-top: 10px;
}
/* -- admonitions ----------------------------------------------------------- */
div.admonition {
margin-top: 10px;
margin-bottom: 10px;
padding: 7px;
}
div.admonition dt {
font-weight: bold;
}
p.admonition-title {
margin: 0px 10px 5px 0px;
font-weight: bold;
}
div.body p.centered {
text-align: center;
margin-top: 25px;
}
/* -- content of sidebars/topics/admonitions -------------------------------- */
div.sidebar > :last-child,
div.topic > :last-child,
div.admonition > :last-child {
margin-bottom: 0;
}
div.sidebar::after,
div.topic::after,
div.admonition::after,
blockquote::after {
display: block;
content: '';
clear: both;
}
/* -- tables ---------------------------------------------------------------- */
table.docutils {
margin-top: 10px;
margin-bottom: 10px;
border: 0;
border-collapse: collapse;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
table.align-default {
margin-left: auto;
margin-right: auto;
}
table caption span.caption-number {
font-style: italic;
}
table caption span.caption-text {
}
table.docutils td, table.docutils th {
padding: 1px 8px 1px 5px;
border-top: 0;
border-left: 0;
border-right: 0;
border-bottom: 1px solid #aaa;
}
table.footnote td, table.footnote th {
border: 0 !important;
}
th {
text-align: left;
padding-right: 5px;
}
table.citation {
border-left: solid 1px gray;
margin-left: 1px;
}
table.citation td {
border-bottom: none;
}
th > :first-child,
td > :first-child {
margin-top: 0px;
}
th > :last-child,
td > :last-child {
margin-bottom: 0px;
}
/* -- figures --------------------------------------------------------------- */
div.figure {
margin: 0.5em;
padding: 0.5em;
}
div.figure p.caption {
padding: 0.3em;
}
div.figure p.caption span.caption-number {
font-style: italic;
}
div.figure p.caption span.caption-text {
}
/* -- field list styles ----------------------------------------------------- */
table.field-list td, table.field-list th {
border: 0 !important;
}
.field-list ul {
margin: 0;
padding-left: 1em;
}
.field-list p {
margin: 0;
}
.field-name {
-moz-hyphens: manual;
-ms-hyphens: manual;
-webkit-hyphens: manual;
hyphens: manual;
}
/* -- hlist styles ---------------------------------------------------------- */
table.hlist {
margin: 1em 0;
}
table.hlist td {
vertical-align: top;
}
/* -- other body styles ----------------------------------------------------- */
ol.arabic {
list-style: decimal;
}
ol.loweralpha {
list-style: lower-alpha;
}
ol.upperalpha {
list-style: upper-alpha;
}
ol.lowerroman {
list-style: lower-roman;
}
ol.upperroman {
list-style: upper-roman;
}
:not(li) > ol > li:first-child > :first-child,
:not(li) > ul > li:first-child > :first-child {
margin-top: 0px;
}
:not(li) > ol > li:last-child > :last-child,
:not(li) > ul > li:last-child > :last-child {
margin-bottom: 0px;
}
ol.simple ol p,
ol.simple ul p,
ul.simple ol p,
ul.simple ul p {
margin-top: 0;
}
ol.simple > li:not(:first-child) > p,
ul.simple > li:not(:first-child) > p {
margin-top: 0;
}
ol.simple p,
ul.simple p {
margin-bottom: 0;
}
dl.footnote > dt,
dl.citation > dt {
float: left;
margin-right: 0.5em;
}
dl.footnote > dd,
dl.citation > dd {
margin-bottom: 0em;
}
dl.footnote > dd:after,
dl.citation > dd:after {
content: "";
clear: both;
}
dl.field-list {
display: grid;
grid-template-columns: fit-content(30%) auto;
}
dl.field-list > dt {
font-weight: bold;
word-break: break-word;
padding-left: 0.5em;
padding-right: 5px;
}
dl.field-list > dt:after {
content: ":";
}
dl.field-list > dd {
padding-left: 0.5em;
margin-top: 0em;
margin-left: 0em;
margin-bottom: 0em;
}
dl {
margin-bottom: 15px;
}
dd > :first-child {
margin-top: 0px;
}
dd ul, dd table {
margin-bottom: 10px;
}
dd {
margin-top: 3px;
margin-bottom: 10px;
margin-left: 30px;
}
dl > dd:last-child,
dl > dd:last-child > :last-child {
margin-bottom: 0;
}
dt:target, span.highlighted {
background-color: #fbe54e;
}
rect.highlighted {
fill: #fbe54e;
}
dl.glossary dt {
font-weight: bold;
font-size: 1.1em;
}
.optional {
font-size: 1.3em;
}
.sig-paren {
font-size: larger;
}
.versionmodified {
font-style: italic;
}
.system-message {
background-color: #fda;
padding: 5px;
border: 3px solid red;
}
.footnote:target {
background-color: #ffa;
}
.line-block {
display: block;
margin-top: 1em;
margin-bottom: 1em;
}
.line-block .line-block {
margin-top: 0;
margin-bottom: 0;
margin-left: 1.5em;
}
.guilabel, .menuselection {
font-family: sans-serif;
}
.accelerator {
text-decoration: underline;
}
.classifier {
font-style: oblique;
}
.classifier:before {
font-style: normal;
margin: 0.5em;
content: ":";
}
abbr, acronym {
border-bottom: dotted 1px;
cursor: help;
}
/* -- code displays --------------------------------------------------------- */
pre {
overflow: auto;
overflow-y: hidden; /* fixes display issues on Chrome browsers */
}
pre, div[class*="highlight-"] {
clear: both;
}
span.pre {
-moz-hyphens: none;
-ms-hyphens: none;
-webkit-hyphens: none;
hyphens: none;
}
div[class*="highlight-"] {
margin: 1em 0;
}
td.linenos pre {
border: 0;
background-color: transparent;
color: #aaa;
}
table.highlighttable {
display: block;
}
table.highlighttable tbody {
display: block;
}
table.highlighttable tr {
display: flex;
}
table.highlighttable td {
margin: 0;
padding: 0;
}
table.highlighttable td.linenos {
padding-right: 0.5em;
}
table.highlighttable td.code {
flex: 1;
overflow: hidden;
}
.highlight .hll {
display: block;
}
div.highlight pre,
table.highlighttable pre {
margin: 0;
}
div.code-block-caption + div {
margin-top: 0;
}
div.code-block-caption {
margin-top: 1em;
padding: 2px 5px;
font-size: small;
}
div.code-block-caption code {
background-color: transparent;
}
table.highlighttable td.linenos,
span.linenos,
div.doctest > div.highlight span.gp { /* gp: Generic.Prompt */
user-select: none;
}
div.code-block-caption span.caption-number {
padding: 0.1em 0.3em;
font-style: italic;
}
div.code-block-caption span.caption-text {
}
div.literal-block-wrapper {
margin: 1em 0;
}
code.descname {
background-color: transparent;
font-weight: bold;
font-size: 1.2em;
}
code.descclassname {
background-color: transparent;
}
code.xref, a code {
background-color: transparent;
font-weight: bold;
}
h1 code, h2 code, h3 code, h4 code, h5 code, h6 code {
background-color: transparent;
}
.viewcode-link {
float: right;
}
.viewcode-back {
float: right;
font-family: sans-serif;
}
div.viewcode-block:target {
margin: -1px -10px;
padding: 0 10px;
}
/* -- math display ---------------------------------------------------------- */
img.math {
vertical-align: middle;
}
div.body div.math p {
text-align: center;
}
span.eqno {
float: right;
}
span.eqno a.headerlink {
position: absolute;
z-index: 1;
}
div.math:hover a.headerlink {
visibility: visible;
}
/* -- printout stylesheet --------------------------------------------------- */
@media print {
div.document,
div.documentwrapper,
div.bodywrapper {
margin: 0 !important;
width: 100%;
}
div.sphinxsidebar,
div.related,
div.footer,
#top-link {
display: none;
}
}
\ No newline at end of file
.fa:before{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-style:normal;font-weight:400;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#FontAwesome) format("svg")}.fa:before{font-family:FontAwesome;font-style:normal;font-weight:400;line-height:1}.fa:before,a .fa{text-decoration:inherit}.fa:before,a .fa,li .fa{display:inline-block}li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-.8em}ul.fas li .fa{width:.8em}ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before,.icon-book:before{content:"\f02d"}.fa-caret-down:before,.icon-caret-down:before{content:"\f0d7"}.fa-caret-up:before,.icon-caret-up:before{content:"\f0d8"}.fa-caret-left:before,.icon-caret-left:before{content:"\f0d9"}.fa-caret-right:before,.icon-caret-right:before{content:"\f0da"}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60}.rst-versions .rst-current-version:after{clear:both;content:"";display:block}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}
\ No newline at end of file
此差异已折叠。
/*
* doctools.js
* ~~~~~~~~~~~
*
* Sphinx JavaScript utilities for all documentation.
*
* :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/
/**
* select a different prefix for underscore
*/
$u = _.noConflict();
/**
* make the code below compatible with browsers without
* an installed firebug like debugger
if (!window.console || !console.firebug) {
var names = ["log", "debug", "info", "warn", "error", "assert", "dir",
"dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace",
"profile", "profileEnd"];
window.console = {};
for (var i = 0; i < names.length; ++i)
window.console[names[i]] = function() {};
}
*/
/**
* small helper function to urldecode strings
*/
jQuery.urldecode = function(x) {
return decodeURIComponent(x).replace(/\+/g, ' ');
};
/**
* small helper function to urlencode strings
*/
jQuery.urlencode = encodeURIComponent;
/**
* This function returns the parsed url parameters of the
* current request. Multiple values per key are supported,
* it will always return arrays of strings for the value parts.
*/
jQuery.getQueryParameters = function(s) {
if (typeof s === 'undefined')
s = document.location.search;
var parts = s.substr(s.indexOf('?') + 1).split('&');
var result = {};
for (var i = 0; i < parts.length; i++) {
var tmp = parts[i].split('=', 2);
var key = jQuery.urldecode(tmp[0]);
var value = jQuery.urldecode(tmp[1]);
if (key in result)
result[key].push(value);
else
result[key] = [value];
}
return result;
};
/**
* highlight a given string on a jquery object by wrapping it in
* span elements with the given class name.
*/
jQuery.fn.highlightText = function(text, className) {
function highlight(node, addItems) {
if (node.nodeType === 3) {
var val = node.nodeValue;
var pos = val.toLowerCase().indexOf(text);
if (pos >= 0 &&
!jQuery(node.parentNode).hasClass(className) &&
!jQuery(node.parentNode).hasClass("nohighlight")) {
var span;
var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg");
if (isInSVG) {
span = document.createElementNS("http://www.w3.org/2000/svg", "tspan");
} else {
span = document.createElement("span");
span.className = className;
}
span.appendChild(document.createTextNode(val.substr(pos, text.length)));
node.parentNode.insertBefore(span, node.parentNode.insertBefore(
document.createTextNode(val.substr(pos + text.length)),
node.nextSibling));
node.nodeValue = val.substr(0, pos);
if (isInSVG) {
var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
var bbox = node.parentElement.getBBox();
rect.x.baseVal.value = bbox.x;
rect.y.baseVal.value = bbox.y;
rect.width.baseVal.value = bbox.width;
rect.height.baseVal.value = bbox.height;
rect.setAttribute('class', className);
addItems.push({
"parent": node.parentNode,
"target": rect});
}
}
}
else if (!jQuery(node).is("button, select, textarea")) {
jQuery.each(node.childNodes, function() {
highlight(this, addItems);
});
}
}
var addItems = [];
var result = this.each(function() {
highlight(this, addItems);
});
for (var i = 0; i < addItems.length; ++i) {
jQuery(addItems[i].parent).before(addItems[i].target);
}
return result;
};
/*
* backward compatibility for jQuery.browser
* This will be supported until firefox bug is fixed.
*/
if (!jQuery.browser) {
jQuery.uaMatch = function(ua) {
ua = ua.toLowerCase();
var match = /(chrome)[ \/]([\w.]+)/.exec(ua) ||
/(webkit)[ \/]([\w.]+)/.exec(ua) ||
/(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) ||
/(msie) ([\w.]+)/.exec(ua) ||
ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) ||
[];
return {
browser: match[ 1 ] || "",
version: match[ 2 ] || "0"
};
};
jQuery.browser = {};
jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true;
}
/**
* Small JavaScript module for the documentation.
*/
var Documentation = {
init : function() {
this.fixFirefoxAnchorBug();
this.highlightSearchWords();
this.initIndexTable();
if (DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) {
this.initOnKeyListeners();
}
},
/**
* i18n support
*/
TRANSLATIONS : {},
PLURAL_EXPR : function(n) { return n === 1 ? 0 : 1; },
LOCALE : 'unknown',
// gettext and ngettext don't access this so that the functions
// can safely bound to a different name (_ = Documentation.gettext)
gettext : function(string) {
var translated = Documentation.TRANSLATIONS[string];
if (typeof translated === 'undefined')
return string;
return (typeof translated === 'string') ? translated : translated[0];
},
ngettext : function(singular, plural, n) {
var translated = Documentation.TRANSLATIONS[singular];
if (typeof translated === 'undefined')
return (n == 1) ? singular : plural;
return translated[Documentation.PLURALEXPR(n)];
},
addTranslations : function(catalog) {
for (var key in catalog.messages)
this.TRANSLATIONS[key] = catalog.messages[key];
this.PLURAL_EXPR = new Function('n', 'return +(' + catalog.plural_expr + ')');
this.LOCALE = catalog.locale;
},
/**
* add context elements like header anchor links
*/
addContextElements : function() {
$('div[id] > :header:first').each(function() {
$('<a class="headerlink">\u00B6</a>').
attr('href', '#' + this.id).
attr('title', _('Permalink to this headline')).
appendTo(this);
});
$('dt[id]').each(function() {
$('<a class="headerlink">\u00B6</a>').
attr('href', '#' + this.id).
attr('title', _('Permalink to this definition')).
appendTo(this);
});
},
/**
* workaround a firefox stupidity
* see: https://bugzilla.mozilla.org/show_bug.cgi?id=645075
*/
fixFirefoxAnchorBug : function() {
if (document.location.hash && $.browser.mozilla)
window.setTimeout(function() {
document.location.href += '';
}, 10);
},
/**
* highlight the search words provided in the url in the text
*/
highlightSearchWords : function() {
var params = $.getQueryParameters();
var terms = (params.highlight) ? params.highlight[0].split(/\s+/) : [];
if (terms.length) {
var body = $('div.body');
if (!body.length) {
body = $('body');
}
window.setTimeout(function() {
$.each(terms, function() {
body.highlightText(this.toLowerCase(), 'highlighted');
});
}, 10);
$('<p class="highlight-link"><a href="javascript:Documentation.' +
'hideSearchWords()">' + _('Hide Search Matches') + '</a></p>')
.appendTo($('#searchbox'));
}
},
/**
* init the domain index toggle buttons
*/
initIndexTable : function() {
var togglers = $('img.toggler').click(function() {
var src = $(this).attr('src');
var idnum = $(this).attr('id').substr(7);
$('tr.cg-' + idnum).toggle();
if (src.substr(-9) === 'minus.png')
$(this).attr('src', src.substr(0, src.length-9) + 'plus.png');
else
$(this).attr('src', src.substr(0, src.length-8) + 'minus.png');
}).css('display', '');
if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) {
togglers.click();
}
},
/**
* helper function to hide the search marks again
*/
hideSearchWords : function() {
$('#searchbox .highlight-link').fadeOut(300);
$('span.highlighted').removeClass('highlighted');
},
/**
* make the url absolute
*/
makeURL : function(relativeURL) {
return DOCUMENTATION_OPTIONS.URL_ROOT + '/' + relativeURL;
},
/**
* get the current relative url
*/
getCurrentURL : function() {
var path = document.location.pathname;
var parts = path.split(/\//);
$.each(DOCUMENTATION_OPTIONS.URL_ROOT.split(/\//), function() {
if (this === '..')
parts.pop();
});
var url = parts.join('/');
return path.substring(url.lastIndexOf('/') + 1, path.length - 1);
},
initOnKeyListeners: function() {
$(document).keydown(function(event) {
var activeElementType = document.activeElement.tagName;
// don't navigate when in search box, textarea, dropdown or button
if (activeElementType !== 'TEXTAREA' && activeElementType !== 'INPUT' && activeElementType !== 'SELECT'
&& activeElementType !== 'BUTTON' && !event.altKey && !event.ctrlKey && !event.metaKey
&& !event.shiftKey) {
switch (event.keyCode) {
case 37: // left
var prevHref = $('link[rel="prev"]').prop('href');
if (prevHref) {
window.location.href = prevHref;
return false;
}
case 39: // right
var nextHref = $('link[rel="next"]').prop('href');
if (nextHref) {
window.location.href = nextHref;
return false;
}
}
}
});
}
};
// quick alias for translations
_ = Documentation.gettext;
$(document).ready(function() {
Documentation.init();
});
var DOCUMENTATION_OPTIONS = {
URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'),
VERSION: '0.1',
LANGUAGE: 'zh_CN',
COLLAPSE_INDEX: false,
BUILDER: 'html',
FILE_SUFFIX: '.html',
LINK_SUFFIX: '.html',
HAS_SOURCE: true,
SOURCELINK_SUFFIX: '.txt',
NAVIGATION_WITH_KEYS: false
};
\ No newline at end of file
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册