Rust The Embedonomiconをやる①

良いものを発見したので、やってみましょう。

Preface - The Embedonomicon

ターゲットはCortex-Mなので、一通り終わったら、RISC-Vをターゲットにやってみます。

Preface

次の3つのことを習得できるようです。

  • #[no_std]アプリケーションのビルド
  • memory layout (linker)
  • 静的にoverride可能なデフォルト機能の実装

People that are curious about the unusual implementation of runtime crates like cortex-m-rt, msp430-rt and riscv-rt

riscv-rtを読み解く上でも役に立ちそうです。

下準備です。

$ # Rust toolchain
$ # If you start from scratch, get rustup from https://rustup.rs/
$ rustup default beta

$ # toolchain should be newer than this one
$ rustc -V
rustc 1.30.0-beta.1 (14f51b05d 2018-09-18)

$ rustup target add thumbv7m-none-eabi

$ # cargo-binutils
$ cargo install cargo-binutils

$ rustup component add llvm-tools-preview

$ # arm-none-eabi-gdb
$ sudo apt-get install gdb-arm-none-eabi

$ # QEMU
$ sudo apt-get install qemu-system-arm

The smallest #![no_std] program

std crateはOS上で動くことを前提としており、coreはbare metalを前提としている、このあたりは既知の内容ですね。

Rust 2018のプロジェクトを作成します。

$ cargo new --edition 2018 --bin app

$ cd app

src/main.rsに次のコードを書きます。
#![no_main]アトリビュートを付けないと、mainをエントリーポイントとするインタフェースが必要です。ベアメタル環境では、誰もmainに来る前の面倒を見てくれないので、忘れずにつけておきましょう。

#![no_main]
#![no_std]

use core::panic::PanicInfo;

#[panic_handler]
fn panic(_panic: &PanicInfo<'_>) -> ! {
    loop {}
}

この時点では、動くバイナリはできません。
目次を見ていると、リンカスクリプトとboot strapを書く手順を踏んでいくのでしょう。

.cargo/configを作成し、下記を書いておくことで、cargo実行時のターゲットがarm v7になります。

[build]
target = "thumbv7m-none-eabi"

Memory Layout

Cortex-M3をターゲットにするようです。

Background information

The Rust side

Cortex-Mリセット後に実行される関数を実装します。

#[no_mangle]
pub unsafe extern "C" fn Reset() -> ! {
    let _x = 42;

    // can't return so we go into an infinite loop here
    loop {}
}

// The reset vector, a pointer into the reset handler
#[link_section = ".vector_table.reset_vector"]
#[no_mangle]
pub static RESET_VECTOR: unsafe extern "C" fn() -> ! = Reset;

extern "C"でC言語のABIが使われるようになります。余談ですが、RustのABIは未定義です。
なるほど。これで、".vector_table.reset_vectorセクションに、RESET_VECTORシンボルが作成されるわけですね。

The linker script side

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);

EXTERN(RESET_VECTOR);

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));
  } > FLASH

  .text :
  {
    *(.text .text.*);
  } > FLASH

  /DISCARD/ :
  {
    *(.ARM.exidx.*);
  }
}

最後の、.ARM.exidx.*は、例外ハンドラ用のセクションだそうです。Flashの領域には不要なので、捨てています。

Putting it all together

.cargo/configにrustflagsを追加します。

[target.thumbv7m-none-eabi]
rustflags = ["-C", "link-arg=-Tlink.x"]

[build]
target = "thumbv7m-none-eabi"

objdumpでバイナリを確認します。

$ cargo objdump --bin app -- -d -no-show-raw-insn
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s

app:    file format ELF32-arm-little

Disassembly of section .text:
Reset:
       8:       sub     sp, #4
       a:       movs    r0, #42
       c:       str     r0, [sp]
       e:       b       #-2 <Reset+0x8>
      10:       b       #-4 <Reset+0x8>

.vector_tableセクションの内容を確認します。

$ cargo objdump --bin app -- -s -section .vector_table
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s

app:    file format ELF32-arm-little

Contents of section .vector_table:
 0000 00000120 09000000                    ... ....

0x2001_0000となっており、リンカスクリプトで指定したスタックポインタと一致します。

Testing it

ターミナルを2つ用意し、一方でqemuを動かします。

$ qemu-system-arm \
      -cpu cortex-m3 \
      -machine lm3s6965evb \
      -gdb tcp::3333 \
      -S \
      -nographic \
      -kernel target/thumbv7m-none-eabi/debug/app

もう一方のターミナルでGDBを実行します。

$ arm-none-eabi-gdb -q target/thumbv7m-none-eabi/debug/app
Reading symbols from target/thumbv7m-none-eabi/debug/app...done.
(gdb) target remot :3333
Remote debugging using :3333
Reset () at src/main.rs:12
12      pub unsafe extern "C" fn Reset() -> ! {
(gdb) print/x $sp
$1 = 0x20010000
(gdb) step
Reset () at src/main.rs:13
13          let _x = 42;
(gdb) step
16          loop {}
(gdb) print _x
$2 = 42
(gdb) print &_x
$3 = (i32 *) 0x2000fffc
(gdb) quit

無事動作確認できました。