一般我们使用 Rust 编写的代码已经由 Rust 工具链帮我们构建好了代码所需要的操作系统下的运行环境,但在编写裸机代码的话,由于没有操作系统支持,按照我们平时的写法是行不通的,这里将简单介绍如何使用 Rust 编写 Risc-V 裸机代码。

环境与工具链

我们要编译成的目标平台是 Risc-V,但是我们编写的代码的平台可能不是,也许是 x84、ARM等,像这种编译成其他平台叫做交叉编译。在这之前,我们需要安装一些组件。

target

使用命令

rustup target list

能够查看 Rust 支持的目标平台,你能够看到输出的列表,以我们要编译的 Risc-V 64 位目标平台为例:riscv64gc-unknown-none-elf,这种格式被一个横杠分隔成四个部分(一般来说是三个部分,架构和厂家是一个部分,但是为什么三个部分被分成了四个?):

  • 架构
  • 厂商
  • 操作系统
  • 环境

以上面为例就是:架构为 riscv64gc(Risc-V 64 位,g 代表通用指令集扩展,支持整数、浮点、乘除法等功能,c 代表精简指令),厂家无,裸机环境,生成的文件为 ELF。

确定好目标平台后,使用命令安装:

rustup target add riscv64gc-unknown-none-elf

component

我们继续安装其他所需要的组件:

rustup component add llvm-tools-preview

链接器

如果说在我们的裸机代码中需要链接一些在链接器中定义的符号,就需要指定我们的链接器脚本。

在项目根目录下创建文件 .cargo/config.toml,输入以下内容:

# .cargo/config.toml

[build]
target = "riscv64gc-unknown-none-elf"

[target.riscv64gc-unknown-none-elf]
rustflags = ["-Clink-arg=-Tsrc/linker.ld", "-Cforce-frame-pointers=yes"]

其中 [build] 中指定了编译的目标平台,[target.riscv64gc-unknown-none-elf] 中的 rustflags 指定了链接器脚本: src/linker.ld

如果你不需要的话这个可以忽略。

构建代码

环境准备好了,但是在正式编写我们的第一句代码之前还要写一些代码。

no_std

Rust 为我们提供了 std 标准库,但是在裸机环境下无法使用标准库,如果此时编译的话编译器抱怨:can't find crate for "std",所以需要告诉编译器我们不使用标准库。

在 main 文件顶部写:

#![no_std]

此时就不会出现找不到标准库的错误了,但同时我们也无法使用任何标准库提供的功能,比如 println!

no_main

在裸机中 main 函数不在成为我们编写代码的入口,因为在 main 函数执行之前实际上是有一个 _start 函数作为程序真正的入口用于初始化等工作,这一步编译器和操作系统帮我们做完了,但在裸机中没有操作系统,所以我们实际上会直接在 _start 这个真正的入口里编写代码。

所以我们要告诉编译器不再使用 main 函数作为入口,并提供一个 _start 函数作为入口,在 main 文件里写:

// src/main.rs

#![no_main]

#[no_mangle]
pub extern "C" fn _start() -> ! {
	loop {}
}

#![no_main] 告诉编译器不使用 main 函数作为入口。然后我们提供了一个 _start 函数,这个函数名是大多数系统的约定,会将这个符号作为入口。我们在 _start 函数上指定了 宏 #[no_mangle],用于告诉编译器不要对它命名重整,这样 _start 函数编译出来的符号就是 _start,如果没有这个宏, _start 函数编译出来的符号会大变样,而找不到 _start 符号作为入口。

panic_handler

Rust 编译器会要你提供一个错误处理函数,这个函数会在 panic 的时候被使用,我们直接在 main 文件下写:

// src/main.rs

use core::panic::PanicInfo;

#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
    loop {}
}

到这里,我们的代码应能编译通过了,你可以在 _start 里编写你的 Risc-V 的裸机代码了。