安定版でのアセンブリ
注: 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>
次に、ビルドスクリプトを変更して、このアーカイブを 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/release.objdump
注: 前と同様に、逆アセンブル結果を短くするため、RAM の初期化を コメントアウトしています。
$ cargo objdump --bin app --release -- -s -j .vector_table
../app/release.vector_table
事前にアセンブルしたアーカイブを同梱する欠点は、最悪の場合、ライブラリが サポートする各コンパイルターゲットごとに 1 つのビルド成果物を同梱する必要が あることです。