Assembly on stable
Note: Since Rust 1.59, both inline assembly (
asm!
) and free form assembly (global_asm!
) become stable. But since it will take some time for the existing crates to catchup the change, and since it's good for us to know the other ways in history we used to deal with assembly, we will keep this chapter here.
So far we have managed to boot the device and handle interrupts without a single line of assembly. That's quite a feat! But depending on the architecture you are targeting you may need some assembly to get to this point. There are also some operations like context switching that require assembly, etc.
The problem is that both inline assembly (asm!
) and free form assembly
(global_asm!
) are unstable, and there's no estimate for when they'll be
stabilized, so you can't use them on stable . This is not a showstopper because
there are some workarounds which we'll document here.
To motivate this section we'll tweak the HardFault
handler to provide
information about the stack frame that generated the exception.
Here's what we want to do:
Instead of letting the user directly put their HardFault
handler in the vector
table we'll make the rt
crate put a trampoline to the user-defined HardFault
handler in the vector table.
$ 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 {} } }
This trampoline will read the stack pointer and then call the user HardFault
handler. The trampoline will have to be written in assembly:
mrs r0, MSP
b HardFault
Due to how the ARM ABI works this sets the Main Stack Pointer (MSP) as the first
argument of the HardFault
function / routine. This MSP value also happens to
be a pointer to the registers pushed to the stack by the exception. With these
changes the user HardFault
handler must now have signature
fn(&StackedRegisters) -> !
.
.s
files
One approach to stable assembly is to write the assembly in an external file:
$ cat ../rt/asm.s
.section .text.HardFaultTrampoline
.global HardFaultTrampoline
.thumb_func
HardFaultTrampoline:
mrs r0, MSP
b HardFault
And use the cc
crate in the build script of the rt
crate to assemble that
file into an object file (.o
) and then into an archive (.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"
And that's it!
We can confirm that the vector table contains a pointer to HardFaultTrampoline
by writing a very simple program.
#![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 {} }
Here's the disassembly. Look at the address of 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
NOTE: To make this disassembly smaller I commented out the initialization of RAM
Now look at the vector table. The 4th entry should be the address of
HardFaultTrampoline
plus one.
$ 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
files
The downside of using the cc
crate is that it requires some assembler program
on the build machine. For example when targeting ARM Cortex-M the cc
crate
uses arm-none-eabi-gcc
as the assembler.
Instead of assembling the file on the build machine we can ship a pre-assembled
file with the rt
crate. That way no assembler program is required on the build
machine. However, you would still need an assembler on the machine that packages
and publishes the crate.
There's not much difference between an assembly (.s
) file and its compiled
version: the object (.o
) file. The assembler doesn't do any optimization; it
simply chooses the right object file format for the target architecture.
Cargo provides support for bundling archives (.a
) with crates. We can package
object files into an archive using the ar
command and then bundle the archive
with the crate. In fact, this what the cc
crate does; you can see the commands
it invoked by searching for a file named output
in the target
directory.
$ 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
We'll do something similar to produce an archive.
$ # most of flags `cc` uses have no effect when assembling so we drop them
$ 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>
Next we modify the build script to bundle this archive with the 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(()) }
Now we can test this new version against the simple program from before and we'll get the same output.
$ 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
NOTE: As before I have commented out the RAM initialization to make the disassembly smaller.
$ 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...
The downside of shipping pre-assembled archives is that, in the worst case scenario, you'll need to ship one build artifact for each compilation target your library supports.