本系列博客系转载,出处: 知乎专栏:从零开始写 OS
所有代码都在:https://github.com/LearningOS/rcore_step_by_step
本章代码对应 commit :40ee051072e5a4b89ca188d5620e9b30f1b68b25
本章我们将把上一章创建的 独立可执行程序 编译为内核,并和 bootloader 链接成为可以被 qemu 加载的 bootimage
。为此我们将介绍:
cargo xbuild
和 目标三元组 编译内核。bootloader
链接成 bootimage
。_start
,使其能够对堆栈进行一些简单的初始化。cargo
在编译内核时,可以用过 --target
支持不同的系统。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 ,所以这里直接给出目标文件的实现:
// in riscv32-xy_os.json
{
"llvm-target": "riscv32",
"data-layout": "e-m:e-p:32:32-i64:64-n32-S128",
"target-endian": "little",
"target-pointer-width": "32",
"target-c-int-width": "32",
"os": "none",
"arch": "riscv32",
"cpu": "generic-rv32",
"features": "+m,+a,+c",
"max-atomic-width": "32",
"linker": "rust-lld",
"linker-flavor": "ld.lld",
"pre-link-args": {
"ld.lld": ["-Tsrc/boot/linker.ld"]
},
"executables": true,
"panic-strategy": "abort",
"relocation-model": "static",
"eliminate-frame-pointer": false
}
对文件各参数细节感兴趣的读者可以自行研究,这里只对 pre-link-args
进行解释:
"pre-link-args": {
"ld.lld": [
"-Tsrc/boot/linker.ld"
]
},
这里我们需要使用指定的链接器,这里同样直接给出 linker.ld
的实现,请自行创建好 src/boot/linker.ld
文件:
/* Copy from bbl-ucore : https://ring00.github.io/bbl-ucore */
/* Simple linker script for the ucore kernel.
See the GNU ld 'info' manual ("info ld") to learn the syntax. */
OUTPUT_ARCH(riscv)
ENTRY(_start)
BASE_ADDRESS = 0xC0020000;
SECTIONS
{
. = 0xC0000000;
.boot : {
KEEP(*(.text.boot))
}
/* Load the kernel at this address: "." means the current address */
. = BASE_ADDRESS;
start = .;
.text : {
stext = .;
*(.text.entry)
*(.text .text.*)
. = ALIGN(4K);
etext = .;
}
.rodata : {
srodata = .;
*(.rodata .rodata.*)
. = ALIGN(4K);
erodata = .;
}
.data : {
sdata = .;
*(.data .data.*)
edata = .;
}
.stack : {
*(.bss.stack)
}
.bss : {
sbss = .;
*(.bss .bss.*)
ebss = .;
}
PROVIDE(end = .);
}
运行 cargo build --target riscv32-xy_os.json
,发现编译失败了:
error[E0463]: can't find crate for `core`
错误的原因是:no_std
的上下文隐式地链接到 core
库 。core
库 包含基础的 Rust
类型,如 Result、Option
和迭代器等。core
库 只支持原生的 目标三元组 ,而我们在编写 OS
时使用的是自定义的 目标三元组 。
如果我们想为其他系统编译代码,我们需要为这些系统重新编译整个 core
库 。这就是为什么我们需要 cargo xbuild
。
这个工具封装了 cargo build
。同时,它将自动交叉编译 core
库 和一些 编译器内建库(compiler built-in libraries)
。我们可以用下面的命令安装它:
cargo install cargo-xbuild
现在运行 cargo xbuild --target riscv32-xy_os.json
,我们的内核已经可以正确编译了。接下来的任务就是将他和 bootloader
链接,得到可以被 qemu
加载的 os
。
编写一个 bootloader
并将其与内核链接成 引导映像 并不是一个简单的事情,所以我们直接使用已有的bootloader
:
git clone https://github.com/LearningOS/rcore_step_by_step.git
之后将其中名为 related items in lab2
的文件夹中的两个子文件夹拷贝至 Cargo.toml
的同级目录下。
感兴趣的读者可以自行阅读 bbl 官方文档。
有了 bootloader
,那么只需要将其与我们的内核链接就可以了。这里我们需要使用到 riscv-pk
中的 configure
。为了以后能够方便的进行编译链接,我们需要编写一个 Makefile
文件(与 Cargo.toml
位于同级目录):
target := riscv32-xy_os
bbl_path := $(abspath riscv-pk)
mode := debug
kernel := target/$(target)/$(mode)/xy_os
bin := target/$(target)/$(mode)/kernel.bin
.PHONY: all clean run build asm qemu kernel
all: kernel
$(bin): kernel
mkdir -p target/$(target)/bbl && \
cd target/$(target)/bbl && \
$(bbl_path)/configure \
--with-arch=rv32imac \
--disable-fp-emulation \
--host=riscv64-unknown-elf \
--with-payload=$(abspath $(kernel)) && \
make -j32 && \
cp bbl $(abspath $@)
build: $(bin)
run: build qemu
kernel:
@cargo xbuild --target riscv32-xy_os.json
asm:
@riscv64-unknown-elf-objdump -d $(kernel) | less
qemu:
qemu-system-riscv32 -kernel $(bin) -nographic -machine virt
docker:
sudo docker run -it --mount type=bind,source=$(shell pwd)/..,destination=/mnt panqinglin/rust_riscv bash
未安装 Prebuilt RISC‑V GCC Toolchain 会导致编译错误,请参考 how to use “Prebuilt RISC‑V GCC Toolchain” 下载使用。
执行 make kernel
生成的 kernel.bin
就是我们需要的 可以被 qemu
加载的 os
。执行 make run
:
> make run
...
qemu-system-riscv32 -kernel target/riscv32-xy_os/debug/kernel.bin -nographic -machine virt
bbl loader
至此,我们的 最小内核 已经“成功”跑起来了!!!吗???
我们将用最简单的方法来验证 os
是否已经正确的被加载了:打印 Hello World!
:
#![no_std] // don't link the Rust standard library
#![no_main] // disable all Rust-level entry points
use core::panic::PanicInfo;
use bbl::sbi;
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
static HELLO: &[u8] = b"Hello World!";
#[no_mangle]
pub extern "C" fn main() -> ! {
for &c in HELLO {
sbi::console_putchar(c as usize);
}
loop {}
}
bbl::sbi
是 依赖项目 中已经完成的库,可以使用 sbi::console_putchar(usize)
打印一个 ASCII
字符 。使用前需要在 Cargo.toml
中添加对其的依赖。
[dependencies]
bbl = { path = "crate/bbl" }
编译运行!很遗憾的发现,这位“新生儿”还没有学会说话(屏幕并没有显示 Hello World!)。还记得上一章的 _start 吗?
一个 成熟的_start
需要能够设置一些简单的堆栈信息,然后跳转至 main
函数。所以我们需要使用 汇编语言 重写 _start
。在 src/boot
中创建 entry.asm
:
.section .text.entry
.globl _start
_start:
add t0, a0, 1
slli t0, t0, 16
lui sp, %hi(bootstack)
addi sp, sp, %lo(bootstack)
add sp, sp, t0
call rust_main
.section .bss.stack
.align 12 #PGSHIFT
.global bootstack
bootstack:
.space 4096 * 16 * 8
.global bootstacktop
bootstacktop:
然后在 main.rs
中通过 global_asm
引入_start
,并实现 rust_main
。现在 main.rs
应该长成这样:
#![no_std] // don't link the Rust standard library
#![no_main] // disable all Rust-level entry points
#![feature(global_asm)]
use core::panic::PanicInfo;
use bbl::sbi;
global_asm!(include_str!("boot/entry.asm"));
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
static HELLO: &[u8] = b"Hello World!";
#[no_mangle]
pub extern "C" fn rust_main() -> ! {
for &c in HELLO {
sbi::console_putchar(c as usize);
}
loop {}
}
#[no_mangle]
pub extern fn abort() {
panic!("abort!");
}
#![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
...
qemu-system-riscv32 -kernel target/riscv32-xy_os/debug/kernel.bin -nographic -machine virt
bbl loader
Hello World!
以后若无特殊说明,编译运行的命令就是make run
最黑暗的日子已经过去,我们已经完成了一个可以正常运行的 最小内核 !下一章我们将在此基础上,实现 Rust 中最经典的宏: println!
,以便于后续的调试输出。