# 构建裸机执行环境 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档

Hide navigation sidebar
Hide table of contents sidebar
Toggle table of contents sidebar

# 构建裸机执行环境

有了上一节实现的用户态的最小执行环境，稍加改造，就可以完成裸机上的最小执行环境了。 本节中，我们将把 `Hello world!` 应用程序从用户态搬到内核态。

## 裸机启动过程

用 QEMU 软件 `qemu-system-riscv64` 来模拟 RISC-V 64 计算机。加载内核程序的命令如下：

    qemu-system-riscv64 \
                -machine virt \
                -nographic \
                -bios $(BOOTLOADER) \
                -device loader,file=$(KERNEL_BIN),addr=$(KERNEL_ENTRY_PA)

- `-bios $(BOOTLOADER)` 意味着硬件加载了一个 BootLoader 程序，即 RustSBI

- `-device loader,file=$(KERNEL_BIN),addr=$(KERNEL_ENTRY_PA)` 表示硬件内存中的特定位置 `$(KERNEL_ENTRY_PA)` 放置了操作系统的二进制代码 `$(KERNEL_BIN)` 。 `$(KERNEL_ENTRY_PA)` 的值是 `0x80200000` 。

当我们执行包含上述启动参数的 qemu-system-riscv64 软件，就意味给这台虚拟的 RISC-V64 计算机加电了。 此时，CPU 的其它通用寄存器清零，而 PC 会指向 `0x1000` 的位置，这里有固化在硬件中的一小段引导代码， 它会很快跳转到 `0x80000000` 的 RustSBI 处。 RustSBI完成硬件初始化后，会跳转到 `$(KERNEL_BIN)` 所在内存位置 `0x80200000` 处， 执行操作系统的第一条指令。

注解

RustSBI 是什么？

SBI 是 RISC-V 的一种底层规范，RustSBI 是它的一种实现。 操作系统内核与 RustSBI 的关系有点像应用与操作系统内核的关系，后者向前者提供一定的服务。只是SBI提供的服务很少， 比如关机，显示字符串等。

## 实现关机功能

对上一节实现的代码稍作调整，通过 `ecall` 调用 RustSBI 实现关机功能：

    // bootloader/rustsbi-qemu.bin 直接添加的SBI规范实现的二进制代码，给操作系统提供基本支持服务

    // os/src/sbi.rs
    fn sbi_call(which: usize, arg0: usize, arg1: usize, arg2: usize) -> usize {
     let mut ret;
      unsafe {
          core::arch::asm!(
              "ecall",
    ...

    const SBI_SHUTDOWN: usize = 8;

    pub fn shutdown() -> ! {
        sbi_call(SBI_SHUTDOWN, 0, 0, 0);
        panic!("It should shutdown!");
    }

    // os/src/main.rs
    #[no_mangle]
    extern "C" fn _start() {
        shutdown();
    }

应用程序访问操作系统提供的系统调用的指令是 `ecall` ，操作系统访问 RustSBI提供的SBI调用的指令也是 `ecall` ， 虽然指令一样，但它们所在的特权级是不一样的。 简单地说，应用程序位于最弱的用户特权级（User Mode）， 操作系统位于内核特权级（Supervisor Mode）， RustSBI位于机器特权级（Machine Mode）。 下一章会进一步阐释具体细节。

编译执行，结果如下：

    # 编译生成ELF格式的执行文件
    $ cargo build --release
     Compiling os v0.1.0 (/media/chyyuu/ca8c7ba6-51b7-41fc-8430-e29e31e5328f/thecode/rust/os_kernel_lab/os)
      Finished release [optimized] target(s) in 0.15s
    # 把ELF执行文件转成bianary文件
    $ rust-objcopy --binary-architecture=riscv64 target/riscv64gc-unknown-none-elf/release/os --strip-all -O binary target/riscv64gc-unknown-none-elf/release/os.bin

    # 加载运行
    $ qemu-system-riscv64 -machine virt -nographic -bios ../bootloader/rustsbi-qemu.bin -device loader,file=target/riscv64gc-unknown-none-elf/release/os.bin,addr=0x80200000
    # 无法退出，风扇狂转，感觉碰到死循环

问题在哪？通过 rust-readobj 分析 `os` 可执行程序，发现其入口地址不是 RustSBI 约定的 `0x80200000` 。我们需要修改程序的内存布局并设置好栈空间。

## 设置正确的程序内存布局

可以通过 链接脚本 (Linker Script) 调整链接器的行为，使得最终生成的可执行文件的内存布局符合我们的预期。

修改 Cargo 的配置文件来使用我们自己的链接脚本 `os/src/linker.ld`：

    1// os/.cargo/config
    2[build]
    3target = "riscv64gc-unknown-none-elf"
    4
    5[target.riscv64gc-unknown-none-elf]
    6rustflags = [
    7    "-Clink-arg=-Tsrc/linker.ld", "-Cforce-frame-pointers=yes"
    8]

具体的链接脚本 `os/src/linker.ld` 如下：

     1OUTPUT_ARCH(riscv)
     2ENTRY(_start)
     3BASE_ADDRESS = 0x80200000;
     4
     5SECTIONS
     6{
     7    . = BASE_ADDRESS;
     8    skernel = .;
     9
    10    stext = .;
    11    .text : {
    12        *(.text.entry)
    13        *(.text .text.*)
    14    }
    15
    16    . = ALIGN(4K);
    17    etext = .;
    18    srodata = .;
    19    .rodata : {
    20        *(.rodata .rodata.*)
    21    }
    22
    23    . = ALIGN(4K);
    24    erodata = .;
    25    sdata = .;
    26    .data : {
    27        *(.data .data.*)
    28    }
    29
    30    . = ALIGN(4K);
    31    edata = .;
    32    .bss : {
    33        *(.bss.stack)
    34        sbss = .;
    35        *(.bss .bss.*)
    36    }
    37
    38    . = ALIGN(4K);
    39    ebss = .;
    40    ekernel = .;
    41
    42    /DISCARD/ : {
    43        *(.eh_frame)
    44    }
    45}

第 1 行我们设置了目标平台为 riscv ；第 2 行我们设置了整个程序的入口点为之前定义的全局符号 `_start`； 第 3 行定义了一个常量 `BASE_ADDRESS` 为 `0x80200000` ，RustSBI 期望的 OS 起始地址；

注意

linker 脚本的语法不做要求，感兴趣的同学可以自行查阅相关资料。

从 `BASE_ADDRESS` 开始，代码段 `.text`, 只读数据段 `.rodata`，数据段 `.data`, bss 段 `.bss` 由低到高依次放置， 且每个段都有两个全局变量给出其起始和结束地址（比如 `.text` 段的开始和结束地址分别是 `stext` 和 `etext` ）。

## 正确配置栈空间布局

用另一段汇编代码初始化栈空间：

     1# os/src/entry.asm
     2    .section .text.entry
     3    .globl _start
     4_start:
     5    la sp, boot_stack_top
     6    call rust_main
     7
     8    .section .bss.stack
     9    .globl boot_stack
    10boot_stack:
    11    .space 4096 * 16
    12    .globl boot_stack_top
    13boot_stack_top:

在第 8 行，我们预留了一块大小为 4096 * 16 字节，也就是 \(64\text{KiB}\) 的空间， 用作操作系统的栈空间。 栈顶地址被全局符号 `boot_stack_top` 标识，栈底则被全局符号 `boot_stack` 标识。 同时，这块栈空间被命名为 `.bss.stack` ，链接脚本里有它的位置。

`_start` 作为操作系统的入口地址，将依据链接脚本被放在 `BASE_ADDRESS` 处。 `la sp, boot_stack_top` 作为 OS 的第一条指令， 将 sp 设置为栈空间的栈顶。 简单起见，我们目前不考虑 sp 越过栈底 `boot_stack` ，也就是栈溢出的情形。 第二条指令则是函数调用 `rust_main` ，这里的 `rust_main` 是我们稍后自己编写的应用入口。

接着，我们在 `main.rs` 中嵌入这些汇编代码并声明应用入口 `rust_main` ：

     1// os/src/main.rs
     2#![no_std]
     3#![no_main]
     4
     5mod lang_items;
     6
     7core::arch::global_asm!(include_str!("entry.asm"));
     8
     9#[no_mangle]
    10pub fn rust_main() -> ! {
    11    shutdown();
    12}

背景高亮指出了 `main.rs` 中新增的代码。

第 7 行，我们使用 `global_asm` 宏，将同目录下的汇编文件 `entry.asm` 嵌入到代码中。

从第 9 行开始， 我们声明了应用的入口点 `rust_main` ，需要注意的是，这里通过宏将 `rust_main` 标记为 `#[no_mangle]` 以避免编译器对它的名字进行混淆，不然在链接时， `entry.asm` 将找不到 `main.rs` 提供的外部符号 `rust_main`，导致链接失败。

再次使用上节中的编译，生成和运行操作，我们看到QEMU模拟的RISC-V 64计算机 优雅 地退出了！

## 清空 .bss 段

等一等，与内存相关的部分太容易出错了， 清零 .bss 段 的工作我们还没有完成。

     1// os/src/main.rs
     2fn clear_bss() {
     3    extern "C" {
     4        fn sbss();
     5        fn ebss();
     6    }
     7    (sbss as usize..ebss as usize).for_each(|a| {
     8        unsafe { (a as *mut u8).write_volatile(0) }
     9    });
    10}
    11
    12pub fn rust_main() -> ! {
    13    clear_bss();
    14    shutdown();
    15}

链接脚本 `linker.ld` 中给出的全局符号 `sbss` 和 `ebss` 让我们能轻松确定 `.bss` 段的位置。

## 添加裸机打印相关函数

在上一节中我们为用户态程序实现的 `println` 宏，略作修改即可用于本节的内核态操作系统。 详见 `os/src/console.rs`。

利用 `println` 宏，我们重写异常处理函数 `panic`，使其在 panic 时能打印错误发生的位置。 相关代码位于 `os/src/lang_items.rs` 中。

我们还使用第三方库 `log` 为你实现了日志模块，相关代码位于 `os/src/logging.rs` 中。

注解

在 cargo 项目中引入外部库 log，需要修改 `Cargo.toml` 加入相应的依赖信息。

现在，让我们重复一遍本章开头的试验，`make run LOG=TRACE`！

产生 panic 的地点与源码中的实际位置一致！至此，我们完成了第一章的实验内容，

注解

背景知识：理解应用程序和执行环境