2. 最小化内核
本章代码对应 commit :f01d6989751ea25a46c58864089cc495946355fe
概要
本章我们将把上一章创建的 独立可执行程序 编译为内核,并和 bootloader 链接成为可以被 qemu 加载的 bootimage 。为此我们将介绍:
使用 目标三元组 描述目标操作系统。
使用 cargo xbuild 和 目标三元组 编译内核。
将 内核 和 bootloader 链接成 bootimage 。
修改 _start ,使其能够对堆栈进行一些简单的初始化。
建立编译目标三元组和linker.ld
cargo 在编译内核时,可以用过 --target <target triple> 支持不同的系统。 target triple 包含:cpu 架构、供应商、操作系统和 ABI 。
由于我们在编写自己的操作系统,所以所有目前的 目标三元组 都不适用。幸运的是,rust 允许我们用 JSON 文件定义自己的 目标三元组 。首先我们来看一下 x86_64-unknown-linux-gnu 的 JSON 文件:
{
"llvm-target": "x86_64-unknown-linux-gnu",
"data-layout": "e-m:e-i64:64-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": "64",
"target-c-int-width": "32",
"os": "linux",
"executables": true,
"linker-flavor": "gcc",
"pre-link-args": ["-m64"],
"morestack": false
}因为我们的主要目的是编写 os ,所以这里直接给出目标文件的实现:
对文件各参数细节感兴趣的读者可以自行研究,这里只对 pre-link-args 进行解释:
这里我们需要使用指定的链接器,这里同样直接给出 linker.ld 的实现,请自行创建好 src/boot/linker.ld 文件:
运行 cargo build --target riscv32-os.json ,发现编译失败了:
错误的原因是:no_std 的程序会隐式地链接到 core 库 。 core 库 包含基础的 Rust 类型,如 Result、Option 和迭代器等。core 库 只支持原生的 目标三元组 ,而我们在编写 os 时使用的是自定义的 目标三元组 。
如果我们想为其他系统编译代码,我们需要为这些系统重新编译整个 core 库 。这就是为什么我们需要 cargo xbuild 。
使用Cargo xbuild重新编译core库
这个工具封装了 cargo build。同时,它将自动交叉编译 core 库 和一些 编译器内建库(compiler built-in libraries) 。我们可以用下面的命令安装它:
现在运行命令来编译目标程序:
也可把上面的命令放到build.sh脚本中,这样直接执行这个脚本就可以完成编译了。但我们发现产生了编译错误:
使用nightly rust toolchains
这个错误是由于没有使用nightly rust toolchains。为此需要在项目目录下建立一个rust-toolchain文件,文件内容是toolchians的版本信息:
这样,在后续编译rust程序时,将采用上述版本的rust工具链。重新执行build.sh,发现我们的内核已经可以正确编译了。接下来的任务就是将他和 bootloader 链接,得到可以被 qemu 加载的 os 。
检查一下编译出来的内核镜像
创建引导映象(Bootimage)
编写一个 bootloader 并将其与内核链接成 引导映像 并不是一个简单的事情,所以我们直接使用已有的 bootloader :
下载 并将其中名为 related items in lab2 的文件夹中的两个子文件夹拷贝至 Cargo.toml 的同级目录下。
感兴趣的读者可以自行阅读 zw 同学的 bbl 文档
有了 bootloader ,那么只需要将其与我们的内核链接就可以了。这里我们需要使用到 riscv-pk 中的 configure 。为了以后能够方便的进行编译链接,我们需要编写一个 Makefile 文件(与 Cargo.toml 位于同级目录):
如果编译错误,基本上是因为没有安装 riscv 的交叉编译器。 执行
riscv64-unknown-elf-gcc -v,找不到程序的话就确定是这个错误了。推荐的解决方式是直接使用已经编译好的交叉编译器。 可参考 how to use "Prebuilt RISC‑V GCC Toolchain"
执行 make kernel 生成的 kernel.bin 就是我们需要的 可以被 qemu 加载的 os 。执行 make run :
至此,我们的 最小内核 已经“成功”跑起来了!!!吗???
退出 qemu 的方法是,按下 Ctrl+A 之后再按 X 即可。当然你也可以直接杀掉 qemu 进程
killall qemu-system-riscv32。
Hello World! -- step 1:显示字符
我们将用最简单的方法来验证 os 是否已经正确的被加载了:打印 Hello World! :
bbl::sbi 是 依赖项目 中已经完成的库,可以使用 sbi::console_putchar(usize) 打印一个 ASCII 字符 。使用前需要在 Cargo.toml 中添加对其的依赖:
编译运行!很遗憾的发现,这位“新生儿”还没有学会说话(屏幕并没有显示 Hello World!)。还记得上一章的 _start 吗?
“你已经是一个成熟的 _start 了,需要学会自己设置堆栈。”
“我不是,我没有,别瞎说!”
Hello World! -- step 2:设置堆栈
一个 成熟的 _start 需要能够设置一些简单的堆栈信息,然后跳转至 main 函数。所以我们需要使用 汇编语言 重写 _start 。在 src/boot 中创建 entry.asm :
然后在 main.rs 中通过 global_asm 引入 _start ,并实现 rust_main 。现在 main.rs 应该长成这样:
#![feature(global_asm)] 使得我们能够使用 global_asm!(include_str!("boot/entry.asm")); 引入外部汇编代码。 entry.asm 中的 call rust_main 告诉我们,需要在 rust_main 中进行打印 Hello World! 的工作。所以修改函数名为 rust_main 。最下方的 abort() 并无意义,只是为了避免一个 error ,参见 rust lld: error: undefined symbol: abort 。
那么,接下来,就是见证奇迹的时刻:
以后若无特殊说明,编译运行的命令就是
make run
预告
最黑暗的日子已经过去,我们已经完成了一个可以正常运行的 最小内核 !下一章我们将在此基础上,实现 rust 中最经典的宏: println! ,以便于后续的调试输出。
Last updated
Was this helpful?