add os[1-8]-ref for os refereces, add guide, add README

This commit is contained in:
Yu Chen
2022-06-27 22:22:44 +08:00
parent 7c1679774c
commit d752a67137
360 changed files with 32863 additions and 1 deletions

View File

@@ -0,0 +1,96 @@
引言
=====================
本章导读
--------------------------
大多数程序员的职业生涯都从 ``Hello, world!`` 开始。
.. code-block::
printf("Hello world!\n");
cout << "Hello world!\n";
print("Hello world!")
System.out.println("Hello world!");
echo "Hello world!"
println!("Hello world!");
然而,要用几行代码向世界问好,并不像表面上那么简单。
``Hello, world!`` 程序能够编译运行,靠的是以 **编译器** 为主的开发环境和以 **操作系统** 为主的执行环境。
在本章中,我们将抽丝剥茧,一步步让 ``Hello, world!`` 程序脱离其依赖的执行环境,
编写一个能打印 ``Hello, world!`` 的 OS。这趟旅途将让我们对应用程序及其执行环境有更深入的理解。
.. attention::
实验指导书存在的目的是帮助读者理解框架代码。
为便于测试,完成编程实验时,请以框架代码为基础,不必跟着文档从零开始编写内核。
为了做到这一步,首先需要让程序不依赖于标准库,
并通过编译。
接下来要让脱离了标准库的程序能输出(即支持 ``println!``),这对程序的开发和调试至关重要。
我们先在用户态下实现该功能,在 `此处 <https://github.com/LearningOS/rCore-Tutorial-Book-2021Autumn/tree/ch2-U-nostd>`_ 获取相关代码。
最后把程序移植到内核态,构建在裸机上支持输出的最小运行时环境。
实践体验
---------------------------
本章一步步实现了支持打印字符串的简单操作系统。
获取本章代码:
.. code-block:: console
$ git clone https://github.com/LearningOS/rCore-Tutorial-Code-2022S
$ cd rCore-Tutorial-Code-2022S
$ git checkout ch1
运行本章代码,并设置日志级别为 ``TRACE``
.. code-block:: console
$ cd os
$ make run LOG=TRACE
预期输出:
.. figure:: color-demo.png
:align: center
除了 ``Hello, world!`` 之外还有一些额外的信息,最后关机。
本章代码树
------------------------------------------------
.. code-block::
├── bootloader (内核依赖的运行在 M 特权级的 SBI 实现,本项目中我们使用 RustSBI)
│   └── rustsbi-qemu.bin
├── os
│   ├── Cargo.toml (cargo 项目配置文件)
│   ├── Makefile
│   └── src
│   ├── console.rs (将打印字符的 SBI 接口进一步封装实现更加强大的格式化输出)
│   ├── entry.asm (设置内核执行环境的的一段汇编代码)
│   ├── lang_items.rs (需要我们提供给 Rust 编译器的一些语义项,目前包含内核 panic 时的处理逻辑)
│   ├── linker.ld (控制内核内存布局的链接脚本以使内核运行在 qemu 虚拟机上)
│   ├── logging.rs (为本项目实现了日志功能)
│   ├── main.rs (内核主函数)
│   └── sbi.rs (封装底层 SBI 实现提供的 SBI 接口)
└── rust-toolchain (整个项目的工具链版本)
cloc os
-------------------------------------------------------------------------------
Language files blank comment code
-------------------------------------------------------------------------------
Rust 5 25 6 155
make 1 11 4 34
Assembly 1 1 0 11
TOML 1 2 1 7
-------------------------------------------------------------------------------
SUM: 8 39 11 207
-------------------------------------------------------------------------------

View File

@@ -0,0 +1,120 @@
应用程序执行环境与平台支持
================================================
.. toctree::
:hidden:
:maxdepth: 5
执行应用程序
-------------------------------
我们先从最简单的 Rust ``Hello, world`` 程序开始,用 Cargo 工具创建 Rust 项目。
.. code-block:: console
$ cargo new os
此时,项目的文件结构如下:
.. code-block:: console
$ tree os
os
├── Cargo.toml
└── src
└── main.rs
1 directory, 2 files
其中 ``Cargo.toml`` 中保存了项目的库依赖、作者信息等。
cargo 为我们准备好了 ``Hello world!`` 源代码:
.. code-block:: rust
:linenos:
:caption: 最简单的 Rust 应用
fn main() {
println!("Hello, world!");
}
输入 ``cargo run`` 构建并运行项目:
.. code-block:: console
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!`` ,但为了打印出 ``Hello, world!``,我们需要的不止几行源代码。
理解应用程序执行环境
-------------------------------
在现代通用操作系统(如 Linux上运行应用程序需要多层次的执行环境栈支持
.. figure:: app-software-stack.png
:align: center
应用程序执行环境栈:图中的白色块自上而下表示各级执行环境,黑色块则表示相邻两层执行环境之间的接口。
下层作为上层的执行环境,支持上层代码运行。
我们的应用程序通过调用标准库或第三方库提供的接口,仅需少量源代码就能完成复杂的功能;
``Hello, world!`` 程序调用的 ``println!`` 宏就是由 Rust 标准库 std 和 GNU Libc 等提供的。
这些库属于应用程序的 **执行环境** (Execution Environment),而它们的实现又依赖于操作系统提供的系统调用。
平台与目标三元组
---------------------------------------
编译器在编译、链接得到可执行文件时需要知道,程序要在哪个 **平台** (Platform) 上运行,
**目标三元组** (Target Triplet) 描述了目标平台的 CPU 指令集、操作系统类型和标准运行时库。
我们研究一下现在 ``Hello, world!`` 程序的目标三元组是什么:
.. code-block:: console
$ rustc --version --verbose
rustc 1.61.0-nightly (68369a041 2022-02-22)
binary: rustc
commit-hash: 68369a041cea809a87e5bd80701da90e0e0a4799
commit-date: 2022-02-22
host: x86_64-unknown-linux-gnu
release: 1.61.0-nightly
LLVM version: 14.0.0
其中 host 一项表明默认目标平台是 ``x86_64-unknown-linux-gnu``
CPU 架构是 x86_64CPU 厂商是 unknown操作系统是 linux运行时库是 gnu libc。
接下来,我们希望把 ``Hello, world!`` 移植到 RICV 目标平台 ``riscv64gc-unknown-none-elf`` 上运行。
.. note::
``riscv64gc-unknown-none-elf`` 的 CPU 架构是 riscv64gc厂商是 unknown操作系统是 none
elf 表示没有标准的运行时库。没有任何系统调用的封装支持,但可以生成 ELF 格式的执行程序。
我们不选择有 linux-gnu 支持的 ``riscv64gc-unknown-linux-gnu``,是因为我们的目标是开发操作系统内核,而非在 linux 系统上运行的应用程序。
修改目标平台
----------------------------------
将程序的目标平台换成 ``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
报错的原因是目标平台上确实没有 Rust 标准库 std也不存在任何受 OS 支持的系统调用。
这样的平台被我们称为 **裸机平台** (bare-metal)。
幸运的是,除了 std 之外Rust 还有一个不需要任何操作系统支持的核心库 core
它包含了 Rust 语言相当一部分核心机制,可以满足本门课程的需求。
有很多第三方库也不依赖标准库 std而仅仅依赖核心库 core。
为了以裸机平台为目标编译程序,我们要将对标准库 std 的引用换成核心库 core。

View File

@@ -0,0 +1,158 @@
.. _term-remove-std:
移除标准库依赖
==========================
.. toctree::
:hidden:
:maxdepth: 5
首先在 ``os`` 目录下新建 ``.cargo`` 目录,并在这个目录下创建 ``config`` 文件,输入如下内容:
.. code-block:: toml
# os/.cargo/config
[build]
target = "riscv64gc-unknown-none-elf"
这将使 cargo 工具在 os 目录下默认会使用 riscv64gc-unknown-none-elf 作为目标平台。
这种编译器运行的平台x86_64与可执行文件运行的目标平台不同的情况称为 **交叉编译** (Cross Compile)。
移除 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 的系统调用。
无论如何,我们先将这行代码注释掉。
提供语义项 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
标准库 std 提供了 Rust 错误处理函数 ``#[panic_handler]``,其大致功能是打印出错位置和原因并杀死当前应用。
但核心库 core 并没有提供这项功能,得靠我们自己实现。
新建一个子模块 ``lang_items.rs``,在里面编写 panic 处理函数,通过标记 ``#[panic_handler]`` 告知编译器采用我们的实现:
.. code-block:: rust
// os/src/lang_items.rs
use core::panic::PanicInfo;
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
目前我们遇到错误什么都不做,只在原地 ``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`` 的语义项。
``start`` 语义项代表了标准库 std 在执行应用程序之前需要进行的一些初始化工作。由于我们禁用了标准库,编译器也就找不到这项功能的实现了。
``main.rs`` 的开头加入设置 ``#![no_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 {}
}
分析被移除标准库的程序
-----------------------------
我们可以通过一些工具来分析目前的程序:
.. code-block:: console
[文件格式]
$ file target/riscv64gc-unknown-none-elf/debug/os
target/riscv64gc-unknown-none-elf/debug/os: ELF 64-bit LSB executable, UCB RISC-V, ......
[文件头信息]
$ rust-readobj -h target/riscv64gc-unknown-none-elf/debug/os
File: target/riscv64gc-unknown-none-elf/debug/os
Format: elf64-littleriscv
Arch: riscv64
AddressSize: 64bit
......
Type: Executable (0x2)
Machine: EM_RISCV (0xF3)
Version: 1
Entry: 0x0
......
}
[反汇编导出汇编程序]
$ rust-objdump -S target/riscv64gc-unknown-none-elf/debug/os
target/riscv64gc-unknown-none-elf/debug/os: file format elf64-littleriscv
通过 ``file`` 工具对二进制程序 ``os`` 的分析可以看到,它好像是一个合法的 RV64 执行程序,
``rust-readobj`` 工具告诉我们它的入口地址 Entry 是 ``0``
再通过 ``rust-objdump`` 工具把它反汇编,没有生成任何汇编代码。
可见,这个二进制程序虽然合法,但它是一个空程序,原因是缺少了编译器规定的入口函数 ``_start``
从下一节开始,我们将着手实现本节移除的、由用户态执行环境提供的功能。
.. note::
本节内容部分参考自 `BlogOS 的相关章节 <https://os.phil-opp.com/freestanding-rust-binary/>`_

View File

@@ -0,0 +1,282 @@
.. _term-print-userminienv:
构建用户态执行环境
=================================
.. toctree::
:hidden:
:maxdepth: 5
.. note::
前三小节的用户态程序案例代码在 `此处 <https://github.com/LearningOS/rCore-Tutorial-Book-2021Autumn/tree/ch2-U-nostd>`_ 获取。
用户态最小化执行环境
----------------------------
执行环境初始化
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
首先我们要给 Rust 编译器编译器提供入口函数 ``_start()``
``main.rs`` 中添加如下内容:
.. code-block:: rust
// os/src/main.rs
#[no_mangle]
extern "C" fn _start() {
loop{};
}
对上述代码重新编译,再用分析工具分析:
.. 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
[反汇编导出汇编程序]
$ rust-objdump -S target/riscv64gc-unknown-none-elf/debug/os
target/riscv64gc-unknown-none-elf/debug/os: file format elf64-littleriscv
Disassembly of section .text:
0000000000011120 <_start>:
; loop {}
11120: 09 a0 j 2 <_start+0x2>
11122: 01 a0 j 0 <_start+0x2>
反汇编出的两条指令就是一个死循环,
这说明编译器生成的已经是一个合理的程序了。
``qemu-riscv64 target/riscv64gc-unknown-none-elf/debug/os`` 命令可以执行这个程序。
程序正常退出
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
我们把 ``_start()`` 函数中的循环语句注释掉,重新编译并分析,看到其汇编代码是:
.. code-block:: console
$ rust-objdump -S target/riscv64gc-unknown-none-elf/debug/os
target/riscv64gc-unknown-none-elf/debug/os: file format elf64-littleriscv
Disassembly of section .text:
0000000000011120 <_start>:
; }
11120: 82 80 ret
看起来是合法的执行程序。但如果我们执行它,会引发问题:
.. code-block:: console
$ qemu-riscv64 target/riscv64gc-unknown-none-elf/debug/os
段错误 (核心已转储)
这个简单的程序导致 ``qemu-riscv64`` 崩溃了!为什么会这样?
.. note::
QEMU有两种运行模式
``User mode`` 模式,即用户态模拟,如 ``qemu-riscv64`` 程序,
能够模拟不同处理器的用户态指令的执行并可以直接解析ELF可执行文件
加载运行那些为不同处理器编译的用户级Linux应用程序。
``System mode`` 模式,即系统态模式,如 ``qemu-system-riscv64`` 程序,
能够模拟一个完整的基于不同CPU的硬件系统包括处理器、内存及其他外部设备支持运行完整的操作系统。
目前的执行环境还缺了一个退出机制,我们需要操作系统提供的 ``exit`` 系统调用来退出程序。这里先给出代码:
.. code-block:: rust
// os/src/main.rs
const SYSCALL_EXIT: usize = 93;
fn syscall(id: usize, args: [usize; 3]) -> isize {
let mut ret;
unsafe {
core::arch::asm!(
"ecall",
inlateout("x10") args[0] => ret,
in("x11") args[1],
in("x12") args[2],
in("x17") id,
);
}
ret
}
pub fn sys_exit(xstate: i32) -> isize {
syscall(SYSCALL_EXIT, [xstate as usize, 0, 0])
}
#[no_mangle]
extern "C" fn _start() {
sys_exit(9);
}
``main.rs`` 增加的内容不多,但还是有点与一般的应用程序有所不同,因为它引入了汇编和系统调用。
第二章的第二节 :doc:`/chapter2/2application` 会详细介绍上述代码的含义。
这里读者只需要知道 ``_start`` 函数调用了一个 ``sys_exit`` 函数,
向操作系统发出了退出的系统调用请求,退出码为 ``9``
我们编译执行以下修改后的程序:
.. code-block:: console
$ cargo build --target riscv64gc-unknown-none-elf
Compiling os v0.1.0 (/media/chyyuu/ca8c7ba6-51b7-41fc-8430-e29e31e5328f/thecode/rust/os_kernel_lab/os)
Finished dev [unoptimized + debuginfo] target(s) in 0.26s
[打印程序的返回值]
$ qemu-riscv64 target/riscv64gc-unknown-none-elf/debug/os; echo $?
9
可以看到,返回的结果确实是 ``9`` 。这样,我们勉强完成了一个简陋的用户态最小化执行环境。
有显示支持的用户态执行环境
----------------------------
没有 ``println`` 输出信息,终究觉得缺了点啥。
Rust 的 core 库内建了以一系列帮助实现显示字符的基本 Trait 和数据结构,函数等,我们可以对其中的关键部分进行扩展,就可以实现定制的 ``println!`` 功能。
实现输出字符串的相关函数
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. attention::
如果你觉得理解 Rust 宏有困难,把它当成黑盒就好!
首先封装一下对 ``SYSCALL_WRITE`` 系统调用。
.. code-block:: rust
const SYSCALL_WRITE: usize = 64;
pub fn sys_write(fd: usize, buffer: &[u8]) -> isize {
syscall(SYSCALL_WRITE, [fd, buffer.as_ptr() as usize, buffer.len()])
}
然后实现基于 ``Write`` Trait 的数据结构,并完成 ``Write`` Trait 所需要的 ``write_str`` 函数,并用 ``print`` 函数进行包装。
.. code-block:: rust
struct Stdout;
impl Write for Stdout {
fn write_str(&mut self, s: &str) -> fmt::Result {
sys_write(1, s.as_bytes());
Ok(())
}
}
pub fn print(args: fmt::Arguments) {
Stdout.write_fmt(args).unwrap();
}
最后,实现基于 ``print`` 函数实现Rust语言 **格式化宏** ( `formatting macros <https://doc.rust-lang.org/std/fmt/#related-macros>`_ )。
.. code-block:: rust
#[macro_export]
macro_rules! print {
($fmt: literal $(, $($arg: tt)+)?) => {
$crate::console::print(format_args!($fmt $(, $($arg)+)?));
}
}
#[macro_export]
macro_rules! println {
($fmt: literal $(, $($arg: tt)+)?) => {
print(format_args!(concat!($fmt, "\n") $(, $($arg)+)?));
}
}
接下来,我们调整一下应用程序,让它发出显示字符串和退出的请求:
.. code-block:: rust
#[no_mangle]
extern "C" fn _start() {
println!("Hello, world!");
sys_exit(9);
}
现在,我们编译并执行一下,可以看到正确的字符串输出,且程序也能正确退出!
.. code-block:: console
$ cargo build --target riscv64gc-unknown-none-elf
Compiling os v0.1.0 (/media/chyyuu/ca8c7ba6-51b7-41fc-8430-e29e31e5328f/thecode/rust/os_kernel_lab/os)
Finished dev [unoptimized + debuginfo] target(s) in 0.61s
$ qemu-riscv64 target/riscv64gc-unknown-none-elf/debug/os; echo $?
Hello, world!
9
.. 下面出错的情况是会在采用 linker.ld加入了 .cargo/config
.. 的内容后会出错:
.. .. [build]
.. .. target = "riscv64gc-unknown-none-elf"
.. .. [target.riscv64gc-unknown-none-elf]
.. .. rustflags = [
.. .. "-Clink-arg=-Tsrc/linker.ld", "-Cforce-frame-pointers=yes"
.. .. ]
.. 重新定义了栈和地址空间布局后才会出错
.. 段错误 (核心已转储)
.. 系统崩溃了借助以往的操作系统内核编程经验和与下一节调试kernel的成果经验我们直接定位为是 **栈** (Stack) 没有设置的问题。我们需要添加建立栈的代码逻辑。
.. .. code-block:: asm
.. # entry.asm
.. .section .text.entry
.. .globl _start
.. _start:
.. la sp, boot_stack_top
.. call rust_main
.. .section .bss.stack
.. .globl boot_stack
.. boot_stack:
.. .space 4096 * 16
.. .globl boot_stack_top
.. boot_stack_top:
.. 然后把汇编代码嵌入到 ``main.rs`` 中,并进行微调。
.. .. code-block:: rust
.. #![feature(global_asm)]
.. global_asm!(include_str!("entry.asm"));
.. #[no_mangle]
.. #[link_section=".text.entry"]
.. extern "C" fn rust_main() {
.. 再次编译执行,可以看到正确的字符串输出,且程序也能正确结束!

View File

@@ -0,0 +1,328 @@
.. _term-print-kernelminienv:
构建裸机执行环境
=================================
.. toctree::
:hidden:
:maxdepth: 5
有了上一节实现的用户态的最小执行环境,稍加改造,就可以完成裸机上的最小执行环境了。
本节中,我们将把 ``Hello world!`` 应用程序从用户态搬到内核态。
裸机启动过程
----------------------------
用 QEMU 软件 ``qemu-system-riscv64`` 来模拟 RISC-V 64 计算机。加载内核程序的命令如下:
.. code-block:: bash
qemu-system-riscv64 \
-machine virt \
-nographic \
-bios $(BOOTLOADER) \
-device loader,file=$(KERNEL_BIN),addr=$(KERNEL_ENTRY_PA)
- ``-bios $(BOOTLOADER)`` 意味着硬件加载了一个 BootLoader 程序,即 RustSBI
- ``-device loader,file=$(KERNEL_BIN),addr=$(KERNEL_ENTRY_PA)`` 表示硬件内存中的特定位置 ``$(KERNEL_ENTRY_PA)`` 放置了操作系统的二进制代码 ``$(KERNEL_BIN)````$(KERNEL_ENTRY_PA)`` 的值是 ``0x80200000``
当我们执行包含上述启动参数的 qemu-system-riscv64 软件,就意味给这台虚拟的 RISC-V64 计算机加电了。
此时CPU 的其它通用寄存器清零,而 PC 会指向 ``0x1000`` 的位置,这里有固化在硬件中的一小段引导代码,
它会很快跳转到 ``0x80000000`` 的 RustSBI 处。
RustSBI完成硬件初始化后会跳转到 ``$(KERNEL_BIN)`` 所在内存位置 ``0x80200000`` 处,
执行操作系统的第一条指令。
.. figure:: chap1-intro.png
:align: center
.. note::
**RustSBI 是什么?**
SBI 是 RISC-V 的一种底层规范RustSBI 是它的一种实现。
操作系统内核与 RustSBI 的关系有点像应用与操作系统内核的关系后者向前者提供一定的服务。只是SBI提供的服务很少
比如关机,显示字符串等。
实现关机功能
----------------------------
对上一节实现的代码稍作调整,通过 ``ecall`` 调用 RustSBI 实现关机功能:
.. _term-llvm-sbicall:
.. code-block:: rust
// bootloader/rustsbi-qemu.bin 直接添加的SBI规范实现的二进制代码给操作系统提供基本支持服务
// os/src/sbi.rs
fn sbi_call(which: usize, arg0: usize, arg1: usize, arg2: usize) -> usize {
let mut ret;
unsafe {
core::arch::asm!(
"ecall",
...
const SBI_SHUTDOWN: usize = 8;
pub fn shutdown() -> ! {
sbi_call(SBI_SHUTDOWN, 0, 0, 0);
panic!("It should shutdown!");
}
// os/src/main.rs
#[no_mangle]
extern "C" fn _start() {
shutdown();
}
应用程序访问操作系统提供的系统调用的指令是 ``ecall`` ,操作系统访问
RustSBI提供的SBI调用的指令也是 ``ecall``
虽然指令一样,但它们所在的特权级是不一样的。
简单地说应用程序位于最弱的用户特权级User Mode
操作系统位于内核特权级Supervisor Mode
RustSBI位于机器特权级Machine Mode
下一章会进一步阐释具体细节。
编译执行,结果如下:
.. code-block:: bash
# 编译生成ELF格式的执行文件
$ cargo build --release
Compiling os v0.1.0 (/media/chyyuu/ca8c7ba6-51b7-41fc-8430-e29e31e5328f/thecode/rust/os_kernel_lab/os)
Finished release [optimized] target(s) in 0.15s
# 把ELF执行文件转成bianary文件
$ rust-objcopy --binary-architecture=riscv64 target/riscv64gc-unknown-none-elf/release/os --strip-all -O binary target/riscv64gc-unknown-none-elf/release/os.bin
# 加载运行
$ qemu-system-riscv64 -machine virt -nographic -bios ../bootloader/rustsbi-qemu.bin -device loader,file=target/riscv64gc-unknown-none-elf/release/os.bin,addr=0x80200000
# 无法退出,风扇狂转,感觉碰到死循环
问题在哪?通过 rust-readobj 分析 ``os`` 可执行程序,发现其入口地址不是
RustSBI 约定的 ``0x80200000`` 。我们需要修改程序的内存布局并设置好栈空间。
设置正确的程序内存布局
----------------------------
可以通过 **链接脚本** (Linker Script) 调整链接器的行为,使得最终生成的可执行文件的内存布局符合我们的预期。
修改 Cargo 的配置文件来使用我们自己的链接脚本 ``os/src/linker.ld``
.. code-block::
:linenos:
:emphasize-lines: 5,6,7,8
// os/.cargo/config
[build]
target = "riscv64gc-unknown-none-elf"
[target.riscv64gc-unknown-none-elf]
rustflags = [
"-Clink-arg=-Tsrc/linker.ld", "-Cforce-frame-pointers=yes"
]
具体的链接脚本 ``os/src/linker.ld`` 如下:
.. code-block::
:linenos:
OUTPUT_ARCH(riscv)
ENTRY(_start)
BASE_ADDRESS = 0x80200000;
SECTIONS
{
. = BASE_ADDRESS;
skernel = .;
stext = .;
.text : {
*(.text.entry)
*(.text .text.*)
}
. = ALIGN(4K);
etext = .;
srodata = .;
.rodata : {
*(.rodata .rodata.*)
}
. = ALIGN(4K);
erodata = .;
sdata = .;
.data : {
*(.data .data.*)
}
. = ALIGN(4K);
edata = .;
.bss : {
*(.bss.stack)
sbss = .;
*(.bss .bss.*)
}
. = ALIGN(4K);
ebss = .;
ekernel = .;
/DISCARD/ : {
*(.eh_frame)
}
}
第 1 行我们设置了目标平台为 riscv ;第 2 行我们设置了整个程序的入口点为之前定义的全局符号 ``_start``
第 3 行定义了一个常量 ``BASE_ADDRESS````0x80200000`` RustSBI 期望的 OS 起始地址;
.. attention::
linker 脚本的语法不做要求,感兴趣的同学可以自行查阅相关资料。
``BASE_ADDRESS`` 开始,代码段 ``.text``, 只读数据段 ``.rodata``,数据段 ``.data``, bss 段 ``.bss`` 由低到高依次放置,
且每个段都有两个全局变量给出其起始和结束地址(比如 ``.text`` 段的开始和结束地址分别是 ``stext````etext`` )。
正确配置栈空间布局
----------------------------
用另一段汇编代码初始化栈空间:
.. code-block:: asm
:linenos:
# os/src/entry.asm
.section .text.entry
.globl _start
_start:
la sp, boot_stack_top
call rust_main
.section .bss.stack
.globl boot_stack
boot_stack:
.space 4096 * 16
.globl boot_stack_top
boot_stack_top:
在第 8 行,我们预留了一块大小为 4096 * 16 字节,也就是 :math:`64\text{KiB}` 的空间,
用作操作系统的栈空间。
栈顶地址被全局符号 ``boot_stack_top`` 标识,栈底则被全局符号 ``boot_stack`` 标识。
同时,这块栈空间被命名为
``.bss.stack`` ,链接脚本里有它的位置。
``_start`` 作为操作系统的入口地址,将依据链接脚本被放在 ``BASE_ADDRESS`` 处。
``la sp, boot_stack_top`` 作为 OS 的第一条指令,
将 sp 设置为栈空间的栈顶。
简单起见,我们目前不考虑 sp 越过栈底 ``boot_stack`` ,也就是栈溢出的情形。
第二条指令则是函数调用 ``rust_main`` ,这里的 ``rust_main`` 是我们稍后自己编写的应用入口。
接着,我们在 ``main.rs`` 中嵌入这些汇编代码并声明应用入口 ``rust_main``
.. code-block:: rust
:linenos:
:emphasize-lines: 7,9,10,11,12
// os/src/main.rs
#![no_std]
#![no_main]
mod lang_items;
core::arch::global_asm!(include_str!("entry.asm"));
#[no_mangle]
pub fn rust_main() -> ! {
shutdown();
}
背景高亮指出了 ``main.rs`` 中新增的代码。
第 7 行,我们使用 ``global_asm`` 宏,将同目录下的汇编文件 ``entry.asm`` 嵌入到代码中。
从第 9 行开始,
我们声明了应用的入口点 ``rust_main`` ,需要注意的是,这里通过宏将 ``rust_main``
标记为 ``#[no_mangle]`` 以避免编译器对它的名字进行混淆,不然在链接时,
``entry.asm`` 将找不到 ``main.rs`` 提供的外部符号 ``rust_main``,导致链接失败。
再次使用上节中的编译生成和运行操作我们看到QEMU模拟的RISC-V 64计算机 **优雅** 地退出了!
.. code-block:: console
# 教程使用的 RustSBI 版本比代码框架稍旧,输出有所不同
$ qemu-system-riscv64 \
> -machine virt \
> -nographic \
> -bios ../bootloader/rustsbi-qemu.bin \
> -device loader,file=target/riscv64gc-unknown-none-elf/release/os.bin,addr=0x80200000
[rustsbi] Version 0.1.0
.______ __ __ _______.___________. _______..______ __
| _ \ | | | | / | | / || _ \ | |
| |_) | | | | | | (----`---| |----`| (----`| |_) || |
| / | | | | \ \ | | \ \ | _ < | |
| |\ \----.| `--' |.----) | | | .----) | | |_) || |
| _| `._____| \______/ |_______/ |__| |_______/ |______/ |__|
[rustsbi] Platform: QEMU
[rustsbi] misa: RV64ACDFIMSU
[rustsbi] mideleg: 0x222
[rustsbi] medeleg: 0xb1ab
[rustsbi] Kernel entry: 0x80200000
清空 .bss 段
----------------------------------
等一等,与内存相关的部分太容易出错了, **清零 .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) }
});
}
pub fn rust_main() -> ! {
clear_bss();
shutdown();
}
链接脚本 ``linker.ld`` 中给出的全局符号 ``sbss````ebss`` 让我们能轻松确定 ``.bss`` 段的位置。
添加裸机打印相关函数
----------------------------------
在上一节中我们为用户态程序实现的 ``println`` 宏,略作修改即可用于本节的内核态操作系统。
详见 ``os/src/console.rs``
利用 ``println`` 宏,我们重写异常处理函数 ``panic``,使其在 panic 时能打印错误发生的位置。
相关代码位于 ``os/src/lang_items.rs`` 中。
我们还使用第三方库 ``log`` 为你实现了日志模块,相关代码位于 ``os/src/logging.rs`` 中。
.. note::
在 cargo 项目中引入外部库 log需要修改 ``Cargo.toml`` 加入相应的依赖信息。
现在,让我们重复一遍本章开头的试验,``make run LOG=TRACE``
.. figure:: color-demo.png
:align: center
产生 panic 的地点与源码中的实际位置一致!至此,我们完成了第一章的实验内容,
.. note::
背景知识:`理解应用程序和执行环境 <https://rcore-os.github.io/rCore-Tutorial-Book-v3/chapter1/4understand-prog.html>`_

View File

@@ -0,0 +1,143 @@
chapter1练习(已经废弃,没删是怕以后有用)
=====================================================
.. toctree::
:hidden:
:maxdepth: 4
- 本节难度: **低**
编程作业
-------------------------------
彩色化 LOG
+++++++++++++++++++++++++++++++
.. lab1 的工作使得我们从硬件世界跳入了软件世界,当看到自己的小 os 可以在裸机硬件上输出 ``hello world`` 是不是很高兴呢但是为了后续的一步开发更好的调试环境也是必不可少的第一章的练习要求大家实现更加炫酷的彩色log。
.. 详细的原理不多说,感兴趣的同学可以参考 `ANSI转义序列 <https://zh.wikipedia.org/wiki/ANSI%E8%BD%AC%E4%B9%89%E5%BA%8F%E5%88%97>`_ ,现在执行如下这条命令试试
.. .. code-block:: console
.. $ echo -e "\x1b[31mhello world\x1b[0m"
.. 如果你明白了我们是如何利用串口实现输出,那么要实现彩色输出就十分容易了,只需要用需要输出的字符串替换上一条命令中的 ``hello world``,用期望颜色替换 ``31(代表红色)`` 即可。
.. .. warning::
.. 以下内容仅为推荐实现,不是练习要求,有时间和兴趣的同学可以尝试。
.. 我们推荐实现如下几个等级的输出,输出优先级依次降低:
.. .. list-table:: log 等级推荐
.. :header-rows: 1
.. :align: center
.. * - 名称
.. - 颜色
.. - 用途
.. * - ERROR
.. - 红色(31)
.. - 表示发生严重错误,很可能或者已经导致程序崩溃
.. * - WARN
.. - 黄色(93)
.. - 表示发生不常见情况,但是并不一定导致系统错误
.. * - INFO
.. - 蓝色(34)
.. - 比较中庸的选项,输出比较重要的信息,比较常用
.. * - DEBUG
.. - 绿色(32)
.. - 输出信息较多,在 debug 时使用
.. * - TRACE
.. - 灰色(90)
.. - 最详细的输出,跟踪了每一步关键路径的执行
.. 我们可以输出比设定输出等级以及更高输出等级的信息,如设置 ``LOG = INFO``,则输出 ``ERROR``、``WARN``、``INFO`` 等级的信息。简单 demo 如下,输出等级为 INFO:
.. .. image:: color-demo.png
.. 为了方便使用彩色输出,我们要求同学们实现彩色输出的宏或者函数,用以代替 print 完成输出内核信息的功能,它们有着和 prinf 十分相似的使用格式,要求支持可变参数解析,形如:
.. .. code-block:: rust
.. // 这段代码输出了 os 内存空间布局,这到这些信息对于编写 os 十分重要
.. info!(".text [{:#x}, {:#x})", s_text as usize, e_text as usize);
.. debug!(".rodata [{:#x}, {:#x})", s_rodata as usize, e_rodata as usize);
.. error!(".data [{:#x}, {:#x})", s_data as usize, e_data as usize);
.. .. code-block:: c
.. info("load range : [%d, %d] start = %d\n", s, e, start);
.. 在以后,我们还可以在 log 信息中增加线程、CPU等信息只是一个推荐不做要求这些信息将极大的方便你的代码调试。
实验要求
+++++++++++++++++++++++++++++++
.. - 实现分支ch1。
.. - 完成实验指导书中的内容,在裸机上实现 ``hello world`` 输出。
.. - 实现彩色输出宏(只要求可以彩色输出,不要求 log 等级控制,不要求多种颜色)。
.. - 隐形要求:可以关闭内核所有输出。从 lab2 开始要求关闭内核所有输出(如果实现了 log 等级控制,那么这一点自然就实现了)。
.. - 利用彩色输出宏输出 os 内存空间布局,即:输出 ``.text``、``.data``、``.rodata``、``.bss`` 各段位置,输出等级为 ``INFO``。
实验检查
+++++++++++++++++++++++++++++++
.. - 实验目录要求(Rust)
.. .. code-block::
.. ├── os(内核实现)
.. │   ├── Cargo.toml(配置文件)
.. │   ├── Makefile (要求 make run LOG=xxx 可以正确执行,可以不实现对 LOG 这一属性的支持,设置默认输出等级为 INFO)
.. │   └── src(所有内核的源代码放在 os/src 目录下)
.. │   ├── main.rs(内核主函数)
.. │   └── ...
.. ├── reports
.. │   ├── lab1.md/pdf
.. │   └── ...
.. ├── README.md其他必要的说明
.. ├── ...
.. 报告命名 labx.md/pdf统一放在 reports 目录下。每个实验新增一个报告,为了方便修改,检查报告是以最新分支的所有报告为准。
.. - 检查
.. .. code-block:: console
.. $ cd os
.. $ git checkout ch1
.. $ make run LOG=INFO
.. 可以正确执行(可以不支持LOG参数只有要彩色输出就好),可以看到正确的内存布局输出,根据实现不同数值可能有差异,但应该位于 ``linker.ld`` 中指示 ``BASE_ADDRESS`` 后一段内存,输出之后关机。
问答作业
-------------------------------
.. 1. 为了方便 os 处理,M态软件会将 S 态异常/中断委托给 S 态软件请指出有哪些寄存器记录了委托信息rustsbi 委托了哪些异常/中断?(也可以直接给出寄存器的值)
.. 2. 请学习 gdb 调试工具的使用(这对后续调试很重要),并通过 gdb 简单跟踪从机器加电到跳转到 0x80200000 的简单过程。只需要描述重要的跳转即可,只需要描述在 qemu 上的情况。
.. 3. tips:
.. - 事实上进入 rustsbi 之后就不需要使用 gdb 调试了。可以直接阅读代码。`rustsbi起始代码 <https://github.com/rustsbi/rustsbi-qemu/blob/main/rustsbi-qemu/src/main.rs#L146>`_ 。
.. - 可以使用示例代码 Makefile 中的 ``make debug`` 指令。
.. - 一些可能用到的 gdb 指令:
.. - ``x/10i 0x80000000`` : 显示 0x80000000 处的10条汇编指令。
.. - ``x/10i $pc`` : 显示即将执行的10条汇编指令。
.. - ``x/10xw 0x80000000`` : 显示 0x80000000 处的10条数据格式为16进制32bit。
.. - ``info register``: 显示当前所有寄存器信息。
.. - ``info r t0``: 显示 t0 寄存器的值。
.. - ``break funcname``: 在目标函数第一条指令处设置断点。
.. - ``break *0x80200000``: 在 0x80200000 出设置断点。
.. - ``continue``: 执行直到碰到断点。
.. - ``si``: 单步执行一条汇编指令。
报告要求
-------------------------------
- 简单总结你实现的功能200字以内不要贴代码
- 完成问答题。
- (optional) 你对本次实验设计及难度/工作量的看法,以及有哪些需要改进的地方,欢迎畅所欲言。

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View File

@@ -0,0 +1,13 @@
.. _link-chapter1:
第一章:应用程序与基本执行环境
==============================================
.. toctree::
:maxdepth: 4
0intro
1app-ee-platform
2remove-std
3mini-rt-usrland
4mini-rt-baremetal