Rust The Embedonomiconをやる②
tomo-wait-for-it-yuki.hatenablog.com
Rust The Embedonomiconの続きです。
前回記事では、下記2つの項目を実施しました。
- The smallest
#![no_std]
program - Memory layout
A main interfaceから続きをやっていきます。
A main interface
まず、binary crateからlibrary crateに変更します。
これまで作ったものを、別アプリケーションから利用できるようにします。
$ mv src/main.rs src/lib.rs
Cargo.tomlのアプリケーション名をapp
からrt
(runtime)に変えます。
[package] edition = "2018" name = "rt" # <- version = "0.1.0"
src/lib.rs
を次のように修正します。
#![no_std] use core::panic::PanicInfo; // CHANGED! #[no_mangle] pub unsafe extern "C" fn Reset() -> ! { extern "Rust" { fn main() -> !; } main() }
#![no_std]
はlibrary crateでは効果がないようです。通常、ライブラリにはエントリーポイントないですからね。
エントリーポイントが作れるライブラリ形式もありますが。
build scriptを作成して、このpackageをビルドする前に実行する処理を実装します。
build.rs
use std::{env, error::Error, fs::File, io::Write, path::PathBuf}; fn main() -> Result<(), Box<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"))?; Ok(()) }
これで、library crateにあるリンカスクリプトが、このcrateに依存するアプリケーションのビルドディレクトリにコピーされます。
後ほど、アプリケーションをビルドする際に、リンカスクリプトがコピーされていることを確認します。
ここまで作ってきたrt
crateを利用するアプリケーションを作成します。
$ cd .. $ mv app rt $ cargo new --edition 2018 --bin app $ cd app
Cargo.toml
にrt
crateへの依存を追加します。
[dependencies] rt = { path = "../rt" }
.cargo/config
をrt
crateから持ってきます。
$ cp -r ../rt/.cargo .
src/main/,rs
を次のように実装します。
#![no_std] #![no_main] extern crate rt; #[no_mangle] pub fn main() -> ! { let _x = 42; loop {} }
ビルドして、バイナリを確認します。
$ cargo build $ cargo objdump --bin app -- -d -no-show-raw-insn app: file format ELF32-arm-little Disassembly of section .text: main: 8: sub sp, #4 a: movs r0, #42 c: str r0, [sp] e: b #-2 <main+0x8> 10: b #-4 <main+0x8> Reset: 12: bl #-14 16: trap
同様のバイナリができていますね。
$ find . -name link.x ./target/thumbv7m-none-eabi/debug/build/rt-f9fe211895384bc9/out/link.x $ cat ./target/thumbv7m-none-eabi/debug/build/rt-f9fe211895384bc9/out/link.x /* Memory layout of the LM3S6965 microcontroller */ /* 1K = 1 KiBi = 1024 bytes */ MEMORY { FLASH : ORIGIN = 0x00000000, LENGTH = 256K RAM : ORIGIN = 0x20000000, LENGTH = 64K } /* The entry point is the reset handler */ ENTRY(Reset); 以下略
rt
crateにあるlink.x
もビルドディレクトリにコピーされていますね。
Making it type safe
ベアメタルなプログラムでは、エントリーポイントから戻る、という動作は通常行いません。
現在のrt
crateの実装では、アプリケーションが、main関数からreturnするようなプログラムを書いても、型チェックできません。
そこで、エントリーポイント関数の型チェックができるようにmacroを導入します。
rt
crateの
src/lib.rs`に次のmacroを追加します。
#[macro_export] macro_rules! entry { ($path:path) => { #[export_name = "main"] pub unsafe fn __main() -> ! { // type check the given path let f fn() -> ! = $path; f() } } }
app
crateからは次のように使います。
#![no_std] #![no_main] use rt::entry; entry!(main); fn main() -> ! { let _x = 42; loop {} }
ここで、main
は、let f fn() -> ! = $path;
を満たす型である必要があるわけです。すごい。賢い!
少し実装を間違っていて、次のようなエラーが発生しました。同様のエラーが発生した方は、main()に#![no_mangle]
アトリビュートがついたままになっていないか、確認してみて下さい。
mangleしないと、main symbolが複数できてしまう、という状態になります。
error: symbol `main` is already defined --> src/main.rs:6:1 | 6 | entry!(main); | ^^^^^^^^^^^^^ | = note: this error originates in a macro outside of the current crate (in Nightly builds, run with -Z external-macro-backtrace for more info)
Life before main
rt
crateのリンカスクリプトに.bssや.data、.rodataセクションがないので、それを補っていきます。
link.x
.text : { *(.text .text.*); } > FLASH /* NEW! */ .rodata : { *(.rodata .rodata.*); } > FLASH .bss : { *(.bss .bss.*); } > RAM .data : { *(.data .data.*); } > RAM /DISCARD/ :
これで、app
側では、次のようなコードが書けます。
#![no_std] #![no_main] use rt::entry; entry!(main); static RODATA: &[u8] = b"Hello, world!"; static mut BSS: u8 = 0; static mut DATA: u16 = 1; fn main() -> ! { let _x = RODATA; let _y = unsafe { &BSS }; let _z = unsafe { &DATA }; loop {} }
これで良いように思えますが、実際のハードウェアでは、RAMの初期値はランダムになるため、うまく動きません。qemuでは同様の現象は発生しません。
ということで、mainに制御を移す前に、static変数を初期化する処理を書きます。
まず、リンカスクリプトを修正します。
.bss : { _sbss = .; *(.bss .bss.*); _ebss = .; } > RAM .data : AT(ADDR(.rodata) + SIZEOF(.rodata)) { _sdata = .; *(.data .data.*); _edata = .; } > RAM _sidata = LOADADDR(.data);
あとでRustで使うためのsymbolをリンカスクリプトに追加します。
rt
crateのlink.x
で、.bssおよび.dataセクションの開始アドレスと終了アドレスにsymbol (_sbss, _ebss, _sdata, _edata
)を作っていますね。
static変数の初期値を、Flashに格納するため、Load Memory Address (LMA)を設定しています。
これで、.rodataセクションに続いて、static変数の初期値が配置されます。
static変数自体は、RAMの.dataセクションのいずれかのアドレスに位置することになります。
Rustから.bssセクションを0で埋め、.dataセクションを初期化します。
rt
crateのsrc/lib.rs
のReset関数を次のように修正します。
use core::ptr; #[no_mangle] pub unsafe extern "C" fn Reset() -> ! { // Initialize RAM extern "C" { static mut _sbss: u8; static mut _ebss: u8; static mut _sdata: u8; static mut _edata: u8; static _sidata: u8; } let count = &_ebss as *const u8 as usize - &_sbss as *const u8 as usize; ptr::write_bytes(&mut _sbss as *mut u8, 0, count); let count = &_edata as *const u8 as usize - &_sdata as *const u8 as usize; ptr::copy_nonoverlapping(&_sidata as *const u8, &mut _sdata as *mut u8, count); extern "Rust" { fn main() -> !; } main() }
これでRAM上のstatic変数が、Flashの初期値で初期化されるわけですね。勉強になりますね。
Exception handling
Background information
例外の一般的な話の後、stable Rustでデフォルト機能を提供しつつ、ユーザーがコンパイル時に割り込みハンドラをoverrideする仕組みを作っていく、ということが書かれています。
Rust side
Cortex-Mで共通となるデバイスに依存しない最初の16個のハンドラを実装していきます。
rt/src/lib.rs
に割り込みベクタを定義します。
pub union Vector { reserved: u32, handler: unsafe extern "C" fn(), } extern "C" { fn NMI(); fn HardFault(); 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: HardFault }, 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 }, ];
ARMの仕様書では、reservedのベクタテーブルは0
になっていないといけないようです。
そのため、unionを使って、確実に0にできるようにします。
ユーザーがハンドラを書き換えできるように、externしておきます。
次にrt/src/lib.rs
にデフォルトハンドラを実装します。
#[no_mangle] pub extern "C" fn DefaultExceptionHandler() { loop {} }
Linker script side
例外ベクタをリセットベクタの後に配置します。
rt/link.x
EXTERN(RESET_VECTOR); EXTERN(EXCEPTIONS); /* <- NEW */ SECTIONS { .vector_table ORIGIN(FLASH) : { /* First entry: initial Stack Pointer value */ LONG(ORIGIN(RAM) + LENGTH(RAM)); /* Second entry: reset vector */ KEEP(*(.vector_table.reset_vector)); /* The next 14 entries are exception vectors */ KEEP(*(.vector_table.exceptions)); /* <- NEW */ } > FLASH
rt/link.x
にPROVIDE
を使ってデフォルトハンドラを提供します。
PROVIDE(NMI = DefaultExceptionHandler); PROVIDE(HardFault = DefaultExceptionHandler); PROVIDE(MemManage = DefaultExceptionHandler); PROVIDE(BusFault = DefaultExceptionHandler); PROVIDE(UsageFault = DefaultExceptionHandler); PROVIDE(SVCall = DefaultExceptionHandler); PROVIDE(PendSV = DefaultExceptionHandler); PROVIDE(SysTick = DefaultExceptionHandler);
PROVIDE
は、入力されたオブジェクトファイルを検査した後、symbolが未定義の場合に有効になるようです。初めて知りました。
Testing it
例外のテストをするために、trap命令を使うようです。ただ、stable Rustでは使えないようで、一時的にnightly Rustを使用します。
$ rustup override add nightly $ rustup target add thumbv7m-none-eabi
とりあえず、nightlyに切り替えて、targetを追加しておきます。
app/src/main.rs
を次のように変更します。
#![feature(core_intrinsics)] #![no_main] #![no_std] use core::intrinsics; use rt::entry; entry!(main); fn main() -> ! { // this executes the undefined instruction (UDF) and causes a HardFault exception unsafe { intrinsics::abort() } }
なるほど、UDFを実行させるため、nightlyの機能が必要なわけですね。
$ cargo build $ qemu-system-arm \ -cpu cortex-m3 \ -machine lm3s6965evb \ -gdb tcp::3333 \ -S \ -nographic \ -kernel target/thumbv7m-none-eabi/debug/app # 別ターミナル $ arm-none-eabi-gdb -q target/thumbv7m-none-eabi/debug/app (gdb) target remote :3333 Remote debugging using :3333 Reset () at ../rt/src/lib.rs:7 7 pub unsafe extern "C" fn Reset() -> ! { (gdb) b DefaultExceptionHandler Breakpoint 1 at 0xec: file ../rt/src/lib.rs, line 95. (gdb) continue Continuing. Breakpoint 1, DefaultExceptionHandler () at ../rt/src/lib.rs:95 95 loop {} (gdb) list 90 Vector { handler: SysTick }, 91 ]; 92 93 #[no_mangle] 94 pub extern "C" fn DefaultExceptionHandler() { 95 loop {} 96 }
最適化されたバイナリを逆アセンブルすると、次のようになっています。
$ cargo objdump --bin app --release -- -d -no-show-raw-insn -print-imm-hex app: file format ELF32-arm-little Disassembly of section .text: main: 40: trap 42: trap Reset: 44: movw r1, #0x0 48: movw r0, #0x0 4c: movt r1, #0x2000 50: movt r0, #0x2000 54: subs r1, r1, r0 56: bl #0xd2 5a: movw r1, #0x0 5e: movw r0, #0x0 62: movt r1, #0x2000 66: movt r0, #0x2000 6a: subs r2, r1, r0 6c: movw r1, #0x0 70: movt r1, #0x0 74: bl #0x8 78: bl #-0x3c 7c: trap DefaultExceptionHandler: 7e: b #-0x4 <DefaultExceptionHandler>
$ cargo objdump --bin app --release -- -s -j .vector_table app: file format ELF32-arm-little Contents of section .vector_table: 0000 00000120 45000000 7f000000 7f000000 ... E........... 0010 7f000000 7f000000 7f000000 00000000 ................ 0020 00000000 00000000 00000000 7f000000 ................ 0030 00000000 00000000 7f000000 7f000000 ................
ARMでは、アドレスの最下位ビットに1が立っている関数は、thumb modeで、実行されます。
.vector_table内の、0x0000_0045は、Resetの0x44をthumb modeで、0x0000_007fは、DefaultExceptionHandlerの0x7eをthumb modeで実行する、となります。
Overriding a handler
アプリケーション側から、ハンドラを上書きします。
app/src/main.rs
#[no_mangle] pub extern "C" fn HardFault() -> ! { // do something interesting here loop {} }
qemuでHardFaultにブレイクポイントを張って実行すると、アプリケーションで実装されたハンドラが呼ばれていることがわかります。
cortex-m-rt v0.6.xのexception
アトリビュートを使うと、関数名の間違いなどをコンパイルエラーにしてくれるようです。
後ほど確認してみたいと思います。
きりが良いので、一旦ここまでにします。続きは別エントリでやります。