Rust The Embedonomiconをやる②

tomo-wait-for-it-yuki.hatenablog.com

Rust The Embedonomiconの続きです。
前回記事では、下記2つの項目を実施しました。

  1. The smallest #![no_std] program
  2. 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.tomlrt crateへの依存を追加します。

[dependencies]
rt = { path = "../rt" }

.cargo/configrt 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 cratesrc/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.xPROVIDEを使ってデフォルトハンドラを提供します。

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アトリビュートを使うと、関数名の間違いなどをコンパイルエラーにしてくれるようです。
後ほど確認してみたいと思います。

きりが良いので、一旦ここまでにします。続きは別エントリでやります。