> For the complete documentation index, see [llms.txt](https://xy-plus.gitbook.io/rcore-step-by-step/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://xy-plus.gitbook.io/rcore-step-by-step/zui-xiao-hua-nei-he.md).

# 2. 最小化内核

> 本章代码对应 commit ：f01d6989751ea25a46c58864089cc495946355fe

## 概要

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

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

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

cargo 在编译内核时，可以用过 `--target <target triple>` 支持不同的系统。 **target triple** 包含：cpu 架构、供应商、操作系统和 [ABI](https://stackoverflow.com/questions/2171177/what-is-an-application-binary-interface-abi/2456882#2456882) 。

由于我们在编写自己的操作系统，所以所有目前的 **目标三元组** 都不适用。幸运的是，rust 允许我们用 **JSON** 文件定义自己的 **目标三元组** 。首先我们来看一下 **x86\_64-unknown-linux-gnu** 的 **JSON** 文件：

```javascript
{
  "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 ，所以这里直接给出目标文件的实现：

```javascript
// 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 进行解释：

```javascript
"pre-link-args": {
    "ld.lld": [
      "-Tsrc/boot/linker.ld"
    ]
  },
```

这里我们需要使用指定的链接器，这里同样直接给出 linker.ld 的实现，请自行创建好 **src/boot/linker.ld** 文件：

```rust
/* 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-os.json` ，发现编译失败了：

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

错误的原因是：no\_std 的程序会隐式地链接到 **core 库** 。 **core 库** 包含基础的 Rust 类型，如 Result、Option 和迭代器等。**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
```

也可把上面的命令放到`build.sh`脚本中，这样直接执行这个脚本就可以完成编译了。但我们发现产生了编译错误：

```
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 rust toolchains。为此需要在项目目录下建立一个rust-toolchain文件，文件内容是toolchians的版本信息：

```
nightly-2019-03-05
```

这样，在后续编译rust程序时，将采用上述版本的rust工具链。重新执行`build.sh`，发现我们的内核已经可以正确编译了。接下来的任务就是将他和 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
```

## 创建引导映象(Bootimage)

编写一个 bootloader 并将其与内核链接成 **引导映像** 并不是一个简单的事情，所以我们直接使用已有的 bootloader ：

[下载](https://github.com/LearningOS/rcore_step_by_step) 并将其中名为 **related items in lab2** 的文件夹中的两个子文件夹拷贝至 **Cargo.toml** 的同级目录下。

> 感兴趣的读者可以自行阅读 [zw 同学的 bbl 文档](https://ring00.github.io/bbl-ucore/#/toolchain-overview?id=all-about-bbl)

有了 bootloader ，那么只需要将其与我们的内核链接就可以了。这里我们需要使用到 **riscv-pk** 中的 **configure** 。为了以后能够方便的进行编译链接，我们需要编写一个 **Makefile** 文件（与 **Cargo.toml** 位于同级目录）:

```
target := riscv32-os
bbl_path := $(abspath riscv-pk)
mode := debug
kernel := target/$(target)/$(mode)/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-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
```

> 如果编译错误，基本上是因为没有安装 riscv 的交叉编译器。 执行 `riscv64-unknown-elf-gcc -v`，找不到程序的话就确定是这个错误了。
>
> 推荐的解决方式是直接使用已经编译好的交叉编译器。 可参考 [how to use "Prebuilt RISC‑V GCC Toolchain"](https://github.com/LearningOS/rcore_step_by_step/wiki/QA#%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8%E9%A2%84%E7%BC%96%E8%AF%91%E5%A5%BD%E7%9A%84gcc%E4%BA%A4%E5%8F%89%E7%BC%96%E8%AF%91%E5%B7%A5%E5%85%B7%E9%93%BE)

执行 `make kernel` 生成的 **kernel.bin** 就是我们需要的 **可以被 qemu 加载的 os** 。执行 `make run` ：

```
> make run
...
qemu-system-riscv32 -kernel target/riscv32-os/debug/kernel.bin -nographic -machine virt
bbl loader
```

至此，我们的 **最小内核** 已经“成功”跑起来了！！！吗？？？

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

## Hello World! -- step 1：显示字符

我们将用最简单的方法来验证 os 是否已经正确的被加载了：打印 **Hello World!** ：

```rust
#![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 中添加对其的依赖：

```rust
[dependencies]
bbl = { path = "crate/bbl" }
```

编译运行！很遗憾的发现，这位“新生儿”还没有学会说话（屏幕并没有显示 **Hello World!**）。还记得上一章的 \_start 吗？

* “你已经是一个成熟的 \_start 了，需要学会自己设置堆栈。”
* “我不是，我没有，别瞎说！”

## Hello World! -- step 2：设置堆栈

一个 **成熟的 \_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** 应该长成这样：

```rust
#![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](https://github.com/LearningOS/rcore_step_by_step/wiki/rust-lld:-error:-undefined-symbol:-abort) 。

那么，接下来，就是见证奇迹的时刻：

```
> make run
...
qemu-system-riscv32 -kernel target/riscv32-os/debug/kernel.bin -nographic -machine virt
bbl loader
Hello World!
```

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

## 预告

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