第一步,先通过cargo创建一个新项目:
1 | cargo new rtoyos |
项目结构如下:
1 | ├── Cargo.toml |
但是Rust项目默认都会链接到std标准库,而标准库会用到很多操作系统的功能,诸如线程,文件,网络等。所以我们要做的第一步就是实现一个不依赖于任何操作系统功能的Rust程序(裸机程序)。
禁用标准库
我们可以通过![no_std]来禁用std库。
1 |
|
运行cargo build,可以看到如下错误:
1 | error: cannot find macro `println` in this scope |
println! 宏
println!宏依赖于std库,这里我们就先不使用它了。
panic 处理函数
第二个错误说需要一个#[panic_handler]函数,这个函数会在程序panic的时候被调用。默认情况下,std中有panic的实现,然而由于我们是在[no_std]环境中,只能自己实现一个panic函数了。
1 | use core::panic::PanicInfo; |
这里我们用了core库,这个库不需要操作系统支持。PanicInfo类型的参数会包含panic发生的文件,代码行数等错误信息。另外!标记表示这个函数的返回类型为never type,即永远不会返回。
eh_personality 语义项
语义项是编译器内部所需的特殊函数或类型,例如Copy trait(#[lang = "copy"]),又或是之前的panic_handler。eh_personality是用来标记函数实现堆栈展开的语义项,该语义与panic有关。
堆栈展开 (Stack Unwinding)
通常当程序出现了异常时,从异常点开始会沿着 caller 调用栈一层一层回溯,直到找到某个函数能够捕获这个异常或终止程序。这个过程称为堆栈展开。
当程序出现异常时,我们需要沿着调用栈一层层回溯上去回收每个 caller 中定义的局部变量(这里的回收包括 C++ 的 RAII 的析构以及 Rust 的 drop 等)避免造成捕获异常并恢复后的内存溢出。
而在 Rust 中,panic 证明程序出现了错误,我们则会对于每个 caller 函数调用依次这个被标记为堆栈展开处理函数的函数进行清理。
这个处理函数是一个依赖于操作系统的复杂过程,在标准库中实现。但是我们禁用了标准库使得编译器找不到该过程的实现函数了。
为了简单起见,我们将堆栈展开禁用,在panic发生时直接abort而不是依次获取堆栈信息。
在Cargo.toml中进行配置:
1 | [profile.dev] |
现在,错误信息变成了:
1 | error: requires `start` lang_item |
移除C运行时依赖
大部分语言都有一个运行时(Runtime),这个运行时会在main函数之前被调用。以Rust为例,一个典型的链接了标准库的Rust程序会先跳转到C语言运行时环境crt0(C runtime zero),crt0会接着跳转到Rust运行时的入口点,这个入口点是被start语义所标记的。最后,Rust的运行时会调用main函数。
由于我们的程序无法访问标准库也就无法访问crt0和Rust运行时,所以我们需要定义我们自己的入口点。这里即使覆写start语义也是没用的,因为它仍然需要crt0的支持,所以我们要做的是直接覆写整个ctr0入口点。
1 |
|
我们使用#![no_main]属性来告诉编译器我们不使用常规入口点,并使用_start函数作为新的入口点(_start是大部分系统的默认入口点)。这里我们使用#[no_mangle]标记来禁用编译时的重命名,保证编译器生成的函数名仍然为_start,并使用extern "C"表示这是一个C语言函数。
此时我们再编译,发现错误变成了链接错误。
解决链接错误
链接器(Linker)是一个程序,它将生成的目标文件组合为一个可执行文件。不同的操作系统如 Windows、macOS 或 Linux,规定了不同的可执行文件格式,因此也各有自己的链接器,抛出不同的错误;但这些错误的根本原因还是相同的:链接器的默认配置假定程序依赖于 C 语言的运行时环境,但我们的程序并不依赖于它。以x86-64-linux为例,错误如下:
1 | error: linking with `cc` failed: exit code: 1 |
我们只需要使用-C link-argflag(cargo rustc -- -C link-arg=-nostartfiles)或是干脆选择编译为裸机目标(例如:cargo build --target thumbv7em-none-eabihf)即可。