• 如何使用Rust编写OS系统内核?
  • 发布于 2个月前
  • 150 热度
    0 评论
  • 那场梦
  • 18 粉丝 53 篇博客
  •   
Rust是一门底层的操作系统语言,大家有没有相关使用Rust编写操作系统呢?这个系列将会告诉大家如何使用Rust编写自己的最小内核操作系统。

最小内核系统
要编写一个操作系统内核,我们需要编写不依赖任何操作系统特性的代码。这意味着我们不能使用线程、文件、堆内存、网络、随机数、标准输出,或其它任何需要操作系统抽象和特定硬件的特性;因为我们正在编写自己的操作系统和硬件驱动。实现这一个操作,Rust标准库的大部分代码我们都无法使用;但还好Rust还有很多特性是可以使用的,比如宏、迭代器、闭包、模式匹配(match)、Option、Result、字符串格式化。

为了用Rust编写一个操作系统内核,我们需要创建一个独立的操作系统可执行应用。独立式可执行程序。

禁用标准库
由于我们需要编写OS内核,很多标准库都无法使用。所以我们需要应用Rust标准库。如果有开发嵌入式的同学应该知道Rust拥有no_std的属性。
cargo new blog_os --bin --edition 2018
这里我们使用Rust 的 2018 版次。

no_std属性
为了禁用这种链接,我们可以尝试添加 no_std 属性:
// main.rs
// 堆代码 duidaima.com
#![no_std]
fn main() {
    println!("Hello, world!");
}
这个时候我们执行cargo build会出现一个异常: 这是因为println!宏是标准库的一部分,而我们的项目不再依赖于标准库。
error: cannot find macro `println!` in this scope
 --> src\main.rs:4:5
  |
4 |     println!("Hello, world!");
  |     ^^^^^^
实现 panic 处理函数
panic_handler 属性定义了一个函数,它会在一个 panic 发生时被调用。标准库中提供了自己的 panic 处理函数,但在 no_std 环境中,我们需要定义一个自己的 panic 处理函数:
// in main.rs

use core::panic::PanicInfo;

/// 这个函数将在 panic 时被调用
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop {}
}
eh_personality 语言项
语言项是一些编译器需求的特殊函数或类型。举例来说,Rust 的 Copy trait 是一个这样的语言项,告诉编译器哪些类型需要遵循复制语义(copy semantics)——当我们查找 Copy trait 的实现时,我们会发现,一个特殊的 #[lang = "copy"] 属性将它定义为了一个语言项,达到与编译器联系的目的。

我们可以自己实现语言项,但这是下下策:目前来看,语言项是高度不稳定的语言细节实现,它们不会经过编译期类型检查(所以编译器甚至不确保它们的参数类型是否正确)。幸运的是,我们有更稳定的方式,来修复上面的语言项错误。

eh_personality 语言项标记的函数,将被用于实现栈展开。在使用标准库的情况下,当 panic 发生时,Rust 将使用栈展开,来运行在栈上所有活跃的变量的析构函数——这确保了所有使用的内存都被释放,允许调用程序的父进程捕获 panic,处理并继续运行。但是,栈展开是一个复杂的过程,如 Linux 的 libunwind 或 Windows 的结构化异常处理,通常需要依赖于操作系统的库;所以我们不在自己编写的操作系统中使用它。

禁用栈展开
Rust 提供了在 panic 时中止的选项。这个选项能禁用栈展开相关的标志信息生成,也因此能缩小生成的二进制程序的长度。
cargo.toml
[profile.dev]
panic = "abort"

[profile.release]
panic = "abort"
start 语言项
它将定义一个程序的入口点,Rust 只拥有一个极小的运行时,它被设计为拥有较少的功能,如爆栈检测和打印栈轨迹。我们通常会认为,当运行一个程序时,首先被调用的是 main 函数。但是,大多数语言都拥有一个运行时系统。

我们也可以通过一下方式重写入口点
#![no_std]
#![no_main]

use core::panic::PanicInfo;

/// 这个函数将在 panic 时被调用
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop {}
}
我们移除了 main 函数。原因很显然,既然没有底层运行时调用它,main 函数也失去了存在的必要性。为了重写操作系统的入口点,我们转而编写一个 _start 函数:
#[no_mangle]
pub extern "C" fn _start() -> ! {
    loop {}
}
将函数标记为 extern "C",告诉编译器这个函数应当使用 C 语言的调用约定,而不是 Rust 语言的调用约定。函数名为 _start ,是因为大多数系统默认使用这个名字作为入口点名称。

链接器
链接器(linker)是一个程序,它将生成的目标文件组合为一个可执行文件。不同的操作系统如 Windows、macOS、Linux,规定了不同的可执行文件格式,因此也各有自己的链接器。

编译为裸机目标
在默认情况下,Rust 尝试适配当前的系统环境,编译可执行程序。举个例子,如果你使用 x86_64 平台的 Windows 系统,Rust 将尝试编译一个扩展名为 .exe 的 Windows 可执行程序,并使用 x86_64 指令集。这个环境又被称作为你的宿主系统。
我们可以使用 rustc --version --verbose 查看当前的目标信息。
rustc 1.35.0-nightly (474e7a648 2019-04-07)
binary: rustc
commit-hash: 474e7a6486758ea6fc761893b1a49cd9076fb0ab
commit-date: 2019-04-07
host: x86_64-unknown-linux-gnu
release: 1.35.0-nightly
LLVM version: 8.0
我们能看到,host 字段的值为三元组 x86_64-unknown-linux-gnu。 当然你也可通过自己的方式添加三元组,比如ARM嵌入式三元组。
rustup target add thumbv7em-none-eabihf
cargo build --target thumbv7em-none-eabihf
总结
一个小的Rust编写最小化没有标准库可独立运行应用
#![no_std] // 不链接 Rust 标准库
#![no_main] // 禁用所有 Rust 层级的入口点

use core::panic::PanicInfo;

#[no_mangle] // 不重整函数名
pub extern "C" fn _start() -> ! {
    // 因为链接器会寻找一个名为 `_start` 的函数,所以这个函数就是入口点
    // 默认命名为 `_start`
    loop {}
}

/// 这个函数将在 panic 时被调用
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop {}
}
Cargo.toml:

[package]
name = "crate_name"
version = "0.1.0"
authors = ["Author Name <author@example.com>"]

# 使用 `cargo build` 编译时需要的配置
[profile.dev]
panic = "abort" # 禁用panic时栈展开

# 使用 `cargo build --release` 编译时需要的配置
[profile.release]
panic = "abort" # 禁用 panic 时栈展开
选用任意一个裸机目标来编译。比如对 thumbv7em-none-eabihf,我们使用以下命令:
cargo build --target thumbv7em-none-eabihf
编译目标系统
# Linux
cargo rustc -- -C link-arg=-nostartfiles
# Windows
cargo rustc -- -C link-args="/ENTRY:_start /SUBSYSTEM:console"
# macOS
cargo rustc -- -C link-args="-e __start -static -nostartfiles"
运行这个二进制程序还需要很多准备,比如在 _start 函数之前需要一个已经预加载完毕的栈。所以为了真正运行这样的程序,我们还有很多事情需要做。
用户评论