嵌入式宝典

嵌入式宝典将会带领你从零创造一个 #![no_std] 应用,经历为Coterx-M微控制器搭建特定于架构的功能的开发迭代过程。

目的

通过阅读这本书你将会学到

  • 搭建一个 #![no_std] 应用。这比搭建一个 #![no_std] 库更复杂,因为目标平台可能没有运行操作系统(或者你的目标就是搭建一个操作系统!),而且你的程序可能是目标平台中运行的唯一一个进程(或者第一个进程)。在这种情况下,程序可能需要为目标平台进行定制。

  • 精细地控制一个Rust程序的存储布局的技巧。你将学到链接器(linkers),链接器脚本和可以用来控制Rust程序的某些ABI的 Rust features 。

  • 如何实现可以被静态重载(没有运行时消耗)的默认函数。

目标读者

这本书主要面向两个读者:

  • 希望为生态系统还没有支持的架构提供板级支持(比如,自Rust 1.28以来的Cortex-R),或者为一个刚获得Rust支持的架构提供帮助(比如 未来可能有Xtensa)的人

  • 对形如 cortex-m-rtmsp430-rtriscv-rt 这样的 runtime 库的不寻常的实现感到好奇的人。

翻译

这本书已经被慷慨的志愿者们翻译了。如果你想把你的翻译列在这里,请打开一个PR添加它。

要求

这本书是自洽的。读者不需要熟悉Cortex-M架构,也不需要一个Cortex-M微控制器 -- 这本书里包含 的所有例子都能在QEMU中测试。然而,需要安装下面的工具来运行和检查这本书中的示例:

  • 这本书中所有代码使用的是2018版的Rust。如果你不熟悉2018的特性和术语,阅读 edition guide

  • Rust 1.31 或者更新的具有ARM Cortex-M编译支持的工具链。

  • cargo-binutils. v0.1.4 或者更新的版本。

  • cargo-edit.

  • 有ARM仿真支持的QEMU。qemu-system-arm 程序必须被安装在你的电脑上。

  • 有ARM支持的GDB 。

安装示例

所有操作系统通用的指令

$ # Rust 工具链
$ # 如果你是从零开始,从 https://rustup.rs/ 获取rustup
$ rustup default stable

$ # 工具链应该比这个更新
$ rustc -V
rustc 1.31.0 (abe02cefd 2018-12-04)

$ rustup target add thumbv7m-none-eabi

$ # cargo-binutils
$ cargo install cargo-binutils

$ rustup component add llvm-tools-preview

macOS

$ # arm-none-eabi-gdb
$ # 你可能需要先运行 `brew tap Caskroom/tap`
$ brew install --cask gcc-arm-embedded

$ # QEMU
$ brew install qemu

Ubuntu 16.04

$ # arm-none-eabi-gdb
$ sudo apt install gdb-arm-none-eabi

$ # QEMU
$ sudo apt install qemu-system-arm

Ubuntu 18.04 或者 Debian

$ # gdb-multiarch -- 当你希望启动gdb时,使用 `gdb-multiarch`
$ sudo apt install gdb-multiarch

$ # QEMU
$ sudo apt install qemu-system-arm

Windows

从ARM安装一个工具链(可选的步骤) (在Ubuntu 18.04上测试过)

$ tar xvjf gcc-arm-none-eabi-8-2018-q4-major-linux.tar.bz2
$ mv gcc-arm-none-eabi-<version_downloaded> <your_desired_path> # 可选
$ export PATH=${PATH}:<path_to_arm_none_eabi_folder>/bin # 把这行添加到 .bashrc 使它永久有效

最简的 #![no_std] 程序

在这部分,我们将写一个可以编译的最简的 #![no_std] 程序。

#![no_std] 是什么意思?

#![no_std] 是一个crate层级的属性,其指出crate将链接到 core 而不是 std crate,然而这对应用来说意味着什么呢?

std crate 是Rust的标准库。它包含的功能需要假设程序将运行在一个操作系统上而不是[直接运行在裸机]上。std 也假设操作系统是一个通用的操作系统,像那些会在服务器和桌面看到的系统。如此,std 在那些经常能在操作系统中遇到的功能:线程,文件,套接字,一个文件系统,进程,等等之上,提供了一个标准的API 。

换句话说,core crate是std crate的一个子集,其不对将运行程序的系统做任何假设。它提供与语言的基本类型,像是浮点数,字符串和切片有关的APIs,也提供像是原子操作和SIMD指令这样的与处理器相关的APIs 。然而它缺少涉及到堆内存分配和I/O有关的APIs。

对于一个应用来说,std 不仅仅只是提供一种方法访问OS抽象。std 在某些情况下,也提供栈溢出保护,处理命令行参数,在一个程序的main函数被启动前打开主线程。一个 #![no_std] 应用缺少上述的所有运行时,因此应用必须在需要的时候初始化它自己的运行时。

由于这些特点,一个 #![no_std] 应用可以成为第一个或者是唯一一个运行在一个系统上的代码。它可以成为许多标准Rust应用无法成为的东西,比如:

  • 一个操作系统的内核。
  • 固件。
  • 一个启动引导。

代码

讲完了,我们可以转向最小的 #![no_std] 程序了:

$ cargo new --edition 2018 --bin app

$ cd app
$ # 把 main.rs 改成这些内容
$ cat src/main.rs
#![allow(unused)]
#![no_main]
#![no_std]

fn main() {
use core::panic::PanicInfo;

#[panic_handler]
fn panic(_panic: &PanicInfo<'_>) -> ! {
    loop {}
}
}

这个程序含有一些在标准的Rust程序中不会出现的东西:

#![no_std] 属性,我们已经讲过了。

#![no_main] 属性,它意味着程序将不会使用标准的 main 函数作为入口。在写这本书的时候,Rust的 main 接口对程序执行的环境做了一些假设: 比如,它假设存在命令行参数,因此,通常它不适合 #![no_std] 程序。

#[panic_handler] 属性。用这个属性标记的函数定义了恐慌的行为,包括库层级的恐慌(core::painc!)和语言层级的恐慌(越界索引)。

这个程序不产生任何有用的东西。事实上,它将产生一个空的二进制项。

$ # 等于 `size target/thumbv7m-none-eabi/debug/app`
$ cargo size --target thumbv7m-none-eabi --bin app
   text	   data	    bss	    dec	    hex	filename
      0	      0	      0	      0	      0	app

在链接之前,crate 包含了恐慌函数的符号。

$ cargo rustc --target thumbv7m-none-eabi -- --emit=obj

$ cargo nm -- target/thumbv7m-none-eabi/debug/deps/app-*.o | grep '[0-9]* [^N] '
00000000 T rust_begin_unwind

不过,它是我们的起点。在下一个部分,我们将搭建一些有用的东西。但是在继续之前,让我们设置一个默认的编译目标避免每次调用Cargo不得不传递--target标志。

$ mkdir .cargo

$ # 把 .cargo/config 改成这些内容
$ cat .cargo/config
[build]
target = "thumbv7m-none-eabi"

eh_personality

如果你配置的不是在恐慌时无条件终止(译者注:panic = "abort"),大多数的有完整的操作系统的目标平台都不是(或者如果你的 客制目标平台 不包含 "panic-strategy": "abort"),那么你必须告诉Cargo要怎么做或者添加一个 eh_personality 函数,后者需要nightly版的编译器。这里是关于它的Rust文档这里是一些关于它的讨论.

在 Cargo.toml 中, 添加:

[profile.dev]
panic = "abort"

[profile.release]
panic = "abort"

或者,声明 eh_personality 函数。它很简单的没啥特别的,展开时和下面一样:

#![allow(unused)]
#![feature(lang_items)]

fn main() {
#[lang = "eh_personality"]
extern "C" fn eh_personality() {}
}

如果没有这么做,你将会收到这个错误 language item required, but not found: 'eh_personality'

存储布局

下一步是确保程序有正确的存储布局,目标平台才可以执行它。在我们的例子里,将使用一个虚拟的Cortex-M3微控制器:LM3S6965 。我们的程序是运行在设备上的唯一一个进程,因此它必须要负责初始化设备。

背景信息

Cortex-M 设备需要把向量表放在代码区的开始处。 向量表是一组指针;启动设备需要前两个指针,剩下来的指针都与异常有关。我们现在忽略它们。

链接器决定程序的最终存储布局,但是我们可以用链接器脚本对它进行一些控制。链接器提供的控制布局的精细度在sections层级。一个section是存储在连续的内存地址中的符号的集合。符号,依次,可以是数据(一个静态变量),或者指令(一个Rust函数)。

每一个符号有一个由编译器分配的名字。自Rust 1.28以来,Rust编译器分配给符号的名字都是这样的格式: _ZN5krate6module8function17he1dfc17c86fe16daE,其展开是 krate::module::function::he1dfc17c86fe16da 其中 krate::module::function 是函数或者变量的路径,he1dfc17c86fe16da 是某个哈希值。Rust编译器将把每个符号放进它自己的独有的section中;比如之前提到的符号将被放进一个名为 .text._ZN5krate6module8function17he1dfc17c86fe16daE 的section中。

这些编译器产生的符号和section名在不同的Rust编译器发布版中不保证是不变的。然而,语言让我们可以使用这些attributes控制符号名和放置的section:

  • #[export_name = "foo"] 把符号名设置成 foo
  • #[no_mangle] 意思是: 使用函数或者变量名(不是它的全路径)作为它的符号名。 #[no_mangle] fn bar() 将产生一个名为 bar 的符号。
  • #[link_section = ".bar"] 把符号放进一个名为 .bar 的section中。

有了这些attributes,我们可以稳定一个程序的ABI,并在链接器脚本中使用它。

Rust 部分

像上面提到的,对于Cortex-M设备,我们需要修改向量表的前两项。第一个,栈指针的初始值,只能使用链接器脚本修改。第二个,重置向量,需要在Rust代码中生成并使用链接器脚本放置到正确的地方。

重置向量是一个指向重置处理函数的指针。重置处理函数是在一个系统重启后,或者第一次上电后,设备将会执行的函数。重置处理函数总是硬件调用栈中的第一个栈帧;从它返回是未定义的行为,因为没有其它栈帧可以给它返回。通过让它变成一个divergent function,我们可以强调这个重置处理函数从来不会返回,divergent function的签名是 fn(/* .. */) -> !

#![allow(unused)]
fn main() {
#[no_mangle]
pub unsafe extern "C" fn Reset() -> ! {
    let _x = 42;

    // can't return so we go into an infinite loop here
    loop {}
}

// The reset vector, a pointer into the reset handler
#[link_section = ".vector_table.reset_vector"]
#[no_mangle]
pub static RESET_VECTOR: unsafe extern "C" fn() -> ! = Reset;

}

我们通过使用 extern "C" 告诉编译器下面的函数使用C的ABI,而不是Rust的ABI,后者不稳定,将函数变成硬件期望的格式。

为了从链接器脚本中指出重置处理函数和重置向量,我们需要它们有一个稳定的符号名因此我们使用 #[no_mangle] 。我们需要稍微控制下 RESET_VECTOR 的位置,因此我们将它放进一个已知的section中,.vector_table.reset_vector 。重置处理函数,Reset ,的确切位置不重要。我们只使用编译器默认生成的section 。

当遍历输入目标文件的列表时,这个链接器将忽略是内部链接的符号(也叫内部符号),因此我们需要让我们的符号变成外部链接。在Rust中让一个符号变成外部的方法,只有使它的相关项变成公共的(pub)和可到达的(在项和crate的根路径间没有私有模块)。

链接器脚本部分

一个最小的,将向量表放进正确的位置的链接器脚本如下所示。让我们仔细看看。

$ cat link.x
/* Memory layout of the LM3S6965 microcontroller */
/* 1K = 1 KiBi = 1024 bytes */
MEMORY
{
  FLASH : ORIGIN = 0x00000000, LENGTH = 256K
  RAM : ORIGIN = 0x20000000, LENGTH = 64K
}

/* The entry point is the reset handler */
ENTRY(Reset);

EXTERN(RESET_VECTOR);

SECTIONS
{
  .vector_table ORIGIN(FLASH) :
  {
    /* First entry: initial Stack Pointer value */
    LONG(ORIGIN(RAM) + LENGTH(RAM));

    /* Second entry: reset vector */
    KEEP(*(.vector_table.reset_vector));
  } > FLASH

  .text :
  {
    *(.text .text.*);
  } > FLASH

  /DISCARD/ :
  {
    *(.ARM.exidx .ARM.exidx.*);
  }
}

MEMORY

链接器脚本的这个部分描述了目标中存储区块的大小和位置。有两个存储区块被定义了: FLASHRAM ;它们都与目标中可用的物理存储有关。这里用的值与LM3S6965微控制器有关。

ENTRY

这里我们指给链接器,符号名为 Reset 的重置处理函数是程序的 entry point 。链接器会主动丢弃未使用的部分。链接器认为entry point和从它处被调用的函数是被使用了的,所以链接器将不会丢弃它们。没有这一行,链接器将会丢弃 Reset 函数和所有它接下来会调用的函数。

EXTERN

链接器是懒惰的;一旦它们找到了从entry point处递归引用的所有的符号,它们将停止查看输入目标文件。EXTERN 强迫链接器去寻找 EXTERN 的参数,即使其它所有的被引用的符号都被找到后。因为thumb的一个规则,如果你想要一个符号,其没有被entry point调用,总是出现在输出的二进制项中,你应该使用结合了 KEEPEXTERN

SECTIONS

这部分描述了输入目标文件中的sections(也被称为input sections)要如何被安排进输出目标文件的sections(也被称为output sections)中的或者它们是否应该被丢弃。这里,我们定义两个输出sections:

  .vector_table ORIGIN(FLASH) : { /* .. */ } > FLASH

.vector_table 包含向量表且坐落于 FLASH 存储的开始处。

  .text : { /* .. */ } > FLASH

.text 包含程序的子程序且坐落于 FLASH 的某些位置。它的开始地址没有指定,但是链接器将把它放在先前的输出section,.vector_table 之后。

输出 .vector_table section包含:

    /* First entry: initial Stack Pointer value */
    LONG(ORIGIN(RAM) + LENGTH(RAM));

我们将把(调用)栈放在RAM的末尾(栈是完全递减的;它向着更小的地址增长)因此RAM的末尾地址将被用作栈指针(SP)值。链接器使用我们输入的RAM存储区块的信息,可以算出那个地址。

    /* Second entry: reset vector */
    KEEP(*(.vector_table.reset_vector));

接下来,我们使用 KEEP 去强迫链接器在初始的SP值之后插入所有的名为 .vector_table.reset_vector 的input sections 。位于那个section中的唯一一个符号是 RESET_VECTOR,所以这将可以把 RESEET_VECTOR 放在向量表的第二个位置。

输出的 .text section 包含:

    *(.text .text.*);

这包括所有的名为 .text.text.* 的input sections 。注意我们这里没有使用 KEEP 去让链接器丢弃不使用的部分。

最后,我们使用特殊的 /DISCARD/ section 去丢弃

    *(.ARM.exidx .ARM.exidx.*);

名为 .ARM.exidx.* 的input sections 。这些sections与异常处理有关,但是我们在恐慌时不进行栈展开,它们占用Flash的存储空间,因此我们就丢弃它们。

全放到一起去

现在我们能链接应用了。作为参考,这里是完整的Rust程序:

#![allow(unused)]
#![no_main]
#![no_std]

fn main() {
use core::panic::PanicInfo;

// The reset handler
#[no_mangle]
pub unsafe extern "C" fn Reset() -> ! {
    let _x = 42;

    // can't return so we go into an infinite loop here
    loop {}
}

// The reset vector, a pointer into the reset handler
#[link_section = ".vector_table.reset_vector"]
#[no_mangle]
pub static RESET_VECTOR: unsafe extern "C" fn() -> ! = Reset;

#[panic_handler]
fn panic(_panic: &PanicInfo<'_>) -> ! {
    loop {}
}
}

我们不得不修改链接的过程让它使用我们的链接器脚本。通过传递 -C link-arg 标志给 rustc 来完成它。使用 cargo-rustc 或者 cargo-build 就可以完成了。

重要: 在运行这个命令之前确保你有 .cargo/config 文件,其在上个章节末尾处被添加了。

使用 cargo-rustc 子命令:

$ cargo rustc -- -C link-arg=-Tlink.x

或者你可以在 .cargo/config 中设置rustflags,并继续使用 cargo-build 子命令。我们将会使用后者因为它与cargo-binutils集成的更好。

# 将 .cargo/config 修改成这些内容
$ cat .cargo/config
[target.thumbv7m-none-eabi]
rustflags = ["-C", "link-arg=-Tlink.x"]

[build]
target = "thumbv7m-none-eabi"

[target.thumbv7m-none-eabi] 部分告知这些标志只会被用于当交叉编译的目标是thumbv7m-none-eabi时。

检查它

现在让我们检查下输出的二进制项,确保存储布局跟我们想要的一样 (这需要 cargo-binutils):

$ cargo objdump --bin app -- -d --no-show-raw-insn

app:	file format elf32-littlearm

Disassembly of section .text:

<Reset>:
               	sub	sp, #0x4
               	movs	r0, #0x2a
               	str	r0, [sp]
               	b	0x10 <Reset+0x8>        @ imm = #-0x2
               	b	0x10 <Reset+0x8>        @ imm = #-0x4

这是 .text section的反汇编。我们看到重置处理函数,名为 Reset,位于 0x8 地址。

$ cargo objdump --bin app -- -s --section .vector_table

app:	file format elf32-littlearm
Contents of section .vector_table:
 0000 00000120 09000000                    ... ....

这是 .vector_table section 的内容。我们可以看到section开始于地址 0x0 且 section 的第一个字是 0x2001_0000 (objdump 的输出是小端模式)。这是初始的SP值,它与RAM的末尾地址匹配。第二个字是 0x9;这是重置处理函数的 thumb mode 的地址。当一个函数运行在thumb mode下,它的地址的第一位被设置成1 。

测试它

这个程序是一个有效的LM3S6965程序;我们可以在一个虚拟微控制器(QEMU)中执行它去测试。

$ # 这个程序将会阻塞住
$ qemu-system-arm \
      -cpu cortex-m3 \
      -machine lm3s6965evb \
      -gdb tcp::3333 \
      -S \
      -nographic \
      -kernel target/thumbv7m-none-eabi/debug/app
$ # 在一个不同的终端上
$ arm-none-eabi-gdb -q target/thumbv7m-none-eabi/debug/app
Reading symbols from target/thumbv7m-none-eabi/debug/app...done.

(gdb) target remote :3333
Remote debugging using :3333
Reset () at src/main.rs:8
8       pub unsafe extern "C" fn Reset() -> ! {

(gdb) # the SP has the initial value we programmed in the vector table
(gdb) print/x $sp
$1 = 0x20010000

(gdb) step
9           let _x = 42;

(gdb) step
12          loop {}

(gdb) # next we inspect the stack variable `_x`
(gdb) print _x
$2 = 42

(gdb) print &_x
$3 = (i32 *) 0x2000fffc

(gdb) quit

一个 main 接口

我们现在有了一个可以工作的最简的程序了,但是我们需要用一种方式将它打包起来,让终端用户可以在其上搭建安全的程序。在这部分,我们将实现一个 main 接口,它用起来就像我们在一个标准的Rust程序中所用的那样。

首先,我们将把我们的binary crate转换成一个library crate:

$ mv src/main.rs src/lib.rs

然后把它重命名为 rt,其表示"runtime" 。

$ sed -i s/app/rt/ Cargo.toml

$ head -n4 Cargo.toml
[package]
edition = "2018"
name = "rt" # <-
version = "0.1.0"

第一个改变是让重置处理函数调用一个外部的 main 函数:

$ head -n13 src/lib.rs
#![no_std]

use core::panic::PanicInfo;

// CHANGED!
#[no_mangle]
pub unsafe extern "C" fn Reset() -> ! {
    extern "Rust" {
        fn main() -> !;
    }

    main()
}

我们也去掉了 #![no_main] attribute,因为它对library crates没有影响。

到了这里出现了一个正交问题:rt库应该提供一个标准的恐慌时行为吗, 或者它不应该提供一个 #[panic_handler] 函数而让终端用户去选 择恐慌时行为吗?这个文档将不会深入这个问题,而且为了方便,在rt crate 中留了一个空的 #[panic_handler] 函数。然而,我们想告诉读者还存在其它选择。

第二个改变涉及到给应用crate提供我们之前写的链接器脚本。链接器将会在库搜索路径(-L)和调用它的文件夹中寻找链接器脚本。应用crate不应该需要将link.x的副本挪来挪去所以我们将使用一个build scriptrt crate 将链接器脚本放到库搜索路径中。

$ # 在`rt`的根目录中生成一个带有这些内容的 build.rs 文件
$ cat build.rs
use std::{env, error::Error, fs::File, io::Write, path::PathBuf};

fn main() -> Result<(), Box<dyn Error>> {
    // build directory for this crate
    let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());

    // extend the library search path
    println!("cargo:rustc-link-search={}", out_dir.display());

    // put `link.x` in the build directory
    File::create(out_dir.join("link.x"))?.write_all(include_bytes!("link.x"))?;

    Ok(())
}

现在用户可以写一个暴露了main符号的应用了,且将它链接到rt crate上。rt 将负责给予程序正确的存储布局。

$ cd ..

$ cargo new --edition 2018 --bin app

$ cd app

$ # 修改Cargo.toml将`rt` crate包含进来作为一个依赖
$ tail -n2 Cargo.toml
[dependencies]
rt = { path = "../rt" }
$ # 拷贝整个config文件,它设置了一个默认的目标并修改了链接器命令
$ cp -r ../rt/.cargo .

$ # 把 `main.rs` 的内容改成
$ cat src/main.rs
#![no_std]
#![no_main]

extern crate rt;

#[no_mangle]
pub fn main() -> ! {
    let _x = 42;

    loop {}
}

反汇编的结果将是相似的,除了现在包含了用户的main函数。

$ cargo objdump --bin app -- -d --no-show-raw-insn

app:	file format elf32-littlearm

Disassembly of section .text:

<main>:
               	sub	sp, #0x4
               	movs	r0, #0x2a
               	str	r0, [sp]
               	b	0x10 <main+0x8>         @ imm = #-0x2
               	b	0x10 <main+0x8>         @ imm = #-0x4

<Reset>:
               	push	{r7, lr}
               	mov	r7, sp
               	bl	0x8 <main>              @ imm = #-0x12
               	trap

把它变成类型安全的

main 接口工作了,但是它容易出错。比如,用户可以把main写成一个non-divergent function,它们将不会带来编译时错误,但会带来未定义的行为(编译器将会错误优化这个程序)。

我们通过暴露一个宏给用户而不是符号接口可以添加类型安全性。在 rt crate 中,我们可以写这个宏:

$ tail -n12 ../rt/src/lib.rs
#![allow(unused)]
fn main() {
#[macro_export]
macro_rules! entry {
    ($path:path) => {
        #[export_name = "main"]
        pub unsafe fn __main() -> ! {
            // type check the given path
            let f: fn() -> ! = $path;

            f()
        }
    }
}
}

然后应用的作者可以像这样调用它:

$ cat src/main.rs
#![no_std]
#![no_main]

use rt::entry;

entry!(main);

fn main() -> ! {
    let _x = 42;

    loop {}
}

如果作者把main的签名改成non divergent function,比如 fn ,将会出现一个错误。

main之前的工作

rt 看起来不错了,但是它的功能不够完整!用它编写的应用不能使用 static 变量或者字符串字面值,因为 rt 的链接器脚本没有定义标准的.bss.data.rodata sections 。让我们修复它!

第一步是在链接器脚本中定义这些sections:

$ # 只展示文件的一小块
$ sed -n 25,46p ../rt/link.x
  .text :
  {
    *(.text .text.*);
  } > FLASH

  /* NEW! */
  .rodata :
  {
    *(.rodata .rodata.*);
  } > FLASH

  .bss :
  {
    *(.bss .bss.*);
  } > RAM

  .data :
  {
    *(.data .data.*);
  } > RAM

  /DISCARD/ :

它们只是重新导出input sections并指定每个output section将会进入哪个内存区域。

有了这些改变,下面的程序可以编译:

#![no_std]
#![no_main]

use rt::entry;

entry!(main);

static RODATA: &[u8] = b"Hello, world!";
static mut BSS: u8 = 0;
static mut DATA: u16 = 1;

fn main() -> ! {
    let _x = RODATA;
    let _y = unsafe { &BSS };
    let _z = unsafe { &DATA };

    loop {}
}

然而如果你在真正的硬件上运行这个程序并调试它,你将发现到达main时,static 变量 BSSDATA 没有 01 值。反而,这些变量将是垃圾值。问题是在设备上电之后,RAM的内容是随机的。如果你在QEMU中运行这个程序,你将看不到这个影响。

在目前的情况下,如果你的程序在对static变量执行一个写入之前,读取任何 static 变量,那么你的程序会出现未定义的行为。让我们通过在调用main之前初始化所有的static变量来修复它。

我们需要修改下链接器脚本去进行RAM初始化:

$ # 只展示文件的一块
$ sed -n 25,52p ../rt/link.x
  .text :
  {
    *(.text .text.*);
  } > FLASH

  /* CHANGED! */
  .rodata :
  {
    *(.rodata .rodata.*);
  } > FLASH

  .bss :
  {
    _sbss = .;
    *(.bss .bss.*);
    _ebss = .;
  } > RAM

  .data : AT(ADDR(.rodata) + SIZEOF(.rodata))
  {
    _sdata = .;
    *(.data .data.*);
    _edata = .;
  } > RAM

  _sidata = LOADADDR(.data);

  /DISCARD/ :

让我们深入下细节:

    _sbss = .;
    _ebss = .;
    _sdata = .;
    _edata = .;

我们将符号关联到.bss.data sections的开始和末尾地址,我之后将会从Rust代码中使用。

  .data : AT(ADDR(.rodata) + SIZEOF(.rodata))

我们将.data section的加载内存地址(LMA)设置成.rodata section的末尾处。.data 包含非零值的static变量;.data section的虚拟内存地址(VMA)在RAM中的某处 -- 这是static变量所在的地方。这些static变量的初始值,然而,必须被分配在非易失存储中(Flash);LMA是Flash中存放这些初始值的地方。

  _sidata = LOADADDR(.data);

最后,我们将一个符号和.data的LMA关联起来。我们可以从Rust代码引用我们在链接器脚本中生成的符号。这些符号的地址1指向 .bss.data sections的边界处。

下面展示的是更新了的重置处理函数:

$ head -n32 ../rt/src/lib.rs
#![no_std]

use core::panic::PanicInfo;
use core::ptr;

#[no_mangle]
pub unsafe extern "C" fn Reset() -> ! {
    // NEW!
    // Initialize RAM
    extern "C" {
        static mut _sbss: u8;
        static mut _ebss: u8;

        static mut _sdata: u8;
        static mut _edata: u8;
        static _sidata: u8;
    }

    let count = &_ebss as *const u8 as usize - &_sbss as *const u8 as usize;
    ptr::write_bytes(&mut _sbss as *mut u8, 0, count);

    let count = &_edata as *const u8 as usize - &_sdata as *const u8 as usize;
    ptr::copy_nonoverlapping(&_sidata as *const u8, &mut _sdata as *mut u8, count);

    // Call user entry point
    extern "Rust" {
        fn main() -> !;
    }

    main()
}

现在终端用户可以直接地和间接地使用static变量而不会导致未定义的行为了!

我们上面展示的代码中,内存的初始化按照一种逐字节的方式进行。可以强迫 .bss.data section对齐,比如,四个字节。然后Rust代码可以利用这个事实去执行逐字的初始化而不需要对齐检查。如果你想知道这是如何做到的,看下 cortex-m-rt crate 。

1

必须在这使用链接器脚本符号这件事会让人疑惑且反直觉。可以在这里找到与这个怪现象有关的详尽解释。

异常处理

在"存储布局"部分,我们刚开始的时候决定说得简单点,所以忽略了对异常的处理。在这部分,我们将添加对异常处理的支持;这将作为一个如何在稳定版的Rust中(比如 不依赖不稳定的 #[linkage = "weak"] 属性,它让一个符号变成弱链接)实现编译时可重载行为的案例。

背景信息

简而言之,异常是Cortex-M和其它架构为了让应用可以响应异步的,通常是外部的,事件,所提供的一种机制。经典的(硬件)中断是大多数人所知道的最常见的异常类型。

Cortex-M异常机制工作起来像下面一样: 当处理器接收到与某个异常的类型相关的信号或者事件,它挂起现在的子程序的执行(通过将状态存进调用栈中)然后在一个新的栈帧中继续执行相关的异常处理函数,也就是另一个子程序。在异常处理函数执行完成之后(比如 从它返回后),处理器恢复被挂起的子程序的执行。

处理器使用向量表去确定执行哪个处理函数。表中的每一项包含一个指向一个处理函数的指针,每一项关联的异常类型都不一样。比如,第二项是重置处理函数,第三项是NMI(不可屏蔽中断)处理函数,等等。

像之前提到的,处理器希望向量表在存储中的某个特定的位置,在运行时处理器可能用到表中的每一项。因此,表中的各项必须包含有效的值。此外,我们希望rt crate是灵活的,因此终端用户可以定制每个异常处理函数的行为。最后,向量表要坐落在只读存储中,或者在不容易被修改的存储中,因此用户必须静态地注册处理函数,而不是在运行时。

为了满足所有的这些需求,我们将给rt crate中的向量表所有的项分配一个默认值,但是让这些值变以让终端用户可以在编译时重载它们。

Rust部分

让我们看下所有的这些要如何被实现。为了方便,我们将只使用向量表的前16个项;这些项不是特定于设备的,所以它们在所有类型的Cotex-M微控制器上都有相同的作用。

我们做的第一件事是在rt create的代码中创造一个向量(指向异常处理函数的指针)数组:

$ sed -n 56,91p ../rt/src/lib.rs
#![allow(unused)]
fn main() {
pub union Vector {
    reserved: u32,
    handler: unsafe extern "C" fn(),
}

extern "C" {
    fn NMI();
    fn HardFault();
    fn MemManage();
    fn BusFault();
    fn UsageFault();
    fn SVCall();
    fn PendSV();
    fn SysTick();
}

#[link_section = ".vector_table.exceptions"]
#[no_mangle]
pub static EXCEPTIONS: [Vector; 14] = [
    Vector { handler: NMI },
    Vector { handler: HardFault },
    Vector { handler: MemManage },
    Vector { handler: BusFault },
    Vector {
        handler: UsageFault,
    },
    Vector { reserved: 0 },
    Vector { reserved: 0 },
    Vector { reserved: 0 },
    Vector { reserved: 0 },
    Vector { handler: SVCall },
    Vector { reserved: 0 },
    Vector { reserved: 0 },
    Vector { handler: PendSV },
    Vector { handler: SysTick },
];
}

向量表中的一些项是保留的;ARM文档说它们应该被分配成 0 值,所以我们使用一个联合体来完成它。需要指向一个处理函数的项必须使用external函数;这很重要,因为它可以让终端用户来提供实际的函数定义。

接下来,我们在Rust代码中定义一个默认的异常处理函数。没有被终端用户分配的异常将使用这个默认处理函数。

$ tail -n4 ../rt/src/lib.rs
#![allow(unused)]
fn main() {
#[no_mangle]
pub extern "C" fn DefaultExceptionHandler() {
    loop {}
}
}

链接器脚本部分

在链接器脚本那部分,我们将这些新的异常向量放在重置向量之后。

$ sed -n 12,25p ../rt/link.x
EXTERN(RESET_VECTOR);
EXTERN(EXCEPTIONS); /* <- NEW */

SECTIONS
{
  .vector_table ORIGIN(FLASH) :
  {
    /* First entry: initial Stack Pointer value */
    LONG(ORIGIN(RAM) + LENGTH(RAM));

    /* Second entry: reset vector */
    KEEP(*(.vector_table.reset_vector));

    /* The next 14 entries are exception vectors */
    KEEP(*(.vector_table.exceptions)); /* <- NEW */
  } > FLASH

并且我们使用 PROVIDE 给我们在rt中未定义的处理函数赋予一个默认值 (NMI和上面的其它处理函数):

$ tail -n8 ../rt/link.x
PROVIDE(NMI = DefaultExceptionHandler);
PROVIDE(HardFault = DefaultExceptionHandler);
PROVIDE(MemManage = DefaultExceptionHandler);
PROVIDE(BusFault = DefaultExceptionHandler);
PROVIDE(UsageFault = DefaultExceptionHandler);
PROVIDE(SVCall = DefaultExceptionHandler);
PROVIDE(PendSV = DefaultExceptionHandler);
PROVIDE(SysTick = DefaultExceptionHandler);

当检测完所有的输入目标文件而等号左侧的符号仍然没有定义的时候,PROVIDE 才会发挥作用。也就是用户没有为相关的异常实现处理函数时。

测试它

这就完了!rt crate现在支持异常处理函数了。我们可以用下列的应用测试它:

注意: 在QEMU中生成一个异常很难。在实际的硬件上对一个无效的存储地址进行 一个读取是足够产生一个中断的,但是QEMU却能接受这个操作并且返回零。一个陷入指 令在QEMU和硬件上都可以发挥中断的作用,但是不幸的是在稳定版上不可以用它,所以你需要 暂时切换到nightly版本中去运行这个和下个案例。

#![feature(core_intrinsics)]
#![no_main]
#![no_std]

use core::intrinsics;

use rt::entry;

entry!(main);

fn main() -> ! {
    // this executes the undefined instruction (UDF) and causes a HardFault exception
    intrinsics::abort()
}
(gdb) target remote :3333
Remote debugging using :3333
Reset () at ../rt/src/lib.rs:7
7       pub unsafe extern "C" fn Reset() -> ! {

(gdb) b DefaultExceptionHandler
Breakpoint 1 at 0xec: file ../rt/src/lib.rs, line 95.

(gdb) continue
Continuing.

Breakpoint 1, DefaultExceptionHandler ()
    at ../rt/src/lib.rs:95
95          loop {}

(gdb) list
90          Vector { handler: SysTick },
91      ];
92
93      #[no_mangle]
94      pub extern "C" fn DefaultExceptionHandler() {
95          loop {}
96      }

为了完整性,这里列出程序被优化过的版本的反汇编:

$ cargo objdump --bin app --release -- -d --no-show-raw-insn --print-imm-hex

app:	file format elf32-littlearm

Disassembly of section .text:

<main>:
               	trap
               	trap

<Reset>:
               	push	{r7, lr}
               	mov	r7, sp
               	movw	r1, #0x0
               	movw	r0, #0x0
               	movt	r1, #0x2000
               	movt	r0, #0x2000
               	subs	r1, r1, r0
               	bl	0x9c <__aeabi_memclr>   @ imm = #0x3e
               	movw	r1, #0x0
               	movw	r0, #0x0
               	movt	r1, #0x2000
               	movt	r0, #0x2000
               	subs	r2, r1, r0
               	movw	r1, #0x282
               	movt	r1, #0x0
               	bl	0x84 <__aeabi_memcpy>   @ imm = #0x8
               	bl	0x40 <main>             @ imm = #-0x40
               	trap

<UsageFault>:
$ cargo objdump --bin app --release -- -s -j .vector_table

app:	file format elf32-littlearm
Contents of section .vector_table:
 0000 00000120 45000000 83000000 83000000  ... E...........
 0010 83000000 83000000 83000000 00000000  ................
 0020 00000000 00000000 00000000 83000000  ................
 0030 00000000 00000000 83000000 83000000  ................

向量表现在像是集合了这本书中迄今为止所有的代码片段。总结下:

  • 在早期的存储章节的检查它部分,我们知道了:
    • 向量表中第一项包含了栈指针的初始值。
    • Objdump使用小端格式打印,所以栈开始于 0x2001_0000
    • 第二项指向地址 0x0000_0045,重置处理函数。
      • 在上面的反汇编中可以看到重置处理函数的地址,是 0x44
      • 由于对齐的要求被设置成1的第一位不会改变地址。而是,它让函数在 thumb mode 下执行。
  • 之后,可以看到在0x830x00之间交替的地址模式。
    • 看下上面的反汇编,很明显 0x83 指的是 DefaultExceptionHandler (0x84用thumb模式执行)。
    • 在这个章节早期所设置的向量表和Cortex-M的向量表布局的模式之间来回查看,很明显每次有个带处理函数的项出现在表中,DefaultExceptionHandler的地址就会出现。
    • 可以看到Rust代码中的向量表的数据结构的布局与Cortex-M向量表中的保留项依次对齐了。因此,所有的保留项被正确的设置成了零值。

重载一个处理函数

为了重载一个异常处理函数,用户必须提供一个函数,其符号名完全匹配我们在EXCEPTIONS中使用的名字。

#![feature(core_intrinsics)]
#![no_main]
#![no_std]

use core::intrinsics;

use rt::entry;

entry!(main);

fn main() -> ! {
    intrinsics::abort()
}

#[no_mangle]
pub extern "C" fn HardFault() -> ! {
    // do something interesting here
    loop {}
}

你可以在QEMU中测试它

(gdb) target remote :3333
Remote debugging using :3333
Reset () at /home/japaric/rust/embedonomicon/ci/exceptions/rt/src/lib.rs:7
7       pub unsafe extern "C" fn Reset() -> ! {

(gdb) b HardFault
Breakpoint 1 at 0x44: file src/main.rs, line 18.

(gdb) continue
Continuing.

Breakpoint 1, HardFault () at src/main.rs:18
18          loop {}

(gdb) list
13      }
14
15      #[no_mangle]
16      pub extern "C" fn HardFault() -> ! {
17          // do something interesting here
18          loop {}
19      }

程序现在执行了用户定义的HardFault函数而不是rt crate中的DefaultExceptionHandler

与我们在main接口中进行的第一次尝试一样,这个实现的问题是没有类型安全性。它也容易混淆异常的名字,但是不会生成一个错误或者警告。而仅仅是忽略用户定义的处理函数。这些问题可以使用一个像是在cortex-m-rt v0.5.x 中定义的exception!宏和cortex-m-rt v0.6.x中的exception属性来解决。

稳定的汇编

注意: 自Rust 1.59以来,inline 汇编(asm!)和 free form 汇编(global_asm!) 变得稳定了。但是因为现存的crates需要花点时间来消化这种变化,而且了解我们在历史中曾用过的处理汇编的方法对我们来说也有好处,所以我们保留了这一章。

到目前位置我们已经成功地引导了设备和处理中断而没使用一行汇编。这真是一个壮举!但是在某些目标架构下,可能需要一些汇编才能达成目的。也有一些操作像是上下文切换需要汇编,等等。

问题是 inline 汇编(asm!) 和 free form 汇编(global_asm!)是不稳定的,没法估计它们什么时候将变得稳定,所以你不能在稳定版中使用它们。这不是一个演示,因为这里记录的是一些变通的方法。

为了激发对这章的兴趣,我们将修改下HardFault处理函数以提供关于产生了异常的栈帧的信息。

我们想要做的是:

我们将让rt crate在向量表中放置一个可以跳向用户定义的HardFault的跳板,而不是让用户直接将它们的HardFault处理函数放在向量表中。

$ tail -n36 ../rt/src/lib.rs
#![allow(unused)]
fn main() {
extern "C" {
    fn NMI();
    fn HardFaultTrampoline(); // <- CHANGED!
    fn MemManage();
    fn BusFault();
    fn UsageFault();
    fn SVCall();
    fn PendSV();
    fn SysTick();
}

#[link_section = ".vector_table.exceptions"]
#[no_mangle]
pub static EXCEPTIONS: [Vector; 14] = [
    Vector { handler: NMI },
    Vector { handler: HardFaultTrampoline }, // <- CHANGED!
    Vector { handler: MemManage },
    Vector { handler: BusFault },
    Vector {
        handler: UsageFault,
    },
    Vector { reserved: 0 },
    Vector { reserved: 0 },
    Vector { reserved: 0 },
    Vector { reserved: 0 },
    Vector { handler: SVCall },
    Vector { reserved: 0 },
    Vector { reserved: 0 },
    Vector { handler: PendSV },
    Vector { handler: SysTick },
];

#[no_mangle]
pub extern "C" fn DefaultExceptionHandler() {
    loop {}
}
}

这个跳板将读取栈指针并调用用户的HardFault处理函数。这个跳板必须要用汇编来写:

  mrs r0, MSP
  b HardFault

由于ARM ABI的工作原理,它将主堆栈指针(MSP)设置为 HardFault 函数/例程的第一个参数。这个MSP值碰巧也是一个指针,其指向被异常推进栈中的寄存器。有了这些改变,用户的 HardFault 处理函数的签名现在必须是 fn(&StackedRegisters) -> !

.s 文件

一个使用稳定的汇编的方法是在一个外部文件中写汇编:

$ cat ../rt/asm.s
  .section .text.HardFaultTrampoline
  .global HardFaultTrampoline
  .thumb_func
HardFaultTrampoline:
  mrs r0, MSP
  b HardFault

并且使用rt crate的build script中的cc crate去把那个文件汇编成一个目标文件(.o),然后变成一个归档文件(.a)。

$ cat ../rt/build.rs
use std::{env, error::Error, fs::File, io::Write, path::PathBuf};

use cc::Build;

fn main() -> Result<(), Box<dyn Error>> {
    // build directory for this crate
    let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());

    // extend the library search path
    println!("cargo:rustc-link-search={}", out_dir.display());

    // put `link.x` in the build directory
    File::create(out_dir.join("link.x"))?.write_all(include_bytes!("link.x"))?;

    // assemble the `asm.s` file
    Build::new().file("asm.s").compile("asm"); // <- NEW!

    // rebuild if `asm.s` changed
    println!("cargo:rerun-if-changed=asm.s"); // <- NEW!

    Ok(())
}
$ tail -n2 ../rt/Cargo.toml
[build-dependencies]
cc = "1.0.25"

完成了!

通过编写一个非常简单的程序我们可以确认向量表包含一个指向 HardFaultTrampoline 的指针。

#![no_main]
#![no_std]

use rt::entry;

entry!(main);

fn main() -> ! {
    loop {}
}

#[allow(non_snake_case)]
#[no_mangle]
pub fn HardFault(_ef: *const u32) -> ! {
    loop {}
}

这是反汇编。我们看下 HardFaultTrampoline 的地址。

$ cargo objdump --bin app --release -- -d --no-show-raw-insn --print-imm-hex

app:	file format elf32-littlearm

Disassembly of section .text:

<HardFault>:
               	b	0x40 <HardFault>        @ imm = #-0x4

<main>:
               	b	0x42 <main>             @ imm = #-0x4

<Reset>:
               	push	{r7, lr}
               	mov	r7, sp
               	bl	0x42 <main>             @ imm = #-0xa
               	trap

<UsageFault>:
               	b	0x4e <UsageFault>       @ imm = #-0x4

<HardFaultTrampoline>:
               	mrs	r0, msp
               	b	0x40 <HardFault>        @ imm = #-0x18

注意: 为了让这个反汇编更小我注释掉了RAM的初始化

现在看下向量表。第四项应该是HardFaultTrampoline的地址加一。

$ cargo objdump --bin app --release -- -s -j .vector_table

app:	file format elf32-littlearm
Contents of section .vector_table:
 0000 00000120 45000000 4f000000 51000000  ... E...O...Q...
 0010 4f000000 4f000000 4f000000 00000000  O...O...O.......
 0020 00000000 00000000 00000000 4f000000  ............O...
 0030 00000000 00000000 4f000000 4f000000  ........O...O...

.o / .a 文件

使用cc crate的缺点是它需要编译机器上有对应的汇编器程序。比如当目标是ARM Cortex-M时,cc crate使用arm-none-eabi-gcc作为汇编器。

我们可以用rt crate来搬运一个预先汇编好的文件而不用在编译机器上汇编文件。这方法不需要在编译机器上拥有汇编器程序。然而,打包和发布crate的机器上仍然需要一个汇编器。

一个汇编(.s)文件和它的编译版:目标(.o)文件之间没有太大区别。汇编器不会做任何优化;它仅仅为目标架构选择正确的目标文件格式。

Cargo提供将归档文件(.a)和crates绑在一起的支持。使用ar命令我们可将目标文件打包进一个归档文件中,然后将归档文件和crate绑一起。事实上,这就是cc crate做的事;通过搜索一个在target文件夹中名为output的文件你可以看到cc crate调用的命令。

$ grep running $(find target -name output)
running: "arm-none-eabi-gcc" "-O0" "-ffunction-sections" "-fdata-sections" "-fPIC" "-g" "-fno-omit-frame-pointer" "-mthumb" "-march=armv7-m" "-Wall" "-Wextra" "-o" "/tmp/app/target/thumbv7m-none-eabi/debug/build/rt-6ee84e54724f2044/out/asm.o" "-c" "asm.s"
running: "ar" "crs" "/tmp/app/target/thumbv7m-none-eabi/debug/build/rt-6ee84e54724f2044/out/libasm.a" "/home/japaric/rust-embedded/embedonomicon/ci/asm/app/target/thumbv7m-none-eabi/debug/build/rt-6ee84e54724f2044/out/asm.o"
$ grep cargo $(find target -name output)
cargo:rustc-link-search=/tmp/app/target/thumbv7m-none-eabi/debug/build/rt-6ee84e54724f2044/out
cargo:rustc-link-lib=static=asm
cargo:rustc-link-search=native=/tmp/app/target/thumbv7m-none-eabi/debug/build/rt-6ee84e54724f2044/out

我们将做一些类似的事来生成一个归档文件。

$ # `cc` 使用的大多数标志在汇编时没有影响因此我们丢弃它们
$ arm-none-eabi-as -march=armv7-m asm.s -o asm.o

$ ar crs librt.a asm.o

$ arm-none-eabi-objdump -Cd librt.a
In archive librt.a:

asm.o:     file format elf32-littlearm


Disassembly of section .text.HardFaultTrampoline:

00000000 <HardFaultTrampoline>:
   0:	f3ef 8008 	mrs	r0, MSP
   4:	e7fe      	b.n	0 <HardFault>

接下来我们修改build script以把这个归档文件和rt rlib绑一起。

$ cat ../rt/build.rs
use std::{
    env,
    error::Error,
    fs::{self, File},
    io::Write,
    path::PathBuf,
};

fn main() -> Result<(), Box<dyn Error>> {
    // build directory for this crate
    let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());

    // extend the library search path
    println!("cargo:rustc-link-search={}", out_dir.display());

    // put `link.x` in the build directory
    File::create(out_dir.join("link.x"))?.write_all(include_bytes!("link.x"))?;

    // link to `librt.a`
    fs::copy("librt.a", out_dir.join("librt.a"))?; // <- NEW!
    println!("cargo:rustc-link-lib=static=rt"); // <- NEW!

    // rebuild if `librt.a` changed
    println!("cargo:rerun-if-changed=librt.a"); // <- NEW!

    Ok(())
}

现在我们可以用以前的简单程序测试这个新版本,我们将得到相同的输出

$ cargo objdump --bin app --release -- -d --no-show-raw-insn --print-imm-hex

app:	file format elf32-littlearm

Disassembly of section .text:

<HardFault>:
               	b	0x40 <HardFault>        @ imm = #-0x4

<main>:
               	b	0x42 <main>             @ imm = #-0x4

<Reset>:
               	push	{r7, lr}
               	mov	r7, sp
               	bl	0x42 <main>             @ imm = #-0xa
               	trap

<UsageFault>:
               	b	0x4e <UsageFault>       @ imm = #-0x4

<HardFaultTrampoline>:
               	mrs	r0, msp
               	b	0x40 <HardFault>        @ imm = #-0x18

注意: 像之前一样我已经注释掉了RAM的初始化以让反汇编变得更小。

$ cargo objdump --bin app --release -- -s -j .vector_table

app:	file format elf32-littlearm
Contents of section .vector_table:
 0000 00000120 45000000 4f000000 51000000  ... E...O...Q...
 0010 4f000000 4f000000 4f000000 00000000  O...O...O.......
 0020 00000000 00000000 00000000 4f000000  ............O...
 0030 00000000 00000000 4f000000 4f000000  ........O...O...

搬运预先汇编好的归档文件的缺点是,在最糟糕的情况下,库所支持的每个编译目标都需要有一个build工件。

使用符号做日志

这部分将展示如何使用符号和ELF格式去实现超级廉价的日志。

任意符号

当我们想要crates之间存在一个稳定的符号接口时,我们主要使用no_mangle属性,有时是export_name属性。export_name attribtue采用一个字符串作为符号的名字,而#[no_mangle]本质上是#[export_name = <item-name>]的语法糖。

事实证明,名字并不局限于单个单词;我们可以使用任意的字符串,比如语句,作为export_name属性的参数。至少当输出格式是ELF时,任何不包含空字节的内容都可以。

让我们看下:

$ cargo new --lib foo

$ cat foo/src/lib.rs
#![allow(unused)]
fn main() {
#[export_name = "Hello, world!"]
#[used]
static A: u8 = 0;

#[export_name = "こんにちは"]
#[used]
static B: u8 = 0;
}
$ ( cd foo && cargo nm --lib )
foo-d26a39c34b4e80ce.3lnzqy0jbpxj4pld.rcgu.o:
0000000000000000 r Hello, world!
0000000000000000 V __rustc_debug_gdb_scripts_section__
0000000000000000 r こんにちは

你能看出这有什么用吗?

编码

这是接下来要做的:我们将为每个日志信息创造一个static变量,但是不是将信息存储变量中,我们将把信息存储进变量的符号名中。然后,我们将记录的不是static变量的内容,而是它们的地址。

只要static变量的大小不是零,每个变量的地址就会不同。这里我们要做的是将每个信息有效地编码为一个唯一的标识符,正好变量的地址满足这个要求。日志系统必须有能力将这个id解码回日志信息。

让我们来编写一些代码解释下这个想法。

在这个例子里我们将需要一些工具来执行I/O操作,因此我们将使用cortex-m-semihosting库。Semihosting是一个技术,它可以让一个目标设备借用主机的I/O功能;这里,主机通常是指用来调试目标设备的机器。在我们的例子里,QEMU支持开箱即用的semihosting,因此不需要调试器。在一个真正的设备上你可以使用其它方法来执行I/O操作比如一个串口;在这个例子里我们使用semihosting,因为这是在QEMU上进行I/O操作的最简单的方法。

这里是代码

#![no_main]
#![no_std]

use core::fmt::Write;
use cortex_m_semihosting::{debug, hio};

use rt::entry;

entry!(main);

fn main() -> ! {
    let mut hstdout = hio::hstdout().unwrap();

    #[export_name = "Hello, world!"]
    static A: u8 = 0;

    let _ = writeln!(hstdout, "{:#x}", &A as *const u8 as usize);

    #[export_name = "Goodbye"]
    static B: u8 = 0;

    let _ = writeln!(hstdout, "{:#x}", &B as *const u8 as usize);

    debug::exit(debug::EXIT_SUCCESS);

    loop {}
}

我们也可以使用debug::exit API来让程序终结QEMU进程。这很方便,这样我们就不必手动终结QEMU进程了。

这里是Cargo.toml的dependencies部分:

[dependencies]
cortex-m-semihosting = "0.3.1"
rt = { path = "../rt" }

现在我们可以编译程序了

$ cargo build

为了让它跑起来,我们需要给QEMU命令添加 --semihosting-config 标志:

$ qemu-system-arm \
      -cpu cortex-m3 \
      -machine lm3s6965evb \
      -nographic \
      -semihosting-config enable=on,target=native \
      -kernel target/thumbv7m-none-eabi/debug/app
0x1fe0
0x1fe1

注意:你主机上获得的地址可能不是这些地址,因为当工具链变了的时候static变量的地址不保障仍然是一样的(比如 优化可能会改进地址)。

现在我们有两个地址被打印到控制台上了。

解码

我们如何把这些地址转换成字符串呢?答案在ELF文件的符号表中。

$ cargo objdump --bin app -- -t | grep '\.rodata\s*0*1\b'
00001fe1 g       .rodata		 00000001 Goodbye
00001fe0 g       .rodata		 00000001 Hello, world!
$ # 第一列是符号地址;最后一列是符号名

objdump -t 打印符号表。这个表包含所有的符号但是我们只查找.rodata中的符号,且它的大小只有一个字节(我们的变量类型是u8) 。

需要注意的是,在优化程序时,符号的地址可能会改变。让我们检查下。

小技巧 你可以在Cargo配置文件(.cargo/config)中把 target.thumbv7m-none-eabi.runner 设置成之前(qemu-system-arm -cpu (..)) 的QEMU命令以让cargo run使用这个runner执行输出的二进制项。

$ head -n2 .cargo/config
[target.thumbv7m-none-eabi]
runner = "qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel"
$ cargo run --release
     Running `qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel target/thumbv7m-none-eabi/release/app`
0xb9c
0xb9d
$ cargo objdump --bin app --release -- -t | grep '\.rodata\s*0*1\b'
00000b9d g     O .rodata	00000001 Goodbye
00000b9c g     O .rodata	00000001 Hello, world!

所以,请确保在找ELF文件中的字符串的时候,这个ELF文件是你执行的那个ELF文件。

当然,可以使用一个工具自动检查ELF文件中的字符串,该工具可以解析ELF文件包含的符号表(.symtab) 。如何实现这样的工具超出了这本书的范围,可以作为一个练习留给读者。

使它变成零开销

我们能做得更好吗?是的,我们可以!

现在的实现把static变量放在.rodata中,这意味着它们会占用Flash的大小即使我们从不会使用它们的内容。使用一点链接器脚本魔法我们可以让它们占用的Flash空间为

$ cat log.x
SECTIONS
{
  .log 0 (INFO) : {
    *(.log);
  }
}

我们将把static变量放在这个新的输出的.log section中。这个链接器脚本将把输入目标文件的.log section中的所有符号集中起来并把它们放进一个输出的.log section中。我们在存储布局章节中已经见过这个方法了。

这个(INFO)部分是新的知识点;这告诉链接器这个section是一个非可分配部分。非可分配部分在ELF中作为元数据被保留起来但是它们不会被加载到目标设备上。

我们也可以指定这个输出section的起始地址:.log 0 (INFO) 中的0

我们可以做的其它改进是从格式化的I/O(fmt::Write)切换到二进制I/O,这是指把地址作为字节而不是字符串往主机发送。

二进制序列化很难,但是把每个地址当做一个字节来序列化就很简单了,这样我们就不必担心大小端或者分帧。这个格式的缺点是单个字节只能表示最多256个不同的地址。

添加上这些修改:

#![no_main]
#![no_std]

use cortex_m_semihosting::{debug, hio};

use rt::entry;

entry!(main);

fn main() -> ! {
    let mut hstdout = hio::hstdout().unwrap();

    #[export_name = "Hello, world!"]
    #[link_section = ".log"] // <- NEW!
    static A: u8 = 0;

    let address = &A as *const u8 as usize as u8;
    hstdout.write_all(&[address]).unwrap(); // <- CHANGED!

    #[export_name = "Goodbye"]
    #[link_section = ".log"] // <- NEW!
    static B: u8 = 0;

    let address = &B as *const u8 as usize as u8;
    hstdout.write_all(&[address]).unwrap(); // <- CHANGED!

    debug::exit(debug::EXIT_SUCCESS);

    loop {}
}

在运行这个程序之前,必须要在传递给链接器的参数之后添加-Tlog.x 。可以在Cargo的配置文件中完成它。

$ cat .cargo/config
[target.thumbv7m-none-eabi]
runner = "qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel"
rustflags = [
  "-C", "link-arg=-Tlink.x",
  "-C", "link-arg=-Tlog.x", # <- NEW!
]

[build]
target = "thumbv7m-none-eabi"

现在可以运行了!因为现在输出是一个二进制的格式,所以通过管道将它输入xxd命令中重新格式化它为十六进制的字符串。

$ cargo run | xxd -p
0001

地址是0x000x01 。让我们现在看下符号表。

$ cargo objdump --bin app -- -t | grep '\.log'
00000001 g     O .log	00000001 Goodbye
00000000 g     O .log	00000001 Hello, world!

这是我们的字符串。可以看到它们的地址现在开始于零;这是因为输出的.log section设置了一个起始地址。

每个变量是一个字节的大小因为使用了u8作为它们的类型。如果我们使用类似于u16的东西,那么所有的地址都是偶数,我们将无法有效的使用所有的地址空间(0...255) 。

打包

注意到,记录一个字符串的步骤总是一样的,因此我们可以把它们重构为宏放在自己的库中。此外,通过将I/O部分抽象成一个trait,我们还可以提高日志库的可重用性。

$ cargo new --lib log

$ cat log/src/lib.rs
#![allow(unused)]
#![no_std]

fn main() {
pub trait Log {
    type Error;

    fn log(&mut self, address: u8) -> Result<(), Self::Error>;
}

#[macro_export]
macro_rules! log {
    ($logger:expr, $string:expr) => {{
        #[export_name = $string]
        #[link_section = ".log"]
        static SYMBOL: u8 = 0;

        $crate::Log::log(&mut $logger, &SYMBOL as *const u8 as usize as u8)
    }};
}
}

因为这个库依赖.log section,所以它应该负责提供log.x链接器脚本,让我们来实现这一需求。

$ mv log.x ../log/
$ cat ../log/build.rs
use std::{env, error::Error, fs::File, io::Write, path::PathBuf};

fn main() -> Result<(), Box<dyn Error>> {
    // Put the linker script somewhere the linker can find it
    let out = PathBuf::from(env::var("OUT_DIR")?);

    File::create(out.join("log.x"))?.write_all(include_bytes!("log.x"))?;

    println!("cargo:rustc-link-search={}", out.display());

    Ok(())
}

现在我们可以重构我们的应用,让它使用log!宏:

$ cat src/main.rs
#![no_main]
#![no_std]

use cortex_m_semihosting::{
    debug,
    hio::{self, HStdout},
};

use log::{log, Log};
use rt::entry;

struct Logger {
    hstdout: HStdout,
}

impl Log for Logger {
    type Error = ();

    fn log(&mut self, address: u8) -> Result<(), ()> {
        self.hstdout.write_all(&[address])
    }
}

entry!(main);

fn main() -> ! {
    let hstdout = hio::hstdout().unwrap();
    let mut logger = Logger { hstdout };

    let _ = log!(logger, "Hello, world!");

    let _ = log!(logger, "Goodbye");

    debug::exit(debug::EXIT_SUCCESS);

    loop {}
}

不要忘记将Cargo.toml文件更新成依赖新的log库 。

$ tail -n4 Cargo.toml
[dependencies]
cortex-m-semihosting = "0.3.1"
log = { path = "../log" }
rt = { path = "../rt" }
$ cargo run | xxd -p
0001
$ cargo objdump --bin app -- -t | grep '\.log'
00000001 g     O .log	00000001 Goodbye
00000000 g     O .log	00000001 Hello, world!

与之前一样的输出!

额外任务:多层日志等级

许多日志框架提供方法让你可以使用不同的日志等级去记录信息。这些日志等级表示信息的严重性:“这是一个错误”,“这只是一个警告”,等等。当搜寻,比如,错误信息时,这些日志等级可以被用来过滤掉不重要的信息。

我们可以扩展我们的日志库,让它在不增加空间的情况下支持日志等级。这是我们接下来要做的:

我们有一个与这些信息相关的平铺的地址空间:从0255(包含255)。为了让事情简单点,我们只想区分错误信息和警告信息。我们可以把所有的错误信息放在地址空间的开始处,并将所有的警告信息放在错误信息之后。如果解码器知道第一个警告信息的地址,那么它可以用这个地址分类出消息来。这个方法可以被扩展到支持两个以上的日志等级。

通过使用两个新的宏:error!warn!替代log宏来测试下这个方法。

$ cat ../log/src/lib.rs
#![allow(unused)]
#![no_std]

fn main() {
pub trait Log {
    type Error;

    fn log(&mut self, address: u8) -> Result<(), Self::Error>;
}

/// Logs messages at the ERROR log level
#[macro_export]
macro_rules! error {
    ($logger:expr, $string:expr) => {{
        #[export_name = $string]
        #[link_section = ".log.error"] // <- CHANGED!
        static SYMBOL: u8 = 0;

        $crate::Log::log(&mut $logger, &SYMBOL as *const u8 as usize as u8)
    }};
}

/// Logs messages at the WARNING log level
#[macro_export]
macro_rules! warn {
    ($logger:expr, $string:expr) => {{
        #[export_name = $string]
        #[link_section = ".log.warning"] // <- CHANGED!
        static SYMBOL: u8 = 0;

        $crate::Log::log(&mut $logger, &SYMBOL as *const u8 as usize as u8)
    }};
}
}

通过将信息放在不同的link sections中,可以区分错误和警告。

下一个必须要做的事是修改链接器脚本,将错误信息放在警告信息之前。

$ cat ../log/log.x
SECTIONS
{
  .log 0 (INFO) : {
    *(.log.error);
    __log_warning_start__ = .;
    *(.log.warning);
  }
}

我们也将错误和警告之间的边界命名为__log_warning_start__。这个符号的地址将是第一个警告信息的地址。

我们现在可以改下应用,让应用使用这些新的宏。

$ cat src/main.rs
#![no_main]
#![no_std]

use cortex_m_semihosting::{
    debug,
    hio::{self, HStdout},
};

use log::{error, warn, Log};
use rt::entry;

entry!(main);

fn main() -> ! {
    let hstdout = hio::hstdout().unwrap();
    let mut logger = Logger { hstdout };

    let _ = warn!(logger, "Hello, world!"); // <- CHANGED!

    let _ = error!(logger, "Goodbye"); // <- CHANGED!

    debug::exit(debug::EXIT_SUCCESS);

    loop {}
}

struct Logger {
    hstdout: HStdout,
}

impl Log for Logger {
    type Error = ();

    fn log(&mut self, address: u8) -> Result<(), ()> {
        self.hstdout.write_all(&[address])
    }
}

输出不会改变太多:

$ cargo run | xxd -p
0100

输出仍然是两个字节,但是错误被赋予了地址0,警告被赋予了地址1,即使警告被先记录。

现在看一下符号表。

$ cargo objdump --bin app -- -t | grep '\.log'
00000000 g     O .log	00000001 Goodbye
00000001 g     O .log	00000001 Hello, world!
00000001 g       .log	00000000 __log_warning_start__

现在在.log section中,有了一个额外符号,__log_warning_start__ 。这个符号的地址是第一个警告信息的地址。地址低于这个值的符号是错误,剩余的符号是警告。

使用一个合适的解码器你可以从所有这些信息获得以下人类可读的输出:

WARNING Hello, world!
ERROR Goodbye

如果你喜欢这部分,看一下stlog日志框架,它完整实现了这个想法。

全局单例

在这部分会说到如何实现一个全局的,可共享的单例。 The embedded Rust book 中提到了局部的,拥有所有权的单例, 这对Rust来说几乎是独一无二的。全局单例本质上就是在C和C++中见到的那些单例模式;它们不止出现在嵌入式开发中,但是因为它们涉及到符号,所以似乎很适合这本书的内容。

TODO(resources team) link "the embedded Rust book" to the singletons section when it's up

为了解释这部分,我们将扩展我们在上部分开发的日志以支持全局日志记录。结果与在the embedded Rust book提到的#[global_allocator]功能非常相似。

TODO(resources team) link #[global_allocator] to the collections chapter of the book when it's in a more stable location.

总结下我们需要的东西:

在上一部分我们创造了一个log!宏以通过一个特定的logger去记录信息,logger是一个实现了Log trait的值。 log!宏的语法是log!(logger, "String")。我们想要扩展下宏,让log!("String")也可以工作。没有logger的版本的宏可以通过一个全局的logger去记录信息;这是std::println!的工作方式。我们也需要一个机制去声明全局logger是什么;这部分与#[global_allocator]相似。

可能是在top crate中声明了全局logger,也可能是在top crate中定义了全局logger的类型。在这种情况下,依赖知道全局logger的确切类型。为了支持这种情况,我们需要一些间接方法。

我们只在log库中声明全局logger的接口,而不是在log库中硬编码全局logger的类型。我们将会给log库添加一个新的trait,GlobalLoglog!宏也必须要使用这个trait 。

$ cat ../log/src/lib.rs
#![allow(unused)]
#![no_std]

fn main() {
// NEW!
pub trait GlobalLog: Sync {
    fn log(&self, address: u8);
}

pub trait Log {
    type Error;

    fn log(&mut self, address: u8) -> Result<(), Self::Error>;
}

#[macro_export]
macro_rules! log {
    // NEW!
    ($string:expr) => {
        unsafe {
            extern "Rust" {
                static LOGGER: &'static dyn $crate::GlobalLog;
            }

            #[export_name = $string]
            #[link_section = ".log"]
            static SYMBOL: u8 = 0;

            $crate::GlobalLog::log(LOGGER, &SYMBOL as *const u8 as usize as u8)
        }
    };

    ($logger:expr, $string:expr) => {{
        #[export_name = $string]
        #[link_section = ".log"]
        static SYMBOL: u8 = 0;

        $crate::Log::log(&mut $logger, &SYMBOL as *const u8 as usize as u8)
    }};
}

// NEW!
#[macro_export]
macro_rules! global_logger {
    ($logger:expr) => {
        #[no_mangle]
        pub static LOGGER: &dyn $crate::GlobalLog = &$logger;
    };
}
}

这里有很多东西要拆开来看。

先从trait开始。

#![allow(unused)]
fn main() {
pub trait GlobalLog: Sync {
    fn log(&self, address: u8);
}
}

GlobalLogLog都有一个log方法。不同的是,GlobalLog需要获取一个对接收者的共享的引用(&self)。 因为全局logger是一个static变量所以这是必须的。之后会提到更多。

另一个不同是,GlobalLog.log不返回一个Result。 这意味着它会向调用者报告错误。这对用来实现全局单例的traits来说不是一个严格的要求。全局单例中有错误处理很好,但是另一方面全局版本的log!宏的的所有用户必须就错误类型达成一致。这里通过让GlobalLog的实现者来处理错误可以简化这个接口。

还有另一个不同,GlobalLog要求实现者是Sync的,它可以在线程间被共享。对于放置在static变量中的值来说这是一个要求;它们的类型必须实现Sync trait 。

此时可能还不完全清楚接口为什么必须要这样。库的其它部分将会解释得更清楚,所以请继续读下去。

接下来是log!宏:

#![allow(unused)]
fn main() {
    ($string:expr) => {
        unsafe {
            extern "Rust" {
                static LOGGER: &'static dyn $crate::GlobalLog;
            }

            #[export_name = $string]
            #[link_section = ".log"]
            static SYMBOL: u8 = 0;

            $crate::GlobalLog::log(LOGGER, &SYMBOL as *const u8 as usize as u8)
        }
    };
}

当不使用一个指定的$logger去调用宏的时候,宏会使用一个被叫做LOGGERextern static变量去记录信息。这个变量定义在其它地方的全局logger;这就是为什么我们会使用extern块。我们可以在main接口章节中看到这种模式。

我们需要声明一个与LOGGER有关的类型要不然代码不会做类型检查。此时我们不知道LOGGER的具体类型,但是我们知道,或者至少要求,它要实现GlobalLog trait,所以这里我们可以使用一个trait对象。

剩余的宏展开与局部版本的log!宏展开很像,因此我不会在这里解释它,因为在先前的章节中已经解释过了。

现在我们知道LOGGER必须是一个trait对象,为什么我们要在GlobalLog中去掉关联的Error类型更清楚了。如果我们没有去掉Error类型,那么我们将需要为LOGGER的类型签名中的Error挑选一个类型。这就是我之前提到的“log!的所有用户需要对错误类型达成一致”。

现在是最后的片段:global_logger!宏。它可以是一个过程宏attribute,但是写一个macro_rules!宏更简单。

#![allow(unused)]
fn main() {
#[macro_export]
macro_rules! global_logger {
    ($logger:expr) => {
        #[no_mangle]
        pub static LOGGER: &dyn $crate::GlobalLog = &$logger;
    };
}
}

这个宏生成了log!宏要使用的LOGGER变量。因为我们需要一个稳定的ABI接口,所以我们使用了no_mangle attribute 。这样子的话,LOGGER的符号名就会是LOGGER,这是log!宏期望的符号名。

另外重要的一点是,这个静态变量的类型必须精确地匹配在log!宏的展开中所使用的类型。如果它们不匹配,由于ABI的误匹配将会导致坏事发生。

让我们来写一个使用这个新的全局logger功能的例子。

$ cat src/main.rs
#![no_main]
#![no_std]

use cortex_m::interrupt;
use cortex_m_semihosting::{
    debug,
    hio::{self, HStdout},
};

use log::{global_logger, log, GlobalLog};
use rt::entry;

struct Logger;

global_logger!(Logger);

entry!(main);

fn main() -> ! {
    log!("Hello, world!");

    log!("Goodbye");

    debug::exit(debug::EXIT_SUCCESS);

    loop {}
}

impl GlobalLog for Logger {
    fn log(&self, address: u8) {
        // we use a critical section (`interrupt::free`) to make the access to the
        // `static mut` variable interrupt safe which is required for memory safety
        interrupt::free(|_| unsafe {
            static mut HSTDOUT: Option<HStdout> = None;

            // lazy initialization
            if HSTDOUT.is_none() {
                HSTDOUT = Some(hio::hstdout()?);
            }

            let hstdout = HSTDOUT.as_mut().unwrap();

            hstdout.write_all(&[address])
        }).ok(); // `.ok()` = ignore errors
    }
}

TODO(resources team) use cortex_m::Mutex instead of a static mut variable when const fn is stabilized.

我们必须添加cortex-m到这个依赖上。

$ tail -n5 Cargo.toml
[dependencies]
cortex-m = "0.5.7"
cortex-m-semihosting = "0.3.1"
log = { path = "../log" }
rt = { path = "../rt" }

这是在先前章节中所写的某个例子的移植。这里的输出和之前的是一样的。

$ cargo run | xxd -p
0001
$ cargo objdump --bin app -- -t | grep '\.log'
00000001 g     O .log	00000001 Goodbye
00000000 g     O .log	00000001 Hello, world!

一些读者可能会担心这种全局单例的实现不是零开销的,因为使用trait对象涉及到动态分发(dynamic dispatch),通过一个虚表(vtable)查找去执行方法调用。

然而,LLVM足够聪明,可以在使用优化/LTO编译时,消除动态分发。通过在符号表中搜索LOGGER可以确认这一点。

$ cargo objdump --bin app --release -- -t | grep LOGGER

如果没有static,则意味着没有虚表,LLVM能够把所有的LOGGER.log调用变成Logger.log调用。

直接存储器访问 (DMA)

本节会围绕DMA传输,讨论要搭建一个内存安全的API的核心需求。

DMA外设被用来以并行于处理器的工作(主程序的执行)的方式来执行存储传输。一个DMA传输或多或少等于启动一个进程(看thread::spawn)去执行一个memcpy 。我们将用fork-join模型去解释一个内存安全的API的要求。

考虑下面的DMA数据类型:

#![allow(unused)]
fn main() {
/// A singleton that represents a single DMA channel (channel 1 in this case)
///
/// This singleton has exclusive access to the registers of the DMA channel 1
pub struct Dma1Channel1 {
    // ..
}

impl Dma1Channel1 {
    /// Data will be written to this `address`
    ///
    /// `inc` indicates whether the address will be incremented after every byte transfer
    ///
    /// NOTE this performs a volatile write
    pub fn set_destination_address(&mut self, address: usize, inc: bool) {
        // ..
    }

    /// Data will be read from this `address`
    ///
    /// `inc` indicates whether the address will be incremented after every byte transfer
    ///
    /// NOTE this performs a volatile write
    pub fn set_source_address(&mut self, address: usize, inc: bool) {
        // ..
    }

    /// Number of bytes to transfer
    ///
    /// NOTE this performs a volatile write
    pub fn set_transfer_length(&mut self, len: usize) {
        // ..
    }

    /// Starts the DMA transfer
    ///
    /// NOTE this performs a volatile write
    pub fn start(&mut self) {
        // ..
    }

    /// Stops the DMA transfer
    ///
    /// NOTE this performs a volatile write
    pub fn stop(&mut self) {
        // ..
    }

    /// Returns `true` if there's a transfer in progress
    ///
    /// NOTE this performs a volatile read
    pub fn in_progress() -> bool {
        // ..
    }
}
}

假设Dma1Channel1被静态地配置成按one-shot的模式(也即不是circular模式)使用串口(又称作UART或者USART) #1,Serial1Serial1提供下面的阻塞版的API:

#![allow(unused)]
fn main() {
/// A singleton that represents serial port #1
pub struct Serial1 {
    // ..
}

impl Serial1 {
    /// Reads out a single byte
    ///
    /// NOTE: blocks if no byte is available to be read
    pub fn read(&mut self) -> Result<u8, Error> {
        // ..
    }

    /// Sends out a single byte
    ///
    /// NOTE: blocks if the output FIFO buffer is full
    pub fn write(&mut self, byte: u8) -> Result<(), Error> {
        // ..
    }
}
}

假设我们想要将Serial1 API扩展成可以(a)异步地发送一个缓存区和(b)异步地填充一个缓存区。

一开始我们将使用一个存储不安全的API,然后我们将迭代它直到它完全变成存储安全的API。在每一步,我们都将向你展示如何破开API, 让你意识到当使用异步的存储操作时,有哪些问题需要被解决。

开场

作为开端,让我们尝试使用Write::write_all API作为参考。为了简便,让我们忽略所有的错误处理。

#![allow(unused)]
fn main() {
/// A singleton that represents serial port #1
pub struct Serial1 {
    // NOTE: we extend this struct by adding the DMA channel singleton
    dma: Dma1Channel1,
    // ..
}

impl Serial1 {
    /// Sends out the given `buffer`
    ///
    /// Returns a value that represents the in-progress DMA transfer
    pub fn write_all<'a>(mut self, buffer: &'a [u8]) -> Transfer<&'a [u8]> {
        self.dma.set_destination_address(USART1_TX, false);
        self.dma.set_source_address(buffer.as_ptr() as usize, true);
        self.dma.set_transfer_length(buffer.len());

        self.dma.start();

        Transfer { buffer }
    }
}

/// A DMA transfer
pub struct Transfer<B> {
    buffer: B,
}

impl<B> Transfer<B> {
    /// Returns `true` if the DMA transfer has finished
    pub fn is_done(&self) -> bool {
        !Dma1Channel1::in_progress()
    }

    /// Blocks until the transfer is done and returns the buffer
    pub fn wait(self) -> B {
        // Busy wait until the transfer is done
        while !self.is_done() {}

        self.buffer
    }
}
}

注意: 不用像上面的API一样,Transfer的API也可以暴露一个futures或者generator。 这是一个API设计问题,与整个API的内存安全性关系不大,因此我们在本文中不会深入讨论。

我们也可以实现一个异步版本的Read::read_exact

#![allow(unused)]
fn main() {
impl Serial1 {
    /// Receives data into the given `buffer` until it's filled
    ///
    /// Returns a value that represents the in-progress DMA transfer
    pub fn read_exact<'a>(&mut self, buffer: &'a mut [u8]) -> Transfer<&'a mut [u8]> {
        self.dma.set_source_address(USART1_RX, false);
        self.dma
            .set_destination_address(buffer.as_mut_ptr() as usize, true);
        self.dma.set_transfer_length(buffer.len());

        self.dma.start();

        Transfer { buffer }
    }
}
}

这里是write_all API的用法:

#![allow(unused)]
fn main() {
fn write(serial: Serial1) {
    // fire and forget
    serial.write_all(b"Hello, world!\n");

    // do other stuff
}
}

这是使用read_exact API的一个例子:

#![allow(unused)]
fn main() {
fn read(mut serial: Serial1) {
    let mut buf = [0; 16];
    let t = serial.read_exact(&mut buf);

    // do other stuff

    t.wait();

    match buf.split(|b| *b == b'\n').next() {
        Some(b"some-command") => { /* do something */ }
        _ => { /* do something else */ }
    }
}
}

mem::forget

mem::forget是一个安全的API。如果我们的API真的是安全的,那么我们应该能够将两者结合使用而不会出现未定义的行为。然而,情况并非如此;考虑下下面的例子:

#![allow(unused)]
fn main() {
fn unsound(mut serial: Serial1) {
    start(&mut serial);
    bar();
}

#[inline(never)]
fn start(serial: &mut Serial1) {
    let mut buf = [0; 16];

    // start a DMA transfer and forget the returned `Transfer` value
    mem::forget(serial.read_exact(&mut buf));
}

#[inline(never)]
fn bar() {
    // stack variables
    let mut x = 0;
    let mut y = 0;

    // use `x` and `y`
}
}

start中我们启动了一个DMA传输以填充一个在堆上分配的数组,然后mem::forget了被返回的Transfer值。然后我们继续从start返回并执行函数bar

这一系列操作导致了未定义的行为。DMA传输向栈的存储区写入,但是当start返回时,那块存储区域会被释放, 然后被bar重新用来分配像是xy这样的变量。在运行时,这可能会导致变量xy随机更改其值。DMA传输 也会覆盖掉被函数bar的序言推入栈中的状态(比如link寄存器)。

注意如果我们不用mem::forget,而是mem::drop,可以让Transfer的析构函数停止DMA的传输,这样程序就变成了安全的了。但是能依赖于运行析构函数来加强存储安全性因为mem::forget和内存泄露(看下RC cycles)在Rust中是安全的。

通过在APIs中把缓存的生命周期从'a变成'static来修复这个问题。

#![allow(unused)]
fn main() {
impl Serial1 {
    /// Receives data into the given `buffer` until it's filled
    ///
    /// Returns a value that represents the in-progress DMA transfer
    pub fn read_exact(&mut self, buffer: &'static mut [u8]) -> Transfer<&'static mut [u8]> {
        // .. same as before ..
    }

    /// Sends out the given `buffer`
    ///
    /// Returns a value that represents the in-progress DMA transfer
    pub fn write_all(mut self, buffer: &'static [u8]) -> Transfer<&'static [u8]> {
        // .. same as before ..
    }
}
}

如果我们尝试复现先前的问题,我们注意到mem::forget不再引起问题了。

#![allow(unused)]
fn main() {
#[allow(dead_code)]
fn sound(mut serial: Serial1, buf: &'static mut [u8; 16]) {
    // NOTE `buf` is moved into `foo`
    foo(&mut serial, buf);
    bar();
}

#[inline(never)]
fn foo(serial: &mut Serial1, buf: &'static mut [u8]) {
    // start a DMA transfer and forget the returned `Transfer` value
    mem::forget(serial.read_exact(buf));
}

#[inline(never)]
fn bar() {
    // stack variables
    let mut x = 0;
    let mut y = 0;

    // use `x` and `y`
}
}

像之前一样,在mem::forget Transfer的值之后,DMA传输继续运行着。这次没有问题了,因为buf是静态分配的(比如static mut变量),不是在栈上。

重复使用(Overlapping use)

我们的API没有阻止用户在DMA传输过程中再次使用Serial接口。这可能导致传输失败或者数据丢失。

有许多方法可以禁止重叠使用。一个方法是让Transfer获取Serial1的所有权,然后当wait被调用时将它返回。

#![allow(unused)]
fn main() {
/// A DMA transfer
pub struct Transfer<B> {
    buffer: B,
    // NOTE: added
    serial: Serial1,
}

impl<B> Transfer<B> {
    /// Blocks until the transfer is done and returns the buffer
    // NOTE: the return value has changed
    pub fn wait(self) -> (B, Serial1) {
        // Busy wait until the transfer is done
        while !self.is_done() {}

        (self.buffer, self.serial)
    }

    // ..
}

impl Serial1 {
    /// Receives data into the given `buffer` until it's filled
    ///
    /// Returns a value that represents the in-progress DMA transfer
    // NOTE we now take `self` by value
    pub fn read_exact(mut self, buffer: &'static mut [u8]) -> Transfer<&'static mut [u8]> {
        // .. same as before ..

        Transfer {
            buffer,
            // NOTE: added
            serial: self,
        }
    }

    /// Sends out the given `buffer`
    ///
    /// Returns a value that represents the in-progress DMA transfer
    // NOTE we now take `self` by value
    pub fn write_all(mut self, buffer: &'static [u8]) -> Transfer<&'static [u8]> {
        // .. same as before ..

        Transfer {
            buffer,
            // NOTE: added
            serial: self,
        }
    }
}
}

移动语义静态地阻止了当传输在进行时对Serial1的访问。

#![allow(unused)]
fn main() {
fn read(serial: Serial1, buf: &'static mut [u8; 16]) {
    let t = serial.read_exact(buf);

    // let byte = serial.read(); //~ ERROR: `serial` has been moved

    // .. do stuff ..

    let (serial, buf) = t.wait();

    // .. do more stuff ..
}
}

还有其它方法可以防止重叠使用。比如,可以往Serial1添加一个(Cell)标志,其指出是否一个DMA传输正在进行中。 当标志被设置了,readwriteread_exactwrite_all全都会在运行时返回一个错误(比如Error::InUse)。 当使用write_all / read_exact时,会设置标志,在Transfer.wait中,标志会被清除。

编译器(误)优化

编译器可以自由地重新排序和合并不是volatile的存储操作以更好地优化一个程序。使用我们现在的API,这种自由度会导致未定义的行为。想一下下面的例子:

#![allow(unused)]
fn main() {
fn reorder(serial: Serial1, buf: &'static mut [u8]) {
    // zero the buffer (for no particular reason)
    buf.iter_mut().for_each(|byte| *byte = 0);

    let t = serial.read_exact(buf);

    // ... do other stuff ..

    let (buf, serial) = t.wait();

    buf.reverse();

    // .. do stuff with `buf` ..
}
}

这里编译器可以将buf.reverse()移到t.wait()之前,其将导致一个数据竞争问题:处理器和DMA最终都会同时修改buf 。同样地编译器可以将赋零操作放到read_exact之后,它也会导致一个数据竞争问题。

为了避免这些存在问题的重排序,我们可以使用一个 compiler_fence

#![allow(unused)]
fn main() {
impl Serial1 {
    /// Receives data into the given `buffer` until it's filled
    ///
    /// Returns a value that represents the in-progress DMA transfer
    pub fn read_exact(mut self, buffer: &'static mut [u8]) -> Transfer<&'static mut [u8]> {
        self.dma.set_source_address(USART1_RX, false);
        self.dma
            .set_destination_address(buffer.as_mut_ptr() as usize, true);
        self.dma.set_transfer_length(buffer.len());

        // NOTE: added
        atomic::compiler_fence(Ordering::Release);

        // NOTE: this is a volatile *write*
        self.dma.start();

        Transfer {
            buffer,
            serial: self,
        }
    }

    /// Sends out the given `buffer`
    ///
    /// Returns a value that represents the in-progress DMA transfer
    pub fn write_all(mut self, buffer: &'static [u8]) -> Transfer<&'static [u8]> {
        self.dma.set_destination_address(USART1_TX, false);
        self.dma.set_source_address(buffer.as_ptr() as usize, true);
        self.dma.set_transfer_length(buffer.len());

        // NOTE: added
        atomic::compiler_fence(Ordering::Release);

        // NOTE: this is a volatile *write*
        self.dma.start();

        Transfer {
            buffer,
            serial: self,
        }
    }
}

impl<B> Transfer<B> {
    /// Blocks until the transfer is done and returns the buffer
    pub fn wait(self) -> (B, Serial1) {
        // NOTE: this is a volatile *read*
        while !self.is_done() {}

        // NOTE: added
        atomic::compiler_fence(Ordering::Acquire);

        (self.buffer, self.serial)
    }

    // ..
}
}

我们在read_exactwrite_all中使用Ordering::Release以避免所有的正在进行中的存储操作被移动到self.dma.start()后面去,其执行了一个volatile写入。

同样地,我们在Transfer.wait中使用Ordering::Acquire以避免所有的后续的存储操作被移到self.is_done()之前,其执行了一个volatile读入。

为了更好地展示fences的影响,稍微修改下上个部分中的例子。我们将fences和它们的orderings添加到注释中。

#![allow(unused)]
fn main() {
fn reorder(serial: Serial1, buf: &'static mut [u8], x: &mut u32) {
    // zero the buffer (for no particular reason)
    buf.iter_mut().for_each(|byte| *byte = 0);

    *x += 1;

    let t = serial.read_exact(buf); // compiler_fence(Ordering::Release) ▲

    // NOTE: the processor can't access `buf` between the fences
    // ... do other stuff ..
    *x += 2;

    let (buf, serial) = t.wait(); // compiler_fence(Ordering::Acquire) ▼

    *x += 3;

    buf.reverse();

    // .. do stuff with `buf` ..
}
}

由于Release fence,赋零操作能被移到read_exact之后。同样地,由于Acquire fence,reverse操作能被移动wait之前。 在两个fences之间的存储操作可以在fences间自由地重新排序,但是这些操作都不会涉及到buf,所以这种重新排序会导致未定义的行为。

请注意compiler_fence比要求的强一些。比如,fences将防止在x上的操作被合并即使我们知道buf不会与x重叠(由于Rust的别名规则)。 然而,没有比compiler_fence更精细的内部函数了。

我们需不需要内存屏障?

这取决于目标平台的架构。在Cortex M0到M4F核心的例子里,AN321说到:

3.2 主要场景

(..)

在Cortex-M处理器中,很少需要用到DMB因为它们不会重新排序存储传输。 然而,如果软件要在其它ARM处理器中复用,那么就需要用到,特别是多主机系统。比如:

  • DMA控制器配置。在CPU存储访问和一个DMA操作间需要一个屏障。

(..)

4.18 多主机系统

(..)

把47页图41和图42的例子中的DMB或者DSB指令去除掉不会导致任何错误,因为Cortex-M处理器:

  • 不会重新排序存储传输。
  • 不会允许两个写传输重叠。

这里图41中展示了在启动DMA传输前使用了一个DMB(存储屏障)指令。

在Cortex-M7内核的例子中,如果你使用了数据缓存(DCache),那么你需要存储屏障(DMB/DSB),除非你手动地无效化被DMA使用的缓存。即使将数据缓存取消掉,可能依然需要内存屏障以避免存储缓存中出现重新排序。

如果你的目标平台是一个多核系统,那么很可能你需要内存屏障。

如果你需要内存屏障,那么你需要使用atomic::fence而不是compiler_fence。这在Cortex-M设备上会生成一个DMB指令。

泛化缓存

我们的API太受限了。比如,下面的程序即使是有效的也不会被通过。

#![allow(unused)]
fn main() {
fn reuse(serial: Serial1, msg: &'static mut [u8]) {
    // send a message
    let t1 = serial.write_all(msg);

    // ..

    let (msg, serial) = t1.wait(); // `msg` is now `&'static [u8]`

    msg.reverse();

    // now send it in reverse
    let t2 = serial.write_all(msg);

    // ..

    let (buf, serial) = t2.wait();

    // ..
}
}

为了能接受这样的程序,我们可以让缓存参数更泛化点。

#![allow(unused)]
fn main() {
// as-slice = "0.1.0"
use as_slice::{AsMutSlice, AsSlice};

impl Serial1 {
    /// Receives data into the given `buffer` until it's filled
    ///
    /// Returns a value that represents the in-progress DMA transfer
    pub fn read_exact<B>(mut self, mut buffer: B) -> Transfer<B>
    where
        B: AsMutSlice<Element = u8>,
    {
        // NOTE: added
        let slice = buffer.as_mut_slice();
        let (ptr, len) = (slice.as_mut_ptr(), slice.len());

        self.dma.set_source_address(USART1_RX, false);

        // NOTE: tweaked
        self.dma.set_destination_address(ptr as usize, true);
        self.dma.set_transfer_length(len);

        atomic::compiler_fence(Ordering::Release);
        self.dma.start();

        Transfer {
            buffer,
            serial: self,
        }
    }

    /// Sends out the given `buffer`
    ///
    /// Returns a value that represents the in-progress DMA transfer
    fn write_all<B>(mut self, buffer: B) -> Transfer<B>
    where
        B: AsSlice<Element = u8>,
    {
        // NOTE: added
        let slice = buffer.as_slice();
        let (ptr, len) = (slice.as_ptr(), slice.len());

        self.dma.set_destination_address(USART1_TX, false);

        // NOTE: tweaked
        self.dma.set_source_address(ptr as usize, true);
        self.dma.set_transfer_length(len);

        atomic::compiler_fence(Ordering::Release);
        self.dma.start();

        Transfer {
            buffer,
            serial: self,
        }
    }
}

}

注意: 可以使用 AsRef<[u8]> (AsMut<[u8]>) 而不是 AsSlice<Element = u8> (AsMutSlice<Element = u8).

现在 reuse 程序可以通过了。

不可移动的缓存

这么修改后,API也可以通过值传递接受数组。(比如 [u8; 16])。然后,使用数组会导致指针无效化。考虑下面的程序。

#![allow(unused)]
fn main() {
fn invalidate(serial: Serial1) {
    let t = start(serial);

    bar();

    let (buf, serial) = t.wait();
}

#[inline(never)]
fn start(serial: Serial1) -> Transfer<[u8; 16]> {
    // array allocated in this frame
    let buffer = [0; 16];

    serial.read_exact(buffer)
}

#[inline(never)]
fn bar() {
    // stack variables
    let mut x = 0;
    let mut y = 0;

    // use `x` and `y`
}
}

read_exact 操作将使用位于 start 函数的 buffer 的地址。当 start 返回时,局部的 buffer 将会被释放,在 read_exact 中使用的指针将会变得无效化。你最后会遇到与 unsound 案例中一样的情况。

为了避免这个问题,我们要求我们的API使用的缓存即使当它被移动时依然保有它的内存区域。Pin 类型提供这样的保障。首先我们可以更新我们的API以要求所有的缓存都是 "pinned" 的。

注意: 要编译下面的所有程序,你的Rust需要 >=1.33.0。写这本书的时候 (2019-01-04) 这意味着要使用 nightly 版的Rust

#![allow(unused)]
fn main() {
/// A DMA transfer
pub struct Transfer<B> {
    // NOTE: changed
    buffer: Pin<B>,
    serial: Serial1,
}

impl Serial1 {
    /// Receives data into the given `buffer` until it's filled
    ///
    /// Returns a value that represents the in-progress DMA transfer
    pub fn read_exact<B>(mut self, mut buffer: Pin<B>) -> Transfer<B>
    where
        // NOTE: bounds changed
        B: DerefMut,
        B::Target: AsMutSlice<Element = u8> + Unpin,
    {
        // .. same as before ..
    }

    /// Sends out the given `buffer`
    ///
    /// Returns a value that represents the in-progress DMA transfer
    pub fn write_all<B>(mut self, buffer: Pin<B>) -> Transfer<B>
    where
        // NOTE: bounds changed
        B: Deref,
        B::Target: AsSlice<Element = u8>,
    {
        // .. same as before ..
    }
}
}

注意: 我们可以使用 StableDeref 特质而不是 Pin newtype but opted for Pin since it's provided in the standard library.

With this new API we can use &'static mut references, Box-ed slices, Rc-ed slices, etc.

#![allow(unused)]
fn main() {
fn static_mut(serial: Serial1, buf: &'static mut [u8]) {
    let buf = Pin::new(buf);

    let t = serial.read_exact(buf);

    // ..

    let (buf, serial) = t.wait();

    // ..
}

fn boxed(serial: Serial1, buf: Box<[u8]>) {
    let buf = Pin::new(buf);

    let t = serial.read_exact(buf);

    // ..

    let (buf, serial) = t.wait();

    // ..
}
}

'static bound

Does pinning let us safely use stack allocated arrays? The answer is no. Consider the following example.

#![allow(unused)]
fn main() {
fn unsound(serial: Serial1) {
    start(serial);

    bar();
}

// pin-utils = "0.1.0-alpha.4"
use pin_utils::pin_mut;

#[inline(never)]
fn start(serial: Serial1) {
    let buffer = [0; 16];

    // pin the `buffer` to this stack frame
    // `buffer` now has type `Pin<&mut [u8; 16]>`
    pin_mut!(buffer);

    mem::forget(serial.read_exact(buffer));
}

#[inline(never)]
fn bar() {
    // stack variables
    let mut x = 0;
    let mut y = 0;

    // use `x` and `y`
}
}

As seen many times before, the above program runs into undefined behavior due to stack frame corruption.

The API is unsound for buffers of type Pin<&'a mut [u8]> where 'a is not 'static. To prevent the problem we have to add a 'static bound in some places.

#![allow(unused)]
fn main() {
impl Serial1 {
    /// Receives data into the given `buffer` until it's filled
    ///
    /// Returns a value that represents the in-progress DMA transfer
    pub fn read_exact<B>(mut self, mut buffer: Pin<B>) -> Transfer<B>
    where
        // NOTE: added 'static bound
        B: DerefMut + 'static,
        B::Target: AsMutSlice<Element = u8> + Unpin,
    {
        // .. same as before ..
    }

    /// Sends out the given `buffer`
    ///
    /// Returns a value that represents the in-progress DMA transfer
    pub fn write_all<B>(mut self, buffer: Pin<B>) -> Transfer<B>
    where
        // NOTE: added 'static bound
        B: Deref + 'static,
        B::Target: AsSlice<Element = u8>,
    {
        // .. same as before ..
    }
}
}

现在有问题的程序将被拒绝。

析构函数

Now that the API accepts Box-es and other types that have destructors we need to decide what to do when Transfer is early-dropped.

Normally, Transfer values are consumed using the wait method but it's also possible to, implicitly or explicitly, drop the value before the transfer is over. For example, dropping a Transfer<Box<[u8]>> value will cause the buffer to be deallocated. This can result in undefined behavior if the transfer is still in progress as the DMA would end up writing to deallocated memory.

In such scenario one option is to make Transfer.drop stop the DMA transfer. The other option is to make Transfer.drop wait for the transfer to finish. We'll pick the former option as it's cheaper.

#![allow(unused)]
fn main() {
/// A DMA transfer
pub struct Transfer<B> {
    // NOTE: always `Some` variant
    inner: Option<Inner<B>>,
}

// NOTE: previously named `Transfer<B>`
struct Inner<B> {
    buffer: Pin<B>,
    serial: Serial1,
}

impl<B> Transfer<B> {
    /// Blocks until the transfer is done and returns the buffer
    pub fn wait(mut self) -> (Pin<B>, Serial1) {
        while !self.is_done() {}

        atomic::compiler_fence(Ordering::Acquire);

        let inner = self
            .inner
            .take()
            .unwrap_or_else(|| unsafe { hint::unreachable_unchecked() });
        (inner.buffer, inner.serial)
    }
}

impl<B> Drop for Transfer<B> {
    fn drop(&mut self) {
        if let Some(inner) = self.inner.as_mut() {
            // NOTE: this is a volatile write
            inner.serial.dma.stop();

            // we need a read here to make the Acquire fence effective
            // we do *not* need this if `dma.stop` does a RMW operation
            unsafe {
                ptr::read_volatile(&0);
            }

            // we need a fence here for the same reason we need one in `Transfer.wait`
            atomic::compiler_fence(Ordering::Acquire);
        }
    }
}

impl Serial1 {
    /// Receives data into the given `buffer` until it's filled
    ///
    /// Returns a value that represents the in-progress DMA transfer
    pub fn read_exact<B>(mut self, mut buffer: Pin<B>) -> Transfer<B>
    where
        B: DerefMut + 'static,
        B::Target: AsMutSlice<Element = u8> + Unpin,
    {
        // .. same as before ..

        Transfer {
            inner: Some(Inner {
                buffer,
                serial: self,
            }),
        }
    }

    /// Sends out the given `buffer`
    ///
    /// Returns a value that represents the in-progress DMA transfer
    pub fn write_all<B>(mut self, buffer: Pin<B>) -> Transfer<B>
    where
        B: Deref + 'static,
        B::Target: AsSlice<Element = u8>,
    {
        // .. same as before ..

        Transfer {
            inner: Some(Inner {
                buffer,
                serial: self,
            }),
        }
    }
}
}

Now the DMA transfer will be stopped before the buffer is deallocated.

#![allow(unused)]
fn main() {
fn reuse(serial: Serial1) {
    let buf = Pin::new(Box::new([0; 16]));

    let t = serial.read_exact(buf); // compiler_fence(Ordering::Release) ▲

    // ..

    // this stops the DMA transfer and frees memory
    mem::drop(t); // compiler_fence(Ordering::Acquire) ▼

    // this likely reuses the previous memory allocation
    let mut buf = Box::new([0; 16]);

    // .. do stuff with `buf` ..
}
}

总结

To sum it up, we need to consider all the following points to achieve memory safe DMA transfers:

  • Use immovable buffers plus indirection: Pin<B>. Alternatively, you can use the StableDeref trait.

  • The ownership of the buffer must be passed to the DMA : B: 'static.

  • Do not rely on destructors running for memory safety. Consider what happens if mem::forget is used with your API.

  • Do add a custom destructor that stops the DMA transfer, or waits for it to finish. Consider what happens if mem::drop is used with your API.


This text leaves out up several details required to build a production grade DMA abstraction, like configuring the DMA channels (e.g. streams, circular vs one-shot mode, etc.), alignment of buffers, error handling, how to make the abstraction device-agnostic, etc. All those aspects are left as an exercise for the reader / community (:P). Overlapping use

与编译器支持有关的笔记

这本书使用了一个内置的编译器目标,thumbv7m-none-eabi,Rust团队为其分发了一个 rust-std 组件,rust-std 是跟 corestd 一样的预先编译好的crates的集合。

如果你想尝试为一个不同的目标架构复制这本书的内容,你需要考虑下Rust为(编译)目标所提供的支持在什么级别。

LLVM 支持

自Rust 1.28以来,官方的Rust编译器,rustc,使用LLVM生成(机器)代码。Rust对某个架构提供的最小级别的支持是在rustc中让它的LLVM后端可用。通过运行下面的命令,你可以看到所有的 rustc 通过LLVM支持的架构:

$ # 运行这个命令,你需要安装`cargo-binutils`
$ cargo objdump -- -version
LLVM (http://llvm.org/):
  LLVM version 7.0.0svn
  Optimized build.
  Default target: x86_64-unknown-linux-gnu
  Host CPU: skylake

  Registered Targets:
    aarch64    - AArch64 (little endian)
    aarch64_be - AArch64 (big endian)
    arm        - ARM
    arm64      - ARM64 (little endian)
    armeb      - ARM (big endian)
    hexagon    - Hexagon
    mips       - Mips
    mips64     - Mips64 [experimental]
    mips64el   - Mips64el [experimental]
    mipsel     - Mipsel
    msp430     - MSP430 [experimental]
    nvptx      - NVIDIA PTX 32-bit
    nvptx64    - NVIDIA PTX 64-bit
    ppc32      - PowerPC 32
    ppc64      - PowerPC 64
    ppc64le    - PowerPC 64 LE
    sparc      - Sparc
    sparcel    - Sparc LE
    sparcv9    - Sparc V9
    systemz    - SystemZ
    thumb      - Thumb
    thumbeb    - Thumb (big endian)
    wasm32     - WebAssembly 32-bit
    wasm64     - WebAssembly 64-bit
    x86        - 32-bit X86: Pentium-Pro and above
    x86-64     - 64-bit X86: EM64T and AMD64

如果LLVM支持了你感兴趣的架构,但是构建的rustc没有使能其后端(自Rust 1.28以来AVR就是这样的情况),那么你将需要修改Rust源码以启用它。PR rust-lang/rust#52787 的前两个commits能让你知道需要做的改变。

换句话说,如果LLVM不支持这个架构,但是LLVM的一个分支支持,在编译rustc之前你将需要使用这个分支替代原来的LLVM 。Rust编译系统允许这么做且理论上,它应该只需要修改llvm的子模组让它指向那个分支就可以了。

如果只有一些供应商提供的GCC支持你的目标架构,你可以选择使用mrustc,一个非官方的Rust编译器,去把你的Rust程序翻译成C代码然后使用GCC编译它。

内置的目标

一个编译目标不仅是它的架构。每个目标都有一个与它关联的规范,其中描述了它的架构,它的操作系统和默认的链接器。

Rust编译器知道几个目标。这些被内置进编译器里且可以通过下列的命令被列出来:

$ rustc --print target-list | column
aarch64-fuchsia                   mipsisa32r6el-unknown-linux-gnu
aarch64-linux-android             mipsisa64r6-unknown-linux-gnuabi64
aarch64-pc-windows-msvc           mipsisa64r6el-unknown-linux-gnuabi64
aarch64-unknown-cloudabi          msp430-none-elf
aarch64-unknown-freebsd           nvptx64-nvidia-cuda
aarch64-unknown-hermit            powerpc-unknown-linux-gnu
aarch64-unknown-linux-gnu         powerpc-unknown-linux-gnuspe
aarch64-unknown-linux-musl        powerpc-unknown-linux-musl
aarch64-unknown-netbsd            powerpc-unknown-netbsd
aarch64-unknown-none              powerpc-wrs-vxworks
aarch64-unknown-none-softfloat    powerpc-wrs-vxworks-spe
aarch64-unknown-openbsd           powerpc64-unknown-freebsd
aarch64-unknown-redox             powerpc64-unknown-linux-gnu
aarch64-uwp-windows-msvc          powerpc64-unknown-linux-musl
aarch64-wrs-vxworks               powerpc64-wrs-vxworks
arm-linux-androideabi             powerpc64le-unknown-linux-gnu
arm-unknown-linux-gnueabi         powerpc64le-unknown-linux-musl
arm-unknown-linux-gnueabihf       riscv32i-unknown-none-elf
arm-unknown-linux-musleabi        riscv32imac-unknown-none-elf
arm-unknown-linux-musleabihf      riscv32imc-unknown-none-elf
armebv7r-none-eabi                riscv64gc-unknown-linux-gnu
armebv7r-none-eabihf              riscv64gc-unknown-none-elf
armv4t-unknown-linux-gnueabi      riscv64imac-unknown-none-elf
armv5te-unknown-linux-gnueabi     s390x-unknown-linux-gnu
armv5te-unknown-linux-musleabi    sparc-unknown-linux-gnu
armv6-unknown-freebsd             sparc64-unknown-linux-gnu
armv6-unknown-netbsd-eabihf       sparc64-unknown-netbsd
armv7-linux-androideabi           sparc64-unknown-openbsd
armv7-unknown-cloudabi-eabihf     sparcv9-sun-solaris
armv7-unknown-freebsd             thumbv6m-none-eabi
armv7-unknown-linux-gnueabi       thumbv7a-pc-windows-msvc
armv7-unknown-linux-gnueabihf     thumbv7em-none-eabi
armv7-unknown-linux-musleabi      thumbv7em-none-eabihf
armv7-unknown-linux-musleabihf    thumbv7m-none-eabi
armv7-unknown-netbsd-eabihf       thumbv7neon-linux-androideabi
armv7-wrs-vxworks-eabihf          thumbv7neon-unknown-linux-gnueabihf
armv7a-none-eabi                  thumbv7neon-unknown-linux-musleabihf
armv7a-none-eabihf                thumbv8m.base-none-eabi
armv7r-none-eabi                  thumbv8m.main-none-eabi
armv7r-none-eabihf                thumbv8m.main-none-eabihf
asmjs-unknown-emscripten          wasm32-unknown-emscripten
hexagon-unknown-linux-musl        wasm32-unknown-unknown
i586-pc-windows-msvc              wasm32-wasi
i586-unknown-linux-gnu            x86_64-apple-darwin
i586-unknown-linux-musl           x86_64-fortanix-unknown-sgx
i686-apple-darwin                 x86_64-fuchsia
i686-linux-android                x86_64-linux-android
i686-pc-windows-gnu               x86_64-linux-kernel
i686-pc-windows-msvc              x86_64-pc-solaris
i686-unknown-cloudabi             x86_64-pc-windows-gnu
i686-unknown-freebsd              x86_64-pc-windows-msvc
i686-unknown-haiku                x86_64-rumprun-netbsd
i686-unknown-linux-gnu            x86_64-sun-solaris
i686-unknown-linux-musl           x86_64-unknown-cloudabi
i686-unknown-netbsd               x86_64-unknown-dragonfly
i686-unknown-openbsd              x86_64-unknown-freebsd
i686-unknown-uefi                 x86_64-unknown-haiku
i686-uwp-windows-gnu              x86_64-unknown-hermit
i686-uwp-windows-msvc             x86_64-unknown-hermit-kernel
i686-wrs-vxworks                  x86_64-unknown-illumos
mips-unknown-linux-gnu            x86_64-unknown-l4re-uclibc
mips-unknown-linux-musl           x86_64-unknown-linux-gnu
mips-unknown-linux-uclibc         x86_64-unknown-linux-gnux32
mips64-unknown-linux-gnuabi64     x86_64-unknown-linux-musl
mips64-unknown-linux-muslabi64    x86_64-unknown-netbsd
mips64el-unknown-linux-gnuabi64   x86_64-unknown-openbsd
mips64el-unknown-linux-muslabi64  x86_64-unknown-redox
mipsel-unknown-linux-gnu          x86_64-unknown-uefi
mipsel-unknown-linux-musl         x86_64-uwp-windows-gnu
mipsel-unknown-linux-uclibc       x86_64-uwp-windows-msvc
mipsisa32r6-unknown-linux-gnu     x86_64-wrs-vxworks

使用下列的命令你可以打印出某个目标的规范:

$ rustc +nightly -Z unstable-options --print target-spec-json --target thumbv7m-none-eabi
{
  "abi-blacklist": [
    "stdcall",
    "fastcall",
    "vectorcall",
    "thiscall",
    "win64",
    "sysv64"
  ],
  "arch": "arm",
  "data-layout": "e-m:e-p:32:32-i64:64-v128:64:128-a:0:32-n32-S64",
  "emit-debug-gdb-scripts": false,
  "env": "",
  "executables": true,
  "is-builtin": true,
  "linker": "arm-none-eabi-gcc",
  "linker-flavor": "gcc",
  "llvm-target": "thumbv7m-none-eabi",
  "max-atomic-width": 32,
  "os": "none",
  "panic-strategy": "abort",
  "relocation-model": "static",
  "target-c-int-width": "32",
  "target-endian": "little",
  "target-pointer-width": "32",
  "vendor": ""
}

如果这些内置的目标都不适合你的目标系统,你将必须通过使用JSON格式编写你自己的目标规范来生成一个自制的目标,在下个章节中会讲到。

rust-std 组件

Rust团队通过rustup为某些内置的目标分发rust-std组件。这个组件是跟 corestd 一样的预先编译好的crates的集合,它要求交叉编译。

通过运行下列的命令,你可以找到具有一个rust-std组件的目标列表:

$ rustup target list | column
aarch64-apple-ios                       mipsel-unknown-linux-musl
aarch64-fuchsia                         nvptx64-nvidia-cuda
aarch64-linux-android                   powerpc-unknown-linux-gnu
aarch64-pc-windows-msvc                 powerpc64-unknown-linux-gnu
aarch64-unknown-linux-gnu               powerpc64le-unknown-linux-gnu
aarch64-unknown-linux-musl              riscv32i-unknown-none-elf
aarch64-unknown-none                    riscv32imac-unknown-none-elf
aarch64-unknown-none-softfloat          riscv32imc-unknown-none-elf
arm-linux-androideabi                   riscv64gc-unknown-linux-gnu
arm-unknown-linux-gnueabi               riscv64gc-unknown-none-elf
arm-unknown-linux-gnueabihf             riscv64imac-unknown-none-elf
arm-unknown-linux-musleabi              s390x-unknown-linux-gnu
arm-unknown-linux-musleabihf            sparc64-unknown-linux-gnu
armebv7r-none-eabi                      sparcv9-sun-solaris
armebv7r-none-eabihf                    thumbv6m-none-eabi
armv5te-unknown-linux-gnueabi           thumbv7em-none-eabi
armv5te-unknown-linux-musleabi          thumbv7em-none-eabihf
armv7-linux-androideabi                 thumbv7m-none-eabi
armv7-unknown-linux-gnueabi             thumbv7neon-linux-androideabi
armv7-unknown-linux-gnueabihf           thumbv7neon-unknown-linux-gnueabihf
armv7-unknown-linux-musleabi            thumbv8m.base-none-eabi
armv7-unknown-linux-musleabihf          thumbv8m.main-none-eabi
armv7a-none-eabi                        thumbv8m.main-none-eabihf
armv7r-none-eabi                        wasm32-unknown-emscripten
armv7r-none-eabihf                      wasm32-unknown-unknown
asmjs-unknown-emscripten                wasm32-wasi
i586-pc-windows-msvc                    x86_64-apple-darwin
i586-unknown-linux-gnu                  x86_64-apple-ios
i586-unknown-linux-musl                 x86_64-fortanix-unknown-sgx
i686-linux-android                      x86_64-fuchsia
i686-pc-windows-gnu                     x86_64-linux-android
i686-pc-windows-msvc                    x86_64-pc-windows-gnu
i686-unknown-freebsd                    x86_64-pc-windows-msvc
i686-unknown-linux-gnu                  x86_64-rumprun-netbsd
i686-unknown-linux-musl                 x86_64-sun-solaris
mips-unknown-linux-gnu                  x86_64-unknown-cloudabi
mips-unknown-linux-musl                 x86_64-unknown-freebsd
mips64-unknown-linux-gnuabi64           x86_64-unknown-linux-gnu (default)
mips64-unknown-linux-muslabi64          x86_64-unknown-linux-gnux32
mips64el-unknown-linux-gnuabi64         x86_64-unknown-linux-musl
mips64el-unknown-linux-muslabi64        x86_64-unknown-netbsd
mipsel-unknown-linux-gnu                x86_64-unknown-redox

如果你的目标没有rust-std组件,或者你正在使用一个自制的目标,那么你必须要使用一个开发版的工具链去构建标准库。看下一页,关于构建自制目标

制造一个自定义的目标

If a custom target triple is not available for your platform, you must create a custom target file that describes your target to rustc.

Keep in mind that it is required to use a nightly compiler to build the core library, which must be done for a target unknown to rustc.

Deciding on a target triple

Many targets already have a known triple used to describe them, typically in the form ARCH-VENDOR-SYS-ABI. You should aim to use the same triple that LLVM uses; however, it may differ if you need to specify additional information to Rust that LLVM does not know about. Although the triple is technically only for human use, it's important for it to be unique and descriptive especially if the target will be upstreamed in the future.

The ARCH part is typically just the architecture name, except in the case of 32-bit ARM. For example, you would probably use x86_64 for those processors, but specify the exact ARM architecture version. Typical values might be armv7, armv5te, or thumbv7neon. Take a look at the names of the built-in targets for inspiration.

The VENDOR part is optional and describes the manufacturer. Omitting this field is the same as using unknown.

The SYS part describes the OS that is used. Typical values include win32, linux, and darwin for desktop platforms. none is used for bare-metal usage.

The ABI part describes how the process starts up. eabi is used for bare metal, while gnu is used for glibc, musl for musl, etc.

Now that you have a target triple, create a file with the name of the triple and a .json extension. For example, a file describing armv7a-none-eabi would have the filename armv7a-none-eabi.json.

Fill the target file

The target file must be valid JSON. There are two places where its contents are described: Target, where every field is mandatory, and TargetOptions, where every field is optional. All underscores are replaced with hyphens.

The recommended way is to base your target file on the specification of a built-in target that's similar to your target system, then tweak it to match the properties of your target system. To do so, use the command rustc +nightly -Z unstable-options --print target-spec-json --target $SOME_SIMILAR_TARGET, using a target that's already built into the compiler.

You can pretty much copy that output into your file. Start with a few modifications:

  • Remove "is-builtin": true
  • Fill llvm-target with the triple that LLVM expects
  • Decide on a panicking strategy. A bare metal implementation will likely use "panic-strategy": "abort". If you decide not to abort on panicking, unless you tell Cargo to per-project, you must define an eh_personality function.
  • Configure atomics. Pick the first option that describes your target:
    • I have a single-core processor, no threads, no interrupts, or any way for multiple things to be happening in parallel: if you are sure that is the case, such as WASM (for now), you may set "singlethread": true. This will configure LLVM to convert all atomic operations to use their single threaded counterparts. Incorrectly using this option may result in UB if using threads or interrupts.
    • I have native atomic operations: set max-atomic-width to the biggest type in bits that your target can operate on atomically. For example, many ARM cores have 32-bit atomic operations. You may set "max-atomic-width": 32 in that case.
    • I have no native atomic operations, but I can emulate them myself: set max-atomic-width to the highest number of bits that you can emulate up to 128, then implement all of the atomic and sync functions expected by LLVM as #[no_mangle] unsafe extern "C". These functions have been standardized by gcc, so the gcc documentation may have more notes. Missing functions will cause a linker error, while incorrectly implemented functions will possibly cause UB. For example, if you have a single-core, single-thread processor with interrupts, you can implement these functions to disable interrupts, perform the regular operation, and then re-enable them.
    • I have no native atomic operations: you'll have to do some unsafe work to manually ensure synchronization in your code. You must set "max-atomic-width": 0.
  • Change the linker if integrating with an existing toolchain. For example, if you're using a toolchain that uses a custom build of gcc, set "linker-flavor": "gcc" and linker to the command name of your linker. If you require additional linker arguments, use pre-link-args and post-link-args as so:
    "pre-link-args": {
        "gcc": [
            "-Wl,--as-needed",
            "-Wl,-z,noexecstack",
            "-m64"
        ]
    },
    "post-link-args": {
        "gcc": [
            "-Wl,--allow-multiple-definition",
            "-Wl,--start-group,-lc,-lm,-lgcc,-lstdc++,-lsupc++,--end-group"
        ]
    }
    
    Ensure that the linker type is the key within link-args.
  • Configure LLVM features. Run llc -march=ARCH -mattr=help where ARCH is the base architecture (not including the version in the case of ARM) to list the available features and their descriptions. If your target requires strict memory alignment access (e.g. armv5te), make sure that you enable strict-align. To enable a feature, place a plus before it. Likewise, to disable a feature, place a minus before it. Features should be comma separated like so: "features": "+soft-float,+neon. Note that this may not be necessary if LLVM knows enough about your target based on the provided triple and CPU.
  • Configure the CPU that LLVM uses if you know it. This will enable CPU-specific optimizations and features. At the top of the output of the command in the last step, there is a list of known CPUs. If you know that you will be targeting a specific CPU, you may set it in the cpu field in the JSON target file.

使用目标文件

一旦你有一个目标规范文件,你可能 Once you have a target specification file, you may refer to it by its path or by its name (i.e. excluding .json) if it is in the current directory or in $RUST_TARGET_PATH.

Verify that it is readable by rustc:

❱ rustc --print cfg --target foo.json # or just foo if in the current directory
debug_assertions
target_arch="arm"
target_endian="little"
target_env=""
target_feature="mclass"
target_feature="v7"
target_has_atomic="16"
target_has_atomic="32"
target_has_atomic="8"
target_has_atomic="cas"
target_has_atomic="ptr"
target_os="none"
target_pointer_width="32"
target_vendor=""

Now, you finally get to use it! Many resources have been recommending xargo or cargo-xbuild. However, its successor, cargo's build-std feature, has received a lot of work recently and has quickly reached feature parity with the other options. As such, this guide will only cover that option.

Start with a bare minimum no_std program. Now, run cargo build -Z build-std=core --target foo.json, again using the above rules about referencing the path. Hopefully, you should now have a binary in the target directory.

You may optionally configure cargo to always use your target. See the recommendations at the end of the page about the smallest no_std program. However, you'll currently have to use the flag -Z build-std=core as that option is unstable.

Build additional built-in crates

When using cargo's build-std feature, you can choose which crates to compile in. By default, when only passing -Z build-std, std, core, and alloc are compiled. However, you may want to exclude std when compiling for bare-metal. To do so, specify the crated you'd like after build-std. For example, to include core and alloc, pass -Z build-std=core,alloc.

Troubleshooting

language item required, but not found: eh_personality

Either add "panic-strategy": "abort" to your target file, or define an eh_personality function. Alternatively, tell Cargo to ignore it.

undefined reference to __sync_val_compare_and_swap_#

Rust thinks that your target has atomic instructions, but LLVM doesn't. Go back to the step about configuring atomics. You will need to reduce the number in max-atomic-width. See #58500 for more details.

could not find sync in alloc

Similar to the above case, Rust doesn't think that you have atomics. You must implement them yourself or tell Rust that you have atomic instructions.

multiple definition of __(something)

You're likely linking your Rust program with code built from another language, and the other language includes compiler built-ins that Rust also creates. To fix this, you'll need to tell your linker to allow multiple definitions. If using gcc, you may add:

"post-link-args": {
    "gcc": [
        "-Wl,--allow-multiple-definition"
    ]
}

error adding symbols: file format not recognized

Switch to cargo's build-std feature and update your compiler. This was a bug introduced for a few compiler builds that tried to pass in internal Rust object to an external linker.