# 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!** ，以便于后续的调试输出。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://xy-plus.gitbook.io/rcore-step-by-step/zui-xiao-hua-nei-he.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
