第三节 内核第一行代码

现在我们开始动手用 Rust 编写内核的第一个版本 v0.1。虽然它只包含一行代码,但是当我们观测到这行代码被执行时,就意味着计算机经过漫长的启动,终于把系统控制权交到我们手里,在此之后就是 ArceOS 内核的 ShowTime!

首先建立工作目录 arceos,以后我们将把它作为根目录,逐步建立组件工程,同时维护一些对编译、运行进行支持的配置和脚本。

在工作目录 arceos 中,建立第一个工程 axorigin,默认是一个基于 Rust STD 标准库的 "Hello, World!" 可执行应用程序。

mkdir arceos cd arceos cargo new axorigin

按上述步骤建立的应用程序,原本预期会在 Linux 上编译和运行。程序入口 main 函数并非真正的程序入口,真正的入口其实是命名为 _start 的函数,Rust 编译器会结合工具链及 Linux 内核的要求生成 _start 函数的框架,在完成一些必要的运行前准备工作后,再调用 main 函数。此外,打印输出的功能也需要通过调用 Rust STD 标准库去申请 Linux 的系统调用服务。

这种默认生成的程序显然不符合我们的需要,我们要开发的程序是操作系统而不是应用。操作系统本身就是一种基于硬件的裸机程序(baremental)。对于裸机程序,配套的 STD 库是不存在的,也无法指望 Rust 编译器为我们生成 _start 函数,我们需要自己定义 _start 函数,并通过直接管理硬件资源来满足自身及上层应用的需求。

清空 main.rs,建立内核程序入口框架。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

通过 no_std 和 no_main 告诉 Rust 编译器,程序自定义 _start 入口,不需要 main 函数和标准库支持。

同时,对 _start 标记两个属性:

  • #[link_section = ".text.boot"]:如上节所说,要求编译器把 _start 函数代码放置在 Image 文件的开头,这样当内核被加载后,入口就是第一行指令。

  • #[no_mangle]:要求编译器保持 _start 函数名称不变。这个入口符号名称在链接过程中是必须存在和可见的,所以必须阻止编译器对 _start 这个符号名称进行混淆。

_start 中当前只有唯一的一行代码 wfi,后面在 qemu 启动时,我们将监控这行代码是否被执行到。

现在尝试在工作目录 arceos 下编译 axorigin 这个工程,需要输入额外的参数:

cargo build --manifest-path axorigin/Cargo.toml \ --target riscv64gc-unknown-none-elf --target-dir ./target --release

注意:必须显式指定 riscv64gc-unknown-none-elf 这个 target,告诉 Rust 编译器我们的编译目标是 RiscV64 架构的裸机程序。

此时可能会收到下面的错误信息:

error[E0463]: can't find crate for `core` | = note: the `riscv64gc-unknown-none-elf` target may not be installed = help: consider downloading the target with `rustup target add riscv64gc-unknown-none-elf` ... ...

我们可以参照提示中的命令去下载和安装 target。

但这里提供另一个解决的办法,在工作目录下直接放配置文件 rust-toolchain.toml,来定制符合我们要求的 Rust 工具链,

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

riscv64gc-unknown-none-elf 加入工具链的默认 target 列表;同时 channel = "nightly",因为后面会用到Rust 的一些实验特性。

再次编译,还是报错,这次的提示:

error: `#[panic_handler]` function required, but not found

根据 Rust 的设计,程序遇到不可恢复异常时将会中止运行,展开调用栈,把异常信息输出到屏幕。这个机制与底层操作系统有关,定义在 STD 标准库中。如前所述,我们没有现成的实现,所以只能自己给出定义。

增加模块文件 lang_items.rs,然后在 main.rs 中加入一行 mod lang_items; 引入该模块。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

现在采取最简单的方式:如果遇到不可恢复的异常,程序将会无限循环下去。后面章节实验中,我们将逐步完善这个处理机制。

再次编译,终于通过了!但是当前输出的是 ELF 格式,还不能在 qemu 中直接运行,先转为原始二进制格式:

rust-objcopy --binary-architecture=riscv64 --strip-all -O binary \ target/riscv64gc-unknown-none-elf/release/axorigin \ target/riscv64gc-unknown-none-elf/release/axorigin.bin

得到的 axorigin.bin 就是我们最初版本的内核 image 文件,先用 xxd 命令查看一下内容:

xxd -e target/riscv64gc-unknown-none-elf/release/axorigin.bin 00000000: 10500073 0000 s.P...

这个内核 image 只有 6 个字节,对照 RiscV 指令手册,最前面的四个字节正是 wfi 的指令编码。这个结果有点意外,我们在前面做的一系列考虑和具体工作,在 Rust 看来,只有那一行代码是有意义的;就连 Rust 自己要求实现的 panic_handler 最终也被它忽略了:-)。不过这个内核文件对我们下面的实验已经足够了。

终于到了运行时刻!用 qemu 运行一下:

qemu-system-riscv64 -m 128M -machine virt -bios default -nographic \ -kernel target/riscv64gc-unknown-none-elf/release/axorigin.bin \ -D qemu.log -d in_asm

成功!屏幕输出了 OpenSBI 的启动信息。但是对我们的内核来说,目前还什么事都没有干,自然不会有输出。

现在 qemu 处于等待状态,通过输入 ctrl+a x 退回到 shell。具体操作:先按 ctrl + a,再按 x 键。

查看本地即当前目录,发现多出一个 qemu.log。

执行 qemu 时的最后一行参数让 qemu 模拟器记录了它执行过的代码序列,我们只需要关注日志文件的最后:

---------------- IN: Priv: 1; Virt: 0 0x80200000: 10500073 wfi 0x80200004: 0000 illegal

qemu 在执行过程的最后阶段,到达了预设的内核入口地址 0x8020_0000,并且执行的正是我们安排的 wfi 指令。这证明:我们之前的努力获得了成功,从此时起,计算机(模拟器)的控制权就落入我们内核的手中,后面将通过一步步的工作逐渐增强完善内核的功能。