使用Rust从零写操作系统 (2) —— 最小化内核

释放双眼,带上耳机,听听看~!

本系列博客系转载,出处: 知乎专栏:从零开始写 OS

所有代码都在:https://github.com/LearningOS/rcore_step_by_step


1
2
3
1本章代码对应 commit :40ee051072e5a4b89ca188d5620e9b30f1b68b25
2
3

概要

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

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

修改三元组

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

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


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1{
2  &quot;llvm-target&quot;: &quot;x86_64-unknown-linux-gnu&quot;,
3  &quot;data-layout&quot;: &quot;e-m:e-i64:64-f80:128-n8:16:32:64-S128&quot;,
4  &quot;arch&quot;: &quot;x86_64&quot;,
5  &quot;target-endian&quot;: &quot;little&quot;,
6  &quot;target-pointer-width&quot;: &quot;64&quot;,
7  &quot;target-c-int-width&quot;: &quot;32&quot;,
8  &quot;os&quot;: &quot;linux&quot;,
9  &quot;executables&quot;: true,
10  &quot;linker-flavor&quot;: &quot;gcc&quot;,
11  &quot;pre-link-args&quot;: [&quot;-m64&quot;],
12  &quot;morestack&quot;: false
13}
14
15

因为我们的主要目的是编写 os ,所以这里直接给出目标文件的实现:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
1// in riscv32-xy_os.json
2
3{
4  &quot;llvm-target&quot;: &quot;riscv32&quot;,
5  &quot;data-layout&quot;: &quot;e-m:e-p:32:32-i64:64-n32-S128&quot;,
6  &quot;target-endian&quot;: &quot;little&quot;,
7  &quot;target-pointer-width&quot;: &quot;32&quot;,
8  &quot;target-c-int-width&quot;: &quot;32&quot;,
9  &quot;os&quot;: &quot;none&quot;,
10  &quot;arch&quot;: &quot;riscv32&quot;,
11  &quot;cpu&quot;: &quot;generic-rv32&quot;,
12  &quot;features&quot;: &quot;+m,+a,+c&quot;,
13  &quot;max-atomic-width&quot;: &quot;32&quot;,
14  &quot;linker&quot;: &quot;rust-lld&quot;,
15  &quot;linker-flavor&quot;: &quot;ld.lld&quot;,
16  &quot;pre-link-args&quot;: {
17    &quot;ld.lld&quot;: [&quot;-Tsrc/boot/linker.ld&quot;]
18  },
19  &quot;executables&quot;: true,
20  &quot;panic-strategy&quot;: &quot;abort&quot;,
21  &quot;relocation-model&quot;: &quot;static&quot;,
22  &quot;eliminate-frame-pointer&quot;: false
23}
24
25

对文件各参数细节感兴趣的读者可以自行研究,这里只对 pre-link-args 进行解释:


1
2
3
4
5
6
7
1&quot;pre-link-args&quot;: {
2    &quot;ld.lld&quot;: [
3      &quot;-Tsrc/boot/linker.ld&quot;
4    ]
5  },
6
7

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


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
1/* Copy from bbl-ucore : https://ring00.github.io/bbl-ucore      */
2
3/* Simple linker script for the ucore kernel.
4   See the GNU ld &#x27;info&#x27; manual (&quot;info ld&quot;) to learn the syntax. */
5
6OUTPUT_ARCH(riscv)
7ENTRY(_start)
8
9BASE_ADDRESS = 0xC0020000;
10
11SECTIONS
12{
13    . = 0xC0000000;
14    .boot : {
15        KEEP(*(.text.boot))
16    }
17
18    /* Load the kernel at this address: &quot;.&quot; means the current address */
19    . = BASE_ADDRESS;
20    start = .;
21
22    .text : {
23        stext = .;
24        *(.text.entry)
25        *(.text .text.*)
26        . = ALIGN(4K);
27        etext = .;
28    }
29
30    .rodata : {
31        srodata = .;
32        *(.rodata .rodata.*)
33        . = ALIGN(4K);
34        erodata = .;
35    }
36
37    .data : {
38        sdata = .;
39        *(.data .data.*)
40        edata = .;
41    }
42
43    .stack : {
44        *(.bss.stack)
45    }
46
47    .bss : {
48        sbss = .;
49        *(.bss .bss.*)
50        ebss = .;
51    }
52
53    PROVIDE(end = .);
54}
55
56

运行 cargo build –target riscv32-xy_os.json ,发现编译失败了:


1
2
3
1error[E0463]: can&#x27;t find crate for `core`
2
3

错误的原因是:no_std 的上下文隐式地链接到 core 库 。core 库 包含基础的 Rust 类型,如 Result、Option 和迭代器等。core 库 只支持原生的 目标三元组 ,而我们在编写 OS 时使用的是自定义的 目标三元组 。

如果我们想为其他系统编译代码,我们需要为这些系统重新编译整个 core 库 。这就是为什么我们需要 cargo xbuild 。

Cargo xbuild

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

cargo install cargo-xbuild

现在运行 cargo xbuild –target riscv32-xy_os.json ,我们的内核已经可以正确编译了。接下来的任务就是将他和 bootloader 链接,得到可以被 qemu 加载的 os 。

创建引导映象(Bootimage)

编写一个 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位于同级目录):


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
1target := riscv32-xy_os
2bbl_path := $(abspath riscv-pk)
3mode := debug
4kernel := target/$(target)/$(mode)/xy_os
5bin := target/$(target)/$(mode)/kernel.bin
6
7.PHONY: all clean run build asm qemu kernel
8
9all: kernel
10
11$(bin): kernel
12    mkdir -p target/$(target)/bbl &amp;&amp; \
13    cd target/$(target)/bbl &amp;&amp; \
14    $(bbl_path)/configure \
15        --with-arch=rv32imac \
16        --disable-fp-emulation \
17        --host=riscv64-unknown-elf \
18        --with-payload=$(abspath $(kernel)) &amp;&amp; \
19    make -j32 &amp;&amp; \
20    cp bbl $(abspath $@)
21
22build: $(bin)
23
24run: build qemu
25
26kernel:
27    @cargo xbuild --target riscv32-xy_os.json
28
29asm:
30    @riscv64-unknown-elf-objdump -d $(kernel) | less
31
32qemu:
33    qemu-system-riscv32 -kernel $(bin) -nographic -machine virt
34
35docker:
36    sudo docker run -it --mount type=bind,source=$(shell pwd)/..,destination=/mnt panqinglin/rust_riscv bash
37
38

未安装 Prebuilt RISC‑V GCC Toolchain 会导致编译错误,请参考 how to use “Prebuilt RISC‑V GCC Toolchain” 下载使用。

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


1
2
3
4
5
6
1&gt; make run
2...
3qemu-system-riscv32 -kernel target/riscv32-xy_os/debug/kernel.bin -nographic -machine virt
4bbl loader
5
6

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

第一个Hello World程序

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


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1#![no_std] // don&#x27;t link the Rust standard library
2#![no_main] // disable all Rust-level entry points
3
4use core::panic::PanicInfo;
5use bbl::sbi;
6
7#[panic_handler]
8fn panic(_info: &amp;PanicInfo) -&gt; ! {
9    loop {}
10}
11static HELLO: &amp;[u8] = b&quot;Hello World!&quot;;
12
13#[no_mangle]
14pub extern &quot;C&quot; fn main() -&gt; ! {
15    for &amp;c in HELLO {
16        sbi::console_putchar(c as usize);
17    }
18    loop {}
19}
20
21

bbl::sbi是 依赖项目 中已经完成的库,可以使用 sbi::console_putchar(usize) 打印一个 ASCII字符 。使用前需要在 Cargo.toml 中添加对其的依赖。


1
2
3
4
1[dependencies]
2bbl = { path = &quot;crate/bbl&quot; }
3
4

编译运行!很遗憾的发现,这位“新生儿”还没有学会说话(屏幕并没有显示 Hello World!)。还记得上一章的 _start 吗?

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

一个 成熟的_start需要能够设置一些简单的堆栈信息,然后跳转至 main 函数。所以我们需要使用 汇编语言 重写 _start。在 src/boot中创建 entry.asm:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1.section .text.entry
2    .globl _start
3_start:
4    add t0, a0, 1
5    slli t0, t0, 16
6
7    lui sp, %hi(bootstack)
8    addi sp, sp, %lo(bootstack)
9    add sp, sp, t0
10
11    call rust_main
12
13    .section .bss.stack
14    .align 12  #PGSHIFT
15    .global bootstack
16bootstack:
17    .space 4096 * 16 * 8
18    .global bootstacktop
19bootstacktop:
20
21

然后在 main.rs 中通过 global_asm 引入_start,并实现 rust_main 。现在 main.rs 应该长成这样:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
1#![no_std] // don&#x27;t link the Rust standard library
2#![no_main] // disable all Rust-level entry points
3#![feature(global_asm)]
4
5use core::panic::PanicInfo;
6use bbl::sbi;
7
8global_asm!(include_str!(&quot;boot/entry.asm&quot;));
9
10#[panic_handler]
11fn panic(_info: &amp;PanicInfo) -&gt; ! {
12    loop {}
13}
14
15static HELLO: &amp;[u8] = b&quot;Hello World!&quot;;
16
17#[no_mangle]
18pub extern &quot;C&quot; fn rust_main() -&gt; ! {
19    for &amp;c in HELLO {
20        sbi::console_putchar(c as usize);
21    }
22    loop {}
23}
24
25#[no_mangle]
26pub extern fn abort() {
27    panic!(&quot;abort!&quot;);
28}
29
30

#![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 。

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


1
2
3
4
5
6
7
1&gt; make run
2...
3qemu-system-riscv32 -kernel target/riscv32-xy_os/debug/kernel.bin -nographic -machine virt
4bbl loader
5Hello World!
6
7

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

预告

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

给TA打赏
共{{data.count}}人
人已打赏
安全技术

c++ list, vector, map, set 区别与用法比较

2022-1-11 12:36:11

安全技术

JavaScript面向对象编程(11)其他继承方式

2022-1-12 12:36:11

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索