stableでのアセンブリ

ここまで、デバイスの起動と割り込み処理とを、1行のアセンブリも書くことなくうまくやって来ました。 これはかなりの偉業です!しかし、ターゲットアーキテクチャ次第では、 ここまで到達するためにアセンブリが必要になるかもしれません。 他にも、コンテキストスイッチのようなアセンブリを必要とする操作があります。

問題は、インラインアセンブリ(asm!)も自由形式アセンブリ(global_asm!)もunstableなことです。 そして、これらがいつ安定化されるかは分かっていないため、stableでは使えません。 これから説明するように、いくつかのワークアラウンドがあるため、致命的な問題ではありません。

本セクションの動機付けとして、HardFaultハンドラを、 例外を発生させたスタックフレームの情報を提供するように修正します。

やりたいことは下記の通りです。

ベクタテーブルにユーザーがHardFaultハンドラを直接配置する代わりに、 rtクレートがユーザー定義のHardFaultをトランポリンするハンドラをベクタテーブルに配置します。

$ tail -n36 ../rt/src/lib.rs

#![allow(unused)]
fn main() {
extern "C" {
    fn NMI();
    fn HardFaultTrampoline(); // <- 変更点!
    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 }, // <- 変更点!
    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; Main Stack Pointer)の設定は、HardFault関数/ルーチンの第一引数になります。 このMSPの値は、例外によってスタックにプッシュされたレジスタへのポインタです。 これらの変更により、ユーザーのHardFaultハンドラは、fn(&StackedRegisters) -> !というシグネチャを持たなければなりません。

.sファイル

stableでアセンブリを書く方法の1つは、アセンブリを外部ファイルに書くことです。

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

そして、rtクレートのビルドスクリプト内で、アセンブリファイルをオブジェクトファイル(.o)にアセンブルし、 アーカイブ(.a)にするために、ccクレートを使います。

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

use cc::Build;

fn main() -> Result<(), Box<Error>> {
    // このクレートのビルドディレクトリです
    let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());

    // ライブラリサーチパスを追加します
    println!("cargo:rustc-link-search={}", out_dir.display());

    // `link.x`をビルドディレクトリに置きます
    File::create(out_dir.join("link.x"))?.write_all(include_bytes!("link.x"))?;

    // `asm.s`ファイルをアセンブルします
    Build::new().file("asm.s").compile("asm"); // <- 追加!

    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-arm-little

Disassembly of section .text:
HardFault:
	b	#-0x4 <HardFault>

main:
	trap

Reset:
	bl	#-0x6
	trap

DefaultExceptionHandler:
	b	#-0x4 <DefaultExceptionHandler>

UsageFault:
 <unknown>

HardFaultTrampoline:
	mrs	r0, msp
	b	#-0x14 <HardFault>

注記 この逆アセンブリ結果を小さくするために、RAMの初期化をコメントアウトしています。

ここで、ベクタテーブルを見ます。 4つ目のエントリは、HardFaultTrampolineに1を足したアドレスになっているはずです。

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

app:	file format ELF32-arm-little

Contents of section .vector_table:
 0000 00000120 45000000 4b000000 4d000000  ... E...K...M...
 0010 4b000000 4b000000 4b000000 00000000  K...K...K.......
 0020 00000000 00000000 00000000 4b000000  ............K...
 0030 00000000 00000000 4b000000 4b000000  ........K...K...

.o / .aファイル

ccクレートを使う欠点は、ビルドマシンにアセンブラプログラムが必要なことです。 例えば、ARM Cortex-Mをターゲットにする時、ccクレートはアセンブラとしてarm-none-eabi-gccを使います。

ビルドマシン上でファイルをアセンブルする代わりに、rtクレートと一緒にあらかじめアセンブルしたファイルを配布できます。 この方法なら、ビルドマシンにアセンブラプログラムは必要ありません。 しかしながら、rtクレートをパッケージして発行するマシン上には、アセンブラが必要です。

アセンブリファイル(.s)と、コンパイルしたオブジェクトファイル(.o)とは、それほど違いがありません。 アセンブラは最適化を行いません。単純にターゲットアーキテクチャ向けに正しいオブジェクトファイル形式を選ぶだけです。

Cargoは、クレートとアーカイブ(.a)をまとめる機能を提供しています。arコマンドを使ってオブジェクトファイルをアーカイブにパッケージできます。 その後、アーカイブをクレートにまとめます。実は、これはccクレートが行っていることなのです。 ccクレートが呼び出しているコマンドは、targetディレクトリのoutputという名前のファイルを探すと見つかります。

$ 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>

次に、rt rlibにアーカイブをまとめるために、ビルドスクリプトを修正します。

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

fn main() -> Result<(), Box<Error>> {
    // このクレートのビルドディレクトリです
    let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());

    // ライブラリサーチパスを追加します
    println!("cargo:rustc-link-search={}", out_dir.display());

    // `link.x`をビルドディレクトリに置きます
    File::create(out_dir.join("link.x"))?.write_all(include_bytes!("link.x"))?;

    // `librt.a`にリンクします
    fs::copy("librt.a", out_dir.join("librt.a"))?; // <- 追加!
    println!("cargo:rustc-link-lib=static=rt"); // <- 追加!

    Ok(())
}

ここで、新バージョンが前のシンプルなプログラムと同じ出力をすることをテストできます。

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

app:	file format ELF32-arm-little

Disassembly of section .text:
HardFault:
	b	#-0x4 <HardFault>

main:
	trap

Reset:
	bl	#-0x6
	trap

DefaultExceptionHandler:
	b	#-0x4 <DefaultExceptionHandler>

UsageFault:
 <unknown>

HardFaultTrampoline:
	mrs	r0, msp
	b	#-0x14 <HardFault>

注記 前回同様、逆アセンブリの結果を小さくするために、RAMの初期化をコメントアウトしています。

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

app:	file format ELF32-arm-little

Contents of section .vector_table:
 0000 00000120 45000000 4b000000 4d000000  ... E...K...M...
 0010 4b000000 4b000000 4b000000 00000000  K...K...K.......
 0020 00000000 00000000 00000000 4b000000  ............K...
 0030 00000000 00000000 4b000000 4b000000  ........K...K...

あらかじめアセンブルしたアーカイブを配布する欠点は、最悪の場合、 ライブラリがサポートするターゲットごとにビルド生成物を配布しないといけないことです。