稳定的汇编

注意: 自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工件。