Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

安定版でのアセンブリ

注: Rust 1.59 以降、インラインアセンブリ (asm!) と 自由形式アセンブリ (global_asm!) はどちらも安定化されています。しかし、既存のクレートがこの 変更に追いつくにはしばらく時間がかかること、また、私たちが歴史的にアセンブリを 扱ってきた別の方法を知っておくのも有益であることから、この章は残しておきます。

これまでのところ、アセンブリを 1 行も書かずにデバイスをブートし、割り込みを処理 してきました。これはかなりの偉業です! しかし、対象としているアーキテクチャに よっては、ここまで到達するためにいくらかのアセンブリが必要になるかもしれません。 また、たとえばコンテキストスイッチのように、アセンブリを必要とする操作もあります。

インラインアセンブリ (asm!) と 自由形式アセンブリ (global_asm!) は どちらも Rust 1.59 より前は不安定でした。通常、クレートでは global_asm!asm! を使うことになるでしょう。ただし、この章では別のアプローチも紹介します。

この節の導入として、例外を発生させたスタックフレームに関する情報を提供するように HardFault ハンドラを少し変更してみます。

やりたいことは次のとおりです:

ユーザーが自分の HardFault ハンドラを直接ベクタテーブルに置くのではなく、 rt クレートがユーザー定義の HardFault ハンドラへのトランポリンを ベクタテーブルに配置するようにします。

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

#[unsafe(link_section = ".vector_table.exceptions")]
#[unsafe(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 },
];

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

このトランポリンはスタックポインタを読み取り、その後でユーザーの HardFault ハンドラを呼び出します。このトランポリンはアセンブリで記述する必要があります:

  mrs r0, MSP
  b HardFault

ARM ABI の仕組みにより、これによって Main Stack Pointer (MSP) が HardFault 関数 / ルーチンの第 1 引数として設定されます。この MSP の値は、例外によって スタックにプッシュされたレジスタを指すポインタにもなっています。これらの変更に より、ユーザーの HardFault ハンドラは fn(&StackedRegisters) -> ! というシグネチャでなければなりません。

.s ファイル

安定版でアセンブリを扱う 1 つの方法は、アセンブリを外部ファイルに書くことです:

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

そして、rt クレートのビルドスクリプトで cc クレートを使い、そのファイルを オブジェクトファイル (.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)]
#[unsafe(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:

00000040 <HardFault>:
      40:      	push	{r7, lr}
      42:      	mov	r7, sp
      44:      	b	0x44 <HardFault+0x4>    @ imm = #-0x4

00000046 <main>:
      46:      	push	{r7, lr}
      48:      	mov	r7, sp
      4a:      	b	0x4a <main+0x4>         @ imm = #-0x4

0000004c <Reset>:
      4c:      	push	{r7, lr}
      4e:      	mov	r7, sp
      50:      	bl	0x46 <main>             @ imm = #-0xe

00000054 <UsageFault>:
      54:      	push	{r7, lr}
      56:      	mov	r7, sp
      58:      	b	0x58 <UsageFault+0x4>   @ imm = #-0x4

0000005a <HardFaultTrampoline>:
      5a:      	mrs	r0, msp
      5e:      	b	0x40 <HardFault>        @ imm = #-0x22

注: この逆アセンブル結果を短くするため、RAM の初期化はコメントアウト しています。

次にベクタテーブルを見てください。4 番目のエントリは HardFaultTrampoline のアドレスに 1 を足した値になっているはずです。

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

app:	file format elf32-littlearm
Contents of section .vector_table:
 0000 00000120 4d000000 55000000 5b000000  ... M...U...[...
 0010 55000000 55000000 55000000 00000000  U...U...U.......
 0020 00000000 00000000 00000000 55000000  ............U...
 0030 00000000 00000000 55000000 55000000  ........U...U...

.o / .a ファイル

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

ビルドマシン上でそのファイルをアセンブルする代わりに、事前にアセンブルした ファイルを rt クレートとともに配布することもできます。そうすれば、ビルド マシン上ではアセンブラプログラムは不要になります。ただし、クレートを パッケージ化して公開するマシンでは、やはりアセンブラが必要です。

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

Cargo は、クレートにアーカイブ (.a) を同梱するためのサポートを提供してい ます。ar コマンドを使ってオブジェクトファイルをアーカイブにまとめ、その アーカイブをクレートに同梱できます。実際、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>

次に、ビルドスクリプトを変更して、このアーカイブを rtrlib に同梱し ます。

$ 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/release.objdump

: 前と同様に、逆アセンブル結果を短くするため、RAM の初期化を コメントアウトしています。

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

事前にアセンブルしたアーカイブを同梱する欠点は、最悪の場合、ライブラリが サポートする各コンパイルターゲットごとに 1 つのビルド成果物を同梱する必要が あることです。