写操作系统内核的时候,我们的代码无法依赖于任何操作系统的特性,除非是那些自己实现了的。这意味着我们无法使用线程、文件、堆内存、网络、随机数、标准输出等等等。
我们无法使用Rust的标准库,但是仍然可以使用Rust提供的迭代器、闭包、模式匹配、Option, Result、格式化字符串(string formatting)以及所有权系统。
为了使用Rust创建操作系统内核,我们需要创建不依赖于底层操作系统的二进制可执行文件。这样的可执行文件通常被称为”freestanding”或”bare-metal”可执行文件。
1. 创建bare-metal可执行文件
1.1. 禁用标准库
1 | |
1.2. 实现panic处理函数
默认情况下,当rust程序出现panic时,编译器会调用标准库提供的panic handler function,但是在no_std环境下,我们需要自己定义该函数
1 | |
PanicInfo参数包含了panic发生的文件和行以及可选的panic信息。同时定义该函数为发散函数(diverging function)。
1.3. 语言项eh_personality
语言项(language items)是被编译器内部需要的特殊函数或类型。例如,Copytrait就是一个语言项,它告知编译器哪些类型有copy语义。在Copy的实现中,可以看到#[lang = "copy"]属性将它定义成一个语言项。
1 | |
通常避免自己实现语言项。
eh_personality语言项标记了用于实现栈展开(stack unwinding)的函数。默认情况下,rust在出现panic的情况下使用该机制来运行栈内变量的析构函数。然而,栈展开需要使用到一些OS具体的库。
1.3.1. 禁用栈展开
当不使用栈展开时,rust也提供了abort on panic的选择。这种方式禁止了展开符号(unwinding symbol)相关信息的生成因此能够减小二进制文件的大小。最容易的方式是在Cargo.toml文件中:
1 | |
如上,同时为dev(cargo build)和release(cargo build --release)的panic情况使用了abort策略。
接着再次编译的时候会出现
1 | |
1.4. start属性
典型的rust二进制程序的执行从C运行时库crt0(“C runtime zero”)开始。它会为c应用程序创建栈、放置命令行参数。接着调用Rust程序的运行时进入点,该进入点由start语言项标注。rust仅有一个非常小的运行时,在其中rust会设置栈溢出的guards或者在panic时打印backtrace。最终该运行时调用main函数。
freestanding的可执行文件无法访问rust运行时和crt0,因而我们需要覆写crt0进入点。
1.4.1. 覆写进入点(entry point)
为告知rust编译器我们不需要使用通常的进入点链,需要加上#![no_main],同时移除main函数
1 | |
使用#[no_mangle]属性意味着我们禁用name mangling以确保Rust编译器输出的名称为_start的函数。若没有这个属性,编译器会生成某个加密的函数名符号。这里使用这个属性,是为了下一步告知linker进入点函数名。
使用extern "C"告知编译器使用C调用惯例。函数名使用_start是因为大多数系统的默认进入点名为_start。
注意到这也是一个发散函数。接着运行cargo build,会收到一个链接器错误。
1.5. 链接器错误
抛出错的原因在于默认情况下linker会假设程序依赖c运行时。
为了解决这个错误,需要告知linker不需要包含c运行时。
1.5.1. 方法一:构建bare metal目标
默认情况下,rust会尝试为当前host系统环境构建可执行文件。例如,在x86_64 Windows下,rust会尝试构建.exe。
为了描述不同的环境,rust使用了被称为target triple的字符串。通过运行rustc --version --verbose可以查看host系统的tartget triple
1 | |
可以注意到target triple为x86_64-unknown-linux-gnu,分别表示CPU架构(x86_64),发行商(unknown),操作系统(linux)以及ABI(gnu)。因此当为了host编译时,rust编译器会假设存在默认使用c运行时库的底层linux系统,从而导致linker错误。为了解决linker错误,可以为了另一个不存在底层操作系统的目标而编译。
一个bare metal环境的示例是thumbv7em-none-eabihf,其描述的是嵌入式arm的系统。none表示无底层操作系统。为了能编译该目标,我们需要将他加入rustup:
1 | |
该操作会下载对应系统标准(standart)和核心(core)库。之后即可为该目标编译
1 |
|
通过--target参数我们为bare metal目标系统交叉编译了可执行文件。
1.5.2. 方法二:使用linker参数
不同的host系统的linker使用不同的参数,下面主要讨论如何解决linux下linker的错误。首先linux下linker的错误如下:
1 | |
问题在于linker默认包含了c运行时的启动例程,也为_start。c运行时的_start需要使用标准库的很多符号,然而由于我们使用了no_std属性,导致linker无法决议这些符号。为了解决该问题,我们可以使用-nostartfiles告知linker它不需要link c启动例程。
一种传递linker参数的方式是通过cargocargo rustc命令,该命令和cargo build等价,但同时允许给底层的rust编译器rustc传递选项。rustc存在-C link-argflag,能够给linker传递参数。总结起来就是
1 | |
我们不需要指明进入点函数的名字,因为linker默认查找名字为_start的函数作为进入点。
1.5.3. 整合构建命令
上面说的针对于linux系统的linker,然而对于其他系统不太适用。为解决这个问题,我们可以创建名为.cargo/config.toml的文件,并加入平台具体的参数如下:
1 | |
rustflags键包含了每次对rustc调用时自动加入的参数。
1.6. 小结
综上,一个最小的free standing的rust库如下
src/main.rs:
1 | |
Cargo.toml
1 | |
使用如下命令进行交叉编译
1 |
|
2. Freestanding/Baremetal Rust
2.1. about libcore, liballoc, libstd
- libcore: 无依赖的rust核心库(core),无libc,无heap
- 要求
panic和eh_personality
- 要求
- liballoc: 智能指针和堆管理的集合(即,Box)
- 要求
global_allocator和alloc_error_handler - 在有了基于物理内存页的受管理的堆内存后能够使用
- 要求
- libstd: rust软件的共有抽象的集合(例如,I/O,网络,线程)
- 依赖于
libcore和liballoc
- 依赖于
2.2. no_std
无默认的prelude和标准库
2.3. no_main
- 无
fn main()定义(即,没有明确定义进入点) - linker默认的进入点是
_start()
1 | |
2.4. panic_handler
1 | |
2.5. eh_personality
填补了系统和语言对于异常处理的语义差异
1 | |
2.6. 关于.elf和.bin
.elf是可执行目标文件,仍需要loader装载到内存,并可能存在load time的动态链接,因此文件中仍存在一些符号信息、重定位条目等。
.bin可以不需要loader提供的一些其他功能,只需放置到内存即可从entry point开始执行。不存在额外信息,因而大小也更小。自己写的bare-metal内核可以先生成elf文件,然后使用objcopy转为binary。
具体到rust,可以在生成.elf后使用
1 | |