移除标准库依赖

本节导读

为了很好地理解一个简单应用所需的服务如何体现,本节将尝试开始构造一个小的执行环境,可建立在 Linux 之上,也可直接建立在裸机之上,我们称为“三叶虫”操作系统。作为第一步,本节将尝试移除之前的 Hello world! 程序对于 Rust std 标准库的依赖,使得它能够编译到裸机平台 RV64GC 或 Linux-RV64 上。

移除 println! 宏

我们首先在 os 目录下新建 .cargo 目录,并在这个目录下创建 config 文件,并在里面输入如下内容:

  1. # os/.cargo/config
  2. [build]
  3. target = "riscv64gc-unknown-none-elf"

这会对于 Cargo 工具在 os 目录下的行为进行调整:现在默认会使用 riscv64gc 作为目标平台而不是原先的默认 x86_64-unknown-linux-gnu。事实上,这是一种编译器运行所在的平台与编译器生成可执行文件的目标平台不同(分别是后者和前者)的情况。这是一种 交叉编译 (Cross Compile)。

当然,这只是使得我们之后在 cargo build 的时候不必再加上 --target 参数的一个小 trick。如果我们现在 cargo build ,还是会和上一小节一样出现找不到标准库 std 的错误。于是我们开始着手移除标准库,当然,这会产生一些副作用。

我们在 main.rs 的开头加上一行 #![no_std] 来告诉 Rust 编译器不使用 Rust 标准库 std 转而使用核心库 core。编译器报出如下错误:

错误

  1. $ cargo build
  2. Compiling os v0.1.0 (/home/shinbokuow/workspace/v3/rCore-Tutorial-v3/os)
  3. error: cannot find macro `println` in this scope
  4. --> src/main.rs:4:5
  5. |
  6. 4 | println!("Hello, world!");
  7. | ^^^^^^^

我们之前提到过, println! 宏是由标准库 std 提供的,且会使用到一个名为 write 的系统调用。现在我们的代码功能还不足以自己实现一个 println! 宏。由于使用了系统调用也不能在核心库 core 中找到它,所以我们目前先通过将它注释掉来绕过它。

提供语义项 panic_handler

错误

  1. $ cargo build
  2. Compiling os v0.1.0 (/home/shinbokuow/workspace/v3/rCore-Tutorial-v3/os)
  3. error: `#[panic_handler]` function required, but not found

在使用 Rust 编写应用程序的时候,我们常常在遇到了一些无法恢复的致命错误导致程序无法继续向下运行的时候手动或自动调用 panic! 宏来并打印出错的位置让我们能够意识到它的存在,并进行一些后续处理。panic! 宏最典型的应用场景包括断言宏 assert! 失败或者对 Option::None/Result::Err 进行 unwrap 操作。

在标准库 std 中提供了 panic 的处理函数 #[panic_handler],其大致功能是打印出错位置和原因并杀死当前应用。可惜的是在核心库 core 中并没有提供,因此我们需要自己实现 panic 处理函数。

注解

Rust 语法卡片:语义项 lang_items

Rust 编译器内部的某些功能的实现并不是硬编码在语言内部的,而是以一种可插入的形式在库中提供。库只需要通过某种方式告诉编译器它的某个方法实现了编译器内部的哪些功能,编译器就会采用库提供的方法来实现它内部对应的功能。通常只需要在库的方法前面加上一个标记即可。

我们开一个新的子模块 lang_items.rs 保存这些语义项,在里面提供 panic 处理函数的实现并通过标记通知编译器采用我们的实现:

  1. // os/src/lang_items.rs
  2. use core::panic::PanicInfo;
  3. #[panic_handler]
  4. fn panic(_info: &PanicInfo) -> ! {
  5. loop {}
  6. }

注意,panic 处理函数的函数签名需要一个 PanicInfo 的不可变借用作为输入参数,它在核心库中得以保留,这也是我们第一次与核心库打交道。之后我们会从 PanicInfo 解析出错位置并打印出来,然后杀死应用程序。但目前我们什么都不做只是在原地 loop

移除 main 函数

错误

  1. $ cargo build
  2. Compiling os v0.1.0 (/home/shinbokuow/workspace/v3/rCore-Tutorial-v3/os)
  3. error: requires `start` lang_item

编译器提醒我们缺少一个名为 start 的语义项。我们回忆一下,之前提到语言标准库和三方库作为应用程序的执行环境,需要负责在执行应用程序之前进行一些初始化工作,然后才跳转到应用程序的入口点(也就是跳转到我们编写的 main 函数)开始执行。事实上 start 语义项正代表着标准库 std 在执行应用程序之前需要进行的一些初始化工作。由于我们禁用了标准库,编译器也就找不到这项功能的实现了。

最简单的解决方案就是压根不让编译器使用这项功能。我们在 main.rs 的开头加入设置 #![no_main] 告诉编译器我们没有一般意义上的 main 函数,并将原来的 main 函数删除。在失去了 main 函数的情况下,编译器也就不需要完成所谓的初始化工作了。

至此,我们成功移除了标准库的依赖并完成裸机平台上的构建。

  1. $ cargo build
  2. Compiling os v0.1.0 (/home/shinbokuow/workspace/v3/rCore-Tutorial-v3/os)
  3. Finished dev [unoptimized + debuginfo] target(s) in 0.06s

目前的代码如下:

  1. // os/src/main.rs
  2. #![no_std]
  3. #![no_main]
  4. mod lang_items;
  5. // os/src/lang_items.rs
  6. use core::panic::PanicInfo;
  7. #[panic_handler]
  8. fn panic(_info: &PanicInfo) -> ! {
  9. loop {}
  10. }

本小节我们固然脱离了标准库,通过了编译器的检验,但也是伤筋动骨,将原有的很多功能弱化甚至直接删除,看起来距离在 RV64GC 平台上打印 Hello world! 相去甚远了(我们甚至连 println! 和 main 函数都删除了)。不要着急,接下来我们会以自己的方式来重塑这些基本功能,并最终完成我们的目标。

分析被移除标准库的程序

对于上面这个被移除标准库的应用程序,通过了编译器的检查和编译,形成了二进制代码。但这个二进制代码是怎样的,它能否被正常执行呢?我们可以通过一些工具来分析一下。

  1. [文件格式]
  2. $ file target/riscv64gc-unknown-none-elf/debug/os
  3. target/riscv64gc-unknown-none-elf/debug/os: ELF 64-bit LSB executable, UCB RISC-V, ......
  4. [文件头信息]
  5. $ rust-readobj -h target/riscv64gc-unknown-none-elf/debug/os
  6. File: target/riscv64gc-unknown-none-elf/debug/os
  7. Format: elf64-littleriscv
  8. Arch: riscv64
  9. AddressSize: 64bit
  10. ......
  11. Type: Executable (0x2)
  12. Machine: EM_RISCV (0xF3)
  13. Version: 1
  14. Entry: 0x0
  15. ......
  16. }
  17. [反汇编导出汇编程序]
  18. $ rust-objdump -S target/riscv64gc-unknown-none-elf/debug/os
  19. target/riscv64gc-unknown-none-elf/debug/os: file format elf64-littleriscv

通过 file 工具对二进制程序 os 的分析可以看到它好像是一个合法的 RV64 执行程序,但通过 rust-readobj 工具进一步分析,发现它的入口地址 Entry 是 0 ,这就比较奇怪了,地址从 0 执行,好像不对。再通过 rust-objdump 工具把它反汇编,可以看到没有生成汇编代码。所以,我们可以断定,这个二进制程序虽然合法,但它是一个空程序。这不是我们希望的,我们希望有具体内容的执行程序。为什么会这样呢?原因是我们缺少了编译器需要找到的入口函数 _start

在下面几节,我们将建立有支持显示字符串的最小执行环境。

注解

在 x86_64 平台上移除标准库依赖

有兴趣的同学可以将目标平台换回之前默认的 x86_64-unknown-linux-gnu 并重复本小节所做的事情,比较两个平台从 ISA 到操作系统 的差异。可以参考 BlogOS 的相关内容

注解

本节内容部分参考自 BlogOS 的相关章节