最小化内核

本章代码对应分支:min-kernel

概要

本章我们将把上一章创建的 独立可执行程序 编译为内核,并和 bootloader 链接成为可以被 qemu 加载的 bootimage 。为此我们将介绍:

  1. 使用 目标三元组 描述目标操作系统。
  2. 使用 cargo xbuild目标三元组 编译内核。
  3. 内核bootloader 链接成 bootimage
  4. 修改 _start ,使其能够对堆栈进行一些简单的初始化。

建立编译目标三元组和 linker.ld

cargo 在编译内核时,可以用过 --target <target triple> 支持不同的系统。 target triple 包含:cpu 架构、供应商、操作系统和 ABI

由于我们在编写自己的操作系统,所以所有目前的 目标三元组 都不适用。幸运的是,rust 允许我们用 JSON 文件定义自己的 目标三元组

首先我们来看一下 x86_64-unknown-linux-gnuJSON 文件:

{
  "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-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 = 0x80400000;

SECTIONS
{
    /* 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-os.json ,发现编译失败了:

error[E0463]: can't find crate for `core`

错误的原因是:no_std 的程序会隐式地链接到 rust 的 core 库core 库 包含基础的 Rust 类型,如 Result、Option 和迭代器等。Rust 工具链中默认只为原生的目标三元组提供了预编译好的 core 库,而我们在编写 os 时使用的是自定义的目标三元组 。

因此我们需要为这些目标重新编译整个 core 库 。这时我们就需要 cargo xbuild

使用 Cargo xbuild 重新编译 core 库

这个工具封装了 cargo build 。同时,它将自动交叉编译 core 库 和一些 编译器内建库(compiler built-in libraries) 。我们可以用下面的命令安装它:

cargo install cargo-xbuild

现在运行命令来编译目标程序:

cargo xbuild --target riscv32-os.json

但我们发现产生了编译错误:

error: The sysroot can't be built for the Stable channel. Switch to nightly.
note: run with `RUST_BACKTRACE=1` for a backtrace

使用 nightly rust toolchains

这个错误是由于没有使用 nightly 版本的工具链。相比较于 stable rust(default) ,nightly rust 可以使用一些 unsafe 的功能,这在编写 os 时是不可避免的。为此需要在项目目录下建立一个 rust-toolchain 文件,文件内容是工具链的版本信息:

nightly-2019-09-02

也可以直接切换 toolchain

这样,在后续编译 rust 程序时,将采用上述版本的 rust 工具链。重新执行 cargo xbuild --target riscv32-os.json ,发现我们的内核已经可以正确编译了。接下来的任务就是将他和 bootloader 链接,得到可以被 qemu 加载的 os 。

检查一下编译出来的内核镜像

$ cargo xbuild --target riscv32-os.json
# 编译成功后, 执行如下命令可以查看内核信息
$ file target/riscv32-os/debug/os
target/riscv32-os/debug/os: ELF 32-bit LSB executable, UCB RISC-V, version 1 (SYSV), statically linked, with debug_info, not stripped

使用 bootloader 创建引导镜像

编写一个 bootloader 并不是一个简单的事情,所以我们直接使用已有的 bootloader:OpenSBI

我们从它的 GitHub 上下载最新的 预编译版本(2019 年 7 月的最新版是 v0.4),解压后将其中的 platform/qemu/virt/firmware/fw_jump.elf 复制为 os/opensbi/virt.elf

有了 bootloader ,接下来将其与我们的内核链接成 引导映像 就可以了。为了以后能够方便的进行编译链接,我们编写一个 Makefile 文件(与 Cargo.toml 位于同级目录):

target := riscv32-os
mode := debug
kernel := target/$(target)/$(mode)/os
bin := target/$(target)/$(mode)/kernel.bin
usr_path := usr

export SFSIMG = $(usr_path)/rcore32.img

.PHONY: all clean run build asm qemu kernel

all: build

build: $(bin)

run: build qemu

kernel:
    @cargo xbuild --target riscv32-os.json

$(bin): kernel
    @riscv64-unknown-elf-objcopy $(kernel) --strip-all -O binary $@

asm:
    @riscv64-unknown-elf-objdump -d $(kernel) | less

qemu:
    @qemu-system-riscv32 -nographic -machine virt \
        -kernel opensbi/virt.elf \
        -device loader,file=$(bin),addr=0x80400000

这里我们还需要安装 RISC-V GCC 工具链,可以使用 SiFive 公司提供的 预编译版本。使用方式可参考 how to use "Prebuilt RISC‑V GCC Toolchain"

执行 make kernel 生成的 kernel.bin 就是我们需要的 可以被 bootloader 引导的 os 二进制文件

执行 make run ,可以看到 bootloader 输出的信息:

OpenSBI v0.4 (Jul  2 2019 11:54:59)
   ____                    _____ ____ _____
  / __ \                  / ____|  _ \_   _|
 | |  | |_ __   ___ _ __ | (___ | |_) || |
 | |  | | '_ \ / _ \ '_ \ \___ \|  _ < | |
 | |__| | |_) |  __/ | | |____) | |_) || |_
  \____/| .__/ \___|_| |_|_____/|____/_____|
        | |
        |_|

Platform Name          : QEMU Virt Machine
Platform HART Features : RV32ACDFIMSU
Platform Max HARTs     : 8
Current Hart           : 0
Firmware Base          : 0x80000000
Firmware Size          : 108 KB
Runtime SBI Version    : 0.1

PMP0: 0x80000000-0x8001ffff (A)
PMP1: 0x00000000-0xffffffff (A,R,W,X)

至此,我们的 最小内核 已经“成功”跑起来了!!!吗???

退出 qemu 的方法是,按下 Ctrl+A 之后再按 X 即可。当然你也可以直接杀掉 qemu 进程 killall qemu-system-riscv32

适配 SBI

SBI 是 Supervisor Binary Interface 的简称,OpenSBI 除了作为 bootloader 还实现了 RISCV SBI(即部分 syscall),比如打印、读取字符等(本质上已经悄悄的实现了一个中断处理机制)。

暂时看不懂就先抄代码,后面会专门讲解 syscall ,可以理解为我们需要 OpenSBI 帮我们进行输出字符的工作。更多内容请参考 官方文档

为了便于使用,我们将 syscall 包一层,将其写为函数形式:

#![allow(dead_code)]

#[inline(always)]
fn sbi_call(which: usize, arg0: usize, arg1: usize, arg2: usize) -> usize {
    let ret;
    unsafe {
        asm!("ecall"
            : "={x10}" (ret)
            : "{x10}" (arg0), "{x11}" (arg1), "{x12}" (arg2), "{x17}" (which)
            : "memory"
            : "volatile");
    }
    ret
}

pub fn console_putchar(ch: usize) {
    sbi_call(SBI_CONSOLE_PUTCHAR, ch, 0, 0);
}

pub fn console_getchar() -> usize {
    sbi_call(SBI_CONSOLE_GETCHAR, 0, 0, 0)
}

pub fn shutdown() {
    sbi_call(SBI_SHUTDOWN, 0, 0, 0);
}

pub fn set_timer(stime_value: u64) {
    #[cfg(target_pointer_width = "32")]
    sbi_call(
        SBI_SET_TIMER,
        stime_value as usize,
        (stime_value >> 32) as usize,
        0,
    );
    #[cfg(target_pointer_width = "64")]
    sbi_call(SBI_SET_TIMER, stime_value as usize, 0, 0);
}

pub fn clear_ipi() {
    sbi_call(SBI_CLEAR_IPI, 0, 0, 0);
}

pub fn send_ipi(hart_mask: usize) {
    sbi_call(SBI_SEND_IPI, &hart_mask as *const _ as usize, 0, 0);
}

pub fn remote_fence_i(hart_mask: usize) {
    sbi_call(SBI_REMOTE_FENCE_I, &hart_mask as *const _ as usize, 0, 0);
}

pub fn remote_sfence_vma(hart_mask: usize, _start: usize, _size: usize) {
    sbi_call(SBI_REMOTE_SFENCE_VMA, &hart_mask as *const _ as usize, 0, 0);
}

pub fn remote_sfence_vma_asid(hart_mask: usize, _start: usize, _size: usize, _asid: usize) {
    sbi_call(
        SBI_REMOTE_SFENCE_VMA_ASID,
        &hart_mask as *const _ as usize,
        0,
        0,
    );
}

const SBI_SET_TIMER: usize = 0;
const SBI_CONSOLE_PUTCHAR: usize = 1;
const SBI_CONSOLE_GETCHAR: usize = 2;
const SBI_CLEAR_IPI: usize = 3;
const SBI_SEND_IPI: usize = 4;
const SBI_REMOTE_FENCE_I: usize = 5;
const SBI_REMOTE_SFENCE_VMA: usize = 6;
const SBI_REMOTE_SFENCE_VMA_ASID: usize = 7;
const SBI_SHUTDOWN: usize = 8;

打印字符

直接在 _start 里进行过多的操作并不是一件优雅的事情,而把所有代码都堆在 main 文件里更不好,所以趁着我们的文件还不多,先把他们整理一下吧:

  • main.rs
#![no_std] // don't link the Rust standard library
#![no_main] // disable all Rust-level entry points

#[allow(unused_imports)]
use os;
  • boot/extry.asm

我们通过汇编代码来实现 _start 。由于我们使用了 OpenSBI ,所以此时 sp 被设置为指向 M 态内存空间。这里 _start 只做了两件事:

  1. 开辟一段新的内存空间作为 rust_main 的运行的堆栈,并使 sp 指向它
  2. 跳转到 rust_main
    .section .text.entry
    .globl _start
_start:
    lui sp, %hi(bootstacktop)   # 将栈指针 sp 置为栈顶地址

    call rust_main

    .section .bss.stack
    .align 12  # PGSHIFT
    .global bootstack
bootstack:
    .space 4096 * 4                # 开辟一块栈空间(4个页)
    .global bootstacktop
bootstacktop:
  • init.rs
use crate::sbi;

global_asm!(include_str!("boot/entry.asm")); // 引入 _start

static HELLO: &[u8] = b"Hello World!";

#[no_mangle]
pub fn rust_main() -> ! {
    for &c in HELLO {
        sbi::console_putchar(c as usize);
    }
    loop {}
}
  • lang_items.rs
use core::panic::PanicInfo;

#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop {}
}

#[no_mangle]
pub extern "C" fn abort() {
    panic!("abort!");
}
  • lib.rs
#![no_std]
#![feature(asm)]
#![feature(global_asm)]

mod init;
mod lang_items;
mod sbi;

那么,接下来,就是见证奇迹的时刻:

> make run
...
Hello World!

以后若无特殊说明,编译运行的命令就是 make run

预告

最黑暗的日子已经过去,我们已经完成了一个可以正常运行的 最小内核 !下一章我们将在此基础上,实现 rust 中最常用的宏: println! ,用于后续的调试和输出。

results matching ""

    No results matching ""

    results matching ""

      No results matching ""