# practice-toy-os-riscv-rust
**Repository Path**: hemashushuzsd/practice-toy-os-riscv-rust
## Basic Information
- **Project Name**: practice-toy-os-riscv-rust
- **Description**: 本项目是教程《rCore-Tutorial-Book 第三版》的阅读笔记和保姆式详细攻略 😄,原教程讲述了如何一步一步地 **从零开始** 用 Rust 语言写一个基于 RISC-V 架构的 _类 Unix 内核_。
- **Primary Language**: Unknown
- **License**: GPL-3.0
- **Default Branch**: main
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 1
- **Forks**: 1
- **Created**: 2022-07-14
- **Last Updated**: 2024-09-29
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
Practice Toy OS - rCore
本项目是教程 [《rCore-Tutorial-Book 第三版》](https://rcore-os.github.io/rCore-Tutorial-Book-v3/index.html) 的阅读笔记和保姆式详细攻略 😄,原教程讲述了如何一步一步地 **从零开始** 用 Rust 语言写一个基于 RISC-V 架构的 _类 Unix 内核_。
根据原教程的讲解,我将每一章的代码都整理成一个独立的文件夹。你可以一边阅读原教程,一边用你喜欢的代码编辑器切入相应的章节文件夹,试试运行看看运行的结果。
实际上官方也有每个章节的代码 [rCore-Tutorial-v3](https://github.com/rcore-os/rCore-Tutorial-v3),不过该代码仓库将每个章节的代码组织为 Git 的分支,有时需要同时打开多个章节的代码对比查阅时会稍显不便。另外我也在原来的代码基础上 **添加了些许额外的注释,以及一些扩展资料的链接**。
- [开发环境的搭建和配置](#开发环境的搭建和配置)
- [在当前系统里配置开发环境](#在当前系统里配置开发环境)
- [使用 Docker 搭建开发环境](#使用-docker-搭建开发环境)
- [RISC-V 指令和裸机汇编程序](#risc-v-指令和裸机汇编程序)
- [汇编](#汇编)
- [链接](#链接)
- [运行](#运行)
- [调试](#调试)
- [编译和运行各章的代码](#编译和运行各章的代码)
- [Chapter 1](#chapter-1)
- [Chapter 2](#chapter-2)
- [Chapter 3](#chapter-3)
- [Chapter 4](#chapter-4)
- [参考链接](#参考链接)
## 开发环境的搭建和配置
如果要编译和运行教程的所有程序,开发环境必须有以下工具:
1. Rust
2. Rust 的 `riscv64gc-unknown-none-elf` 编译目标
3. Qemu 7.0
4. RISC-V toolchains
如果不想在当前系统上安装以上工具,也可以在 Docker 里搭建该开发环境。在 Docker 里编译和运行所有程序,教程学习完毕之后把该 Docker Image 删掉即可,对于当前系统来说就像什么事都没发生过一样。
### 在当前系统里配置开发环境
操作系统建议使用 Arch Linux,该发行版的软件包数量巨多而且版本都是最新的,上面提到的工具直接用系统包管理工具安装即可,省去很多麻烦。
如果要在其他发行版或者系统安装,则 [根据教程的指引](https://rcore-os.github.io/rCore-Tutorial-Book-v3/chapter0/5setup-devel-env.html) 下载和安装各个工具即可。
需注意 RISC-V toolchains 的最新版本的仓库地址是 ,如果不需要调试程序,不安装这个工具链也可以。
### 使用 Docker 搭建开发环境
准确来说是构建一个 Docker Image,然后 `run` 这个 Image 并在里面完成教程所述的所有程序的开发和运行。如果你不想更改当前的系统,或者安装一些平时用不着的程序,推荐采用 Docker 搭建开发环境这种方式(前提是你得接受在系统里安装 Docker 或者 Podman 😁)。
我在本项目的 `docker-image` 目录里面放置了两个子目录:`mini` 和 `full`,进入其中的一个目录执行命令(或者执行目录当中的脚本 `build-image`):
`$ docker build -t rust-riscv .`
然后 Docker 会开始构建,构建完成后执行命令:
`$ docker image list`
检查是否存在一项 `rust-riscv`,若存在则表示构建成功。
因为 RISC-V toolchains 的体积较大,所以 `mini` 版默认不安装这个工具链,如果需要安装可以在 Image 构建完成之后,进入该 Container 然后使用下面的命令手动安装:
```bash
$ cd /opt
$ wget https://github.com/riscv-collab/riscv-gnu-toolchain/releases/download/2022.06.10/riscv64-elf-ubuntu-20.04-nightly-2022.06.10-nightly.tar.gz
$ tar xzf riscv64-elf-ubuntu-20.04-nightly-2022.06.10-nightly.tar.gz
$ rm riscv64-elf-ubuntu-20.04-nightly-2022.06.10-nightly.tar.gz
$ echo 'export PATH=/opt/riscv/bin:$PATH' >> ~/.bashrc
$ . ~/.bashrc
```
## RISC-V 指令和裸机汇编程序
现在我们有 RISC-V 的编译工具以及运行和调试程序的模拟器 QEMU,现在可以写最原始的 `Hello World` 程序测试以下,所谓最原始的程序,是指在没有引导程序,没有操作系统的情况下,让机器直接执行指令,这种程序叫做 Bare-metal 程序(裸机程序)。
写这种程序,我们只需一个汇编器,把汇编代码翻译成(二进制指令)目标文件,然后扔给 QEMU 运行即可。极端情况下,比如仅仅想执行几个指令,我们也可以直接写这些指令的二进制到一个文件里,然后把这个文件扔给 QEMU 运行(说笑的,不过的确是可行的)。通过裸机程序,我们可以学习 RISC-V 指令以及基本知识。
### 汇编
新建一个文件,名称为 `first.s`,内容如下:
```asm
.globl _start
_start:
li s1, 0x10000000 # set s1 = 0x1000_0000
li s2, 0x41 # set s2 = 0x48
sb s2, 0(s1) # set memory[s1 + 0] = s2
```
简单讲解:`.globl _start` 定义个全局 `符号`,类比 "一个库的导出函数(的名称),可供外部查看和调用",`_start` 定义一个位置,类比 `自动行号`。最后 3 行是 RISC-V 指令,作用看句末的注释。
关于 RISC-V ISA 的基本知识,可以参考 [《RISC-V 手册》](http://riscvbook.com/chinese/RISC-V-Reader-Chinese-v2p1.pdf),有关指令更详细的资料可以参考 [《RISC-V 规范》](https://riscv.org/technical/specifications/) 以及 [《RISC-V Assembly Programmer's Manual》](https://github.com/riscv-non-isa/riscv-asm-manual/blob/master/riscv-asm.md)。
下面命令将汇编源码汇编(动词)为目标文件:
```bash
$ riscv64-unknown-elf-as -g -o first.o first.s
```
`g` 参数用于生成调试信息。
### 链接
新建一个文件,名称为 `default.lds`,内容如下:
```ld
OUTPUT_ARCH(riscv)
ENTRY(_start)
BASE_ADDRESS = 0x80000000;
SECTIONS
{
. = BASE_ADDRESS;
.text : {
*(.text.entry)
*(.text .text.*)
}
.rodata : {
*(.rodata .rodata.*)
}
.data : {
. = ALIGN(4096);
*(.sdata .sdata.*)
*(.data .data.*)
}
.bss :{
*(.sbss .sbss.*)
*(.bss .bss.*)
}
}
```
这是一个链接脚本,可见一个常见可执行文件的 `.text`, `.rodata`, `.data`, `.bss` 等段的定义,其中 `BASE_ADDRESS` 用于指定程序的开始位置,之所以值为 0x8000_0000 是因为模拟器程序 `qemu-system-riscv64 -machine virt` 启动后,PC 寄存器的值为 0x1000,也就是说位置 0x1000 的指令将会第一个被执行,通过调试可以发现该处的指令如下:
```asm
0x1000: auipc t0,0x0 # set t0 = $pc + sign_extend(immediate[31:12] << 12)
# 现在 t0 == 0x1000,即当前指令的位置
0x1004: addi a2,t0,40 # set a2 = t0 + 0x28
# 现在 a2 == 0x1028
# 暂时不用理会
0x1008: csrr a0,mhartid # Hart ID Register (mhartid), 运行当前代码的硬件线程(hart)的 ID
# 现在 a0 == 0
# 暂时不用理会
0x100c: ld a1,32(t0) # set a1 = int64(t0 + 0x20)
# 现在 a1 == 0x87000000
# 可以使用命令 `x/2wx 0x1020` 查看
# 暂时不用理会
0x1010: ld t0,24(t0) # set t0 = int64(t0 + 0x18)
# 现在 t0 == 0x80000000
# 可以使用命令 `x/2wx 0x1018` 查看
0x1014: jr t0 # 跳转到 0x80000000
0x1018: 0x0000
0x101a: .2byte 0x8000
0x101c: 0x0000
0x101e: 0x0000
```
其中 `jr t0` 表示即将会跳到寄存器 `t0` 的值所指向的位置。
用下面的命令链接(动词)得出目标文件:
```bash
$ riscv64-unknown-elf-ld -T default.lds -o first first.o
```
### 运行
使用 QEMU 运行上一步得到的目标文件:
```bash
$ qemu-system-riscv64 \
-machine virt \
-nographic \
-bios none \
-kernel first
```
应该能看到一个字符 `A` 输出。
按 `Ctrl+a` 然后再按 `x` 退出 QEMU (温馨提示,退出 QEMU 不是按 `Ctrl+x`,也不是 `:q`)
### 调试
在运行 QEMU 的命令后面加上 `-s -S` 能启动 GDB 调试服务端,打开另外一个终端窗口,运行下面命令进入 GDB 调试客户端
```bash
$ riscv64-elf-gdb
```
进入后输入 `target remote :1234` 连接服务端。
**调试命令**
- 命令 `x/10i $pc`
查看 $pc 位置的 10 条指令,命令 `x` 用于查看内存
- 命令 `si`
逐条指令运行
- 命令 `b *0x80000000`
设置断点
- 命令 `c`
可以持续运行程序直到遇到断点
- 命令 `p/d $x1`
打印 x1 寄存器的数值
- 命令 `p/x $sp`
同样也是打印寄存器的数值,以 16 进制格式打印
- 命令 `i r`
列出所有寄存器的值。
- 命令 `q`
退出调试环境
输入命令 `help` 获取各个命令的帮助信息,比如:
`help info`
会列出 `info` 命令的详细用法。如果有时忘记命令的完整名称,可以在输入前面的一两个字符时,按下 `tab` 键列出提示或者自动补完,比如输入 `info reg` 按下 `tab` 键,会自动补完为 `info registers`。
对于高频次使用的命令,只需输入命令的第一个字符即可,比如 `info` 可以输入 `i` 代替,同样 `i registers` 可以输入 `i r` 代替。
> 注,在 GDB 里是没法直接输入和执行 RISC-V 指令的,所以如果想要测试一些 RISC-V 指令,需要编写一个简单的汇编程序,然后再使用上述的步骤运行和调试。
"Hello world!" 程序的代码在 [bare-metal-asm/hello.s](bare-metal-asm/hello.s)。
### 深入了解 RISC-V 指令集及工作原理
可以借助图形化的 RISC-V CPU 模拟工具用于学习和实践 RISC-V 的指令集,下面推荐两个:
- QtRVSim
https://github.com/cvut/qtrvsim
- RARS
https://github.com/TheThirdOne/rars
比起 QEMU,它们能够比较直观地显示程序、内存、寄存器的内容,甚至能够显示 RTL 级(寄存器级,可以简单地认为是数字电子电路) CPU 状态。有直观的工具辅助学习,往往可以事半功倍。
## 编译和运行各章的代码
> 以下内容请按顺序阅读和运行,即必须先完成第一章的每一个步骤,才能进入第二章,如此类推。
在开始编译和运行各章的代码之前,首先切换到本项目的首层目录,然后你会看到诸如 `ch1`,`ch2`,`ch3` …… 等子目录,它们对应着各章的程序。
如果你是 Docker 的开发环境,则运行命令:
```bash
docker run -it --rm \
--name rust-riscv \
--mount type=bind,source=$PWD,target=/mnt \
rust-riscv
```
该命令会创建一个容器,进入之后是一个 Bash shell,切换到 `/mnt` 目录即可看到 `ch1`,`ch2` …… 等子目录,这时候跟在当前系统里直接搭建的开发环境是一致的。
### Chapter 1
1. 进入 `ch1` 目录
2. 运行脚本 `build-bin` 开始编译
3. 运行脚本 `run` 运行程序,看到 `panic at (src/main.rs:86) Shutdown machine!` 字样则表示成功。
> 这些脚本只是为了简化命令,大部分脚本的内容都是非常简单的。如果你想知道脚本里面具体执行了什么,可以用文本编辑器打开查看。
教程第一章里有一个使用 GDB 进入调试环境的环节,这个步骤可以跳过。如果你还是想完整体验完所有环节,则运行脚本 `start-debug-server` 开始调试的服务端,接下来则根据开发环境的不同而不同:
* 对于在当前系统直接进行开发的,打开另一个终端窗口,然后在里面运行 `ch1` 目录当中的脚本 `start-debug-client-archlinux`。
* 对于在 Docker 里面进行开发的,打开另一个终端窗口,然后在里面运行本项目首层目录当中的脚本 `join-docker`,进入到刚才的容器,切换到 `/mnt` 目录,然后进入 `ch1` 目录,再运行脚本 `start-debug-client-docker`。
### Chapter 2
1. 进入 `ch2` 目录
2. 进入 `user` 目录
3. 运行脚本 `build-app` 开始编译 5 个用户应用程序
4. 运行脚本 `run` 会通过用户态模拟器 `qemu-riscv64` 来运行刚才编译出的应用程序。注意几个程序运行之后会显示错误信息,这是正常的,能看到 `Hello, world!` 和 `Test power OK!` 字样则表示成功。
5. 返回上一级目录,进入 `os` 目录
6. 运行脚本 `build-bin` 开始编译
7. 运行脚本 `run` 运行程序,看到 `All applications completed!` 字样则表示成功。
### Chapter 3
::TODO
### Chapter 4
::TODO
## 参考链接
- 《Writing an OS in Rust》
Blog OS
https://os.phil-opp.com/
https://github.com/rustcc/writing-an-os-in-rust
-
https://osblog.stephenmarz.com/index.html
- 《Rust Raspberry Pi OS》
https://github.com/rust-embedded/rust-raspberrypi-OS-tutorials
- 《OS from scratch》
C 语言, x86
https://github.com/cfenollosa/os-tutorial
https://www.cs.bham.ac.uk/~exr/lectures/opsys/10_11/lectures/os-dev.pdf
https://littleosbook.github.io/
- 《Xv6 - RISC-V》
C 语言
https://pdos.csail.mit.edu/6.828/2021/xv6.html
- 《rCore》
http://rcore-os.cn/rCore-Tutorial-Book-v3/index.html
https://github.com/skyzh/core-os-riscv.git
- OSDev.org
https://wiki.osdev.org/Main_Page