Rust: Xargoとarm-binutils無しでCortex-Mターゲットのファームウェアをビルドする
2018年4月、Rustでの組み込みプログラミング環境に2つの大きなニュースが舞い込んできた。
Xargo と arm-none-eabi-lld 無しでCortex-M向けのビルドが可能に
PSA: You no longer need Xargo to do ARM Cortex-M development#rustlanghttps://t.co/C6VhbyM0tW
— $ cargo uninstall xargo (@japaricious) April 8, 2018
1つ目は、rustupのターゲットに thumbv*m-none-eabi*
(Cortex-MのTarget Triple)が追加されたというニュース。
今まではrustupでは、thumbv*m-none-eabi*
ターゲットがサポートされておらず、Xargo というツールを使ってlibcoreを thumbv*m-none-eabi*
用にビルドしなくてはならなかった。
しかし、nightly (nightly-2018-04-08) 以降のRustでは thumbv*m-none-eabi*
が追加されたため、Xargoを使って自前でlibcoreをビルドする必要はなくなる。
nightly 2018-04-09で確認したところ、確かにターゲットリストに thumbv*m-none-eabi*
が追加されていた。
$ rustup show Default host: x86_64-unknown-linux-gnu ... active toolchain ---------------- nightly-x86_64-unknown-linux-gnu (default) rustc 1.27.0-nightly (4b9b70c39 2018-04-09) $ rustup target list | pr -w 90 --columns=2 aarch64-apple-ios mips64el-unknown-linux-gnuabi64 aarch64-linux-android mipsel-unknown-linux-gnu aarch64-unknown-fuchsia mipsel-unknown-linux-musl aarch64-unknown-linux-gnu powerpc-unknown-linux-gnu aarch64-unknown-linux-musl powerpc64-unknown-linux-gnu arm-linux-androideabi powerpc64le-unknown-linux-gnu arm-unknown-linux-gnueabi s390x-unknown-linux-gnu arm-unknown-linux-gnueabihf sparc64-unknown-linux-gnu arm-unknown-linux-musleabi sparcv9-sun-solaris arm-unknown-linux-musleabihf thumbv6m-none-eabi armv5te-unknown-linux-gnueabi thumbv7em-none-eabi armv7-apple-ios thumbv7em-none-eabihf armv7-linux-androideabi thumbv7m-none-eabi armv7-unknown-linux-gnueabihf wasm32-unknown-emscripten armv7-unknown-linux-musleabihf wasm32-unknown-unknown armv7s-apple-ios x86_64-apple-darwin asmjs-unknown-emscripten x86_64-apple-ios i386-apple-ios x86_64-linux-android i586-pc-windows-msvc x86_64-pc-windows-gnu i586-unknown-linux-gnu x86_64-pc-windows-msvc i586-unknown-linux-musl x86_64-rumprun-netbsd i686-apple-darwin x86_64-sun-solaris i686-linux-android x86_64-unknown-cloudabi i686-pc-windows-gnu x86_64-unknown-freebsd i686-pc-windows-msvc x86_64-unknown-fuchsia i686-unknown-freebsd x86_64-unknown-linux-gnu (default) i686-unknown-linux-gnu x86_64-unknown-linux-gnux32 i686-unknown-linux-musl x86_64-unknown-linux-musl mips-unknown-linux-gnu x86_64-unknown-netbsd mips-unknown-linux-musl x86_64-unknown-redox mips64-unknown-linux-gnuabi64
cortex-m-rt v0.4.0 has been released! 🎉
— $ cargo uninstall xargo (@japaricious) April 9, 2018
With it you can link ARM Cortex-M programs using LLD.
And since the #rustlang toolchain includes LLD you no longer need to install arm-none-eabi-ld to link your #embedded programshttps://t.co/VgRVh1VEFE
2つ目は、Cortex-Mバイナリ用のリンクにlldを使えるようになったというニュース。
今までは最後のリンクにはarm-binutilsのarm-none-eabi-ldを使ってリンクを行なっていたが、それをlldに置き換えられるようになった。 これはcoretex-m-rtクレートに含まれているリンカスクリプトの記述をlldに対応させたことで可能になったようだ。
この2つの変更によって、RustでのCortex-Mターゲット向けの開発のために必要なツールが減り、開発環境構築のハードルが大きく下がった印象だ。 (あとはgdbをlldbに置き換えることができれば、arm-binutilsは一切不要になる)
このニュースを報告しているJaparicさんはXargoの作者でもあり、Rustでの組み込み開発環境をずっとリードし続けている。 この方がいなかったら、今のRustはここまで組み込み分野を視野に入れたものになっていただろうか…。
nightly-2018-04-08 以降でのCortex-Mターゲットのファームウェアビルド方法
Xargo無し、arm-binutils無しでのビルド手順についてメモしておく。
動作確認には、STM32F401 Nucleo-64ボード(Cortex-M4F)を使用する。
大まかな手順は次のとおり。
- rustupを最新にする
- rustupから対象となるarm MCUの
thumbv*m-none-eabi*
ターゲットを追加する - Rustプロジェクトを作成する。
.cargo/config
にリンカの設定を記述する- 必要なクレートを
Cargo.toml
に追記する - コードを書いてビルドする
rustupを最新にする
$ rustup update
また、ビルドにはnightlyが要求されるので、デフォルトをnightlyに変更しておく。
$ rustup default nightly
thumbv*m-none-eabi*
をターゲットに追加する
今回動作確認を行うMCUはCortex-M4Fなので、rustupから thumbv7em-none-eabihf
をターゲットとして追加する。
$ rustup target add thumbv7em-none-eabihf info: downloading component 'rust-std' for 'thumbv7em-none-eabihf' info: installing component 'rust-std' for 'thumbv7em-none-eabihf'
自分が使うCortex-M MCUがどのTarget Tripleを選べばよいかについては次のとおり。
Target Triple | Target MCUs |
---|---|
thumbv6m-none-eabi |
Targets the Cortex-M0, Cortex-M0+ and Cortex-M1 processors (ARMv6-M architecture) |
thumbv7em-none-eabi |
Targets the Cortex-M4 and Cortex-M7 processors (ARMv7E-M) |
thumbv7em-none-eabihf |
Targets the Cortex-M4F and Cortex-M7F processors (ARMv7E-M) |
thumbv7m-none-eabi |
Targets the Cortex-M3 processor (ARMv7-M) |
Rustプロジェクトを作成する
LEDを点滅させる簡易プログラムのリポジトリを作成したため、この手順ではこちらを使う。
ryochack/Rust_NUCLEO-F401RE_Led_Blink
$ git clone https://github.com/ryochack/Rust_NUCLEO-F401RE_Led_Blink.git
一から作る場合には、 cargo new
でプロジェクトを作成する。
.cargo/config
にリンカの設定を記述する
.cargo/config
にターゲット毎にリンカの設定を記述する。
ここではリンカにlldを指定している。
.cargo/config
[target.thumbv7em-none-eabihf] runner = 'arm-none-eabi-gdb' rustflags = [ "-C", "link-arg=-Tlink.x", "-C", "linker=lld", "-Z", "linker-flavor=ld.lld", "-Z", "thinlto=no", ]
必要なクレートを Cargo.toml
に追記する
thumbv*m-none-eabi*
のビルドに必要なクレートを Cargo.toml
にそれぞれ追加する。
Cargo.toml
[package] name = "nucleo-f401re_led_blink" authors = ["ryochack"] version = "0.1.0" [dependencies] cortex-m = "0.4.0" cortex-m-rt = "0.4.0" panic-abort = "0.1.1" volatile-register = "0.2.0"
コードを書いてビルドする
コードが書けたら(ここではRust_NUCLEO-F401RE_Led_Blinkリポジトリのコードを使う想定)、次のコマンドでターゲットを指定してビルドする。
$ cargo build --target thumbv7em-none-eabihf
これでSTM32F401用のELFファイルが target/thumbv7em-none-eabihf/debug/nucleo-f401re_led_blink
として出力される。
$ file nucleo-f401re_led_blink nucleo-f401re_led_blink: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, with debug_info, not stripped
このELFファイルを次の記事の内容に従ってSTM32F401 Nucleo-64ボードに書き込むことで動作が確認できる。
OpenOCD + ST-LinkでFirmware書き込み - ryochack.blog
Rustの構造体メモリレイアウト
Rustの構造体のメモリレイアウトについてのメモ。
Rustで次のような構造体を定義したときに、構造体のメモリレイアウトはどうなるか?
struct Layout { b1: u8, s1: u16, b2: u8, w1: u32, b3: u8, w2: u32, s2: u16, s3: u16, }
検証時のRustのバージョンは次の通り。
stable-x86_64-unknown-linux-gnu rustc 1.24.1 (d3ae9a9e0 2018-02-27)
TL;DR
先に結論を書く。
アトリビュート指定によって構造体のメモリレイアウトとサイズは以下のように変化する。
デフォルト
構造体サイズ20Byte
repr(C)アトリビュート指定
構造体サイズ24Byte
repr(packed)アトリビュート指定
構造体サイズ17Byte
以下に確認の過程を残しておく。
C言語的にメモリレイアウトを予想
まずは、C言語的にRustの構造体でのメモリレイアウトを予想してみる。
各フィールドの宣言順の通りにメモリ上にデータが配置され、アライメント制約を守るために適宜パディングが入るとすると、次のようなメモリ配置になり、構造体サイズは24Byteとなるはず。
確認方法
次のようなコードを書いて確認した。
use std::mem; struct Layout { b1: u8, s1: u16, b2: u8, w1: u32, b3: u8, w2: u32, s2: u16, s3: u16, } fn main() { let mem = Layout { b1: 0, b2: 0, b3: 0, s1: 0, s2: 0, s3: 0, w1: 0, w2: 0, }; let mem_ptr: *const Layout = &mem; let base = mem_ptr as usize; let ptr_b1: *const u8 = &mem.b1; let ptr_b2: *const u8 = &mem.b2; let ptr_b3: *const u8 = &mem.b3; let ptr_s1: *const u16 = &mem.s1; let ptr_s2: *const u16 = &mem.s2; let ptr_s3: *const u16 = &mem.s3; let ptr_w1: *const u32 = &mem.w1; let ptr_w2: *const u32 = &mem.w2; println!("Memory size = {}", mem::size_of::<Layout>()); println!("base = 0x{:x}", base); println!("offset ptr_w1 = {}", ptr_w1 as usize - base); println!("offset ptr_w2 = {}", ptr_w2 as usize - base); println!("offset ptr_s1 = {}", ptr_s1 as usize - base); println!("offset ptr_s2 = {}", ptr_s2 as usize - base); println!("offset ptr_s3 = {}", ptr_s3 as usize - base); println!("offset ptr_b1 = {}", ptr_b1 as usize - base); println!("offset ptr_b2 = {}", ptr_b2 as usize - base); println!("offset ptr_b3 = {}", ptr_b3 as usize - base); }
デフォルト
前述のコードを使って確認したところ、以下のような構造体のメモリレイアウトになっているのがわかった。
struct Layout { b1: u8, s1: u16, b2: u8, w1: u32, b3: u8, w2: u32, s2: u16, s3: u16, }
Rustではメモリが最も節約されるように、構造体の各フィールドは各型サイズ降順に並び替えられるようだ。
そのおかげで、最初に24Byteと予想した構造体のサイズも20Byteに抑えられている。
repr(C)
前述のようなメモリレイアウトの最適化は、通常のアプリケーションを作る上ではありがたい。
しかし、組み込みソフトのドライバ開発でペリフェラルのレジスタマップを構造体で定義したい場合などには、構造体のフィールドが勝手に並び替えられてしまうのは具合がよくない。
そこでRustには、構造体のメモリレイアウトの最適化を抑制する #[repr(C)]
アトリビュートが用意されている。
構造体定義の頭に #[repr(C)]
アトリビュートを追加してみよう。
#[repr(C)] struct Layout { b1: u8, s1: u16, b2: u8, w1: u32, b3: u8, w2: u32, s2: u16, s3: u16, }
#[repr(C)]
アトリビュートを指定後のメモリレイアウトは次のようになった。
これは最初に予想したC言語的なメモリレイアウトと一致する。
よって、構造体のメモリレイアウト最適化を抑制したい時には #[repr(C)]
アトリビュートを指定すればいい。
repr(packed)
Rustには、#[repr(C)]
アトリビュートの他にも、#[repr(packed)]
アトリビュートが用意されている。
#[repr(packed)]
アトリビュートは構造体のフィールド間のパディングを無くすアトリビュート指定となる。(gccなどが提供する pragma pack(1)
指定と同じ効果が得られる)
#[repr(packed)] struct Layout { b1: u8, s1: u16, b2: u8, w1: u32, b3: u8, w2: u32, s2: u16, s3: u16, }
#[repr(packed)]
アトリビュートを指定後のメモリレイアウトは次のようになった。
構造体のデータ途中のパディング、及び最後のパディングが削除されているため、構造体のサイズも最小の17Byteとなる。 ただし、このメモリレイアウトはアライメント制約を無視したものとなっているため、通信のペイロードサイズに制約がありとにかくデータを詰めて送りたい、などの場合にのみ使うのが良さそう。
※補足として、#[repr(C)]
アトリビュートと#[repr(packed)]
アトリビュートの両方を同時に指定してもこのメモリレイアウトになる。
参考
RustでのIOストリーム処理
Go言語のio.Reader, io.Writer interfaceを使ったio.Cooyのような処理をRustで行うためには、std::io::Read traitとstd::io::Write traitを使って各ストリームを扱えばいい。
標準入力から標準出力へデータをコピーするサンプルコードを次に書く。
copy関数にstd::io::StdinのRead traitとstd::io::StdoutのWrite traitを渡して、その間でデータを流している。
use std::io::{self, Read, Write}; fn copy(reader: &mut Read, writer: &mut Write) { const BUFFER_SIZE: usize = 32 * 1024; let mut buf = [0u8; BUFFER_SIZE]; while let Ok(n) = reader.read(&mut buf) { if n == 0 { break; } let _ = writer.write(&buf[..n]); } } fn main() { let r = io::stdin(); let mut reader = r.lock(); let w = io::stdout(); let mut writer = w.lock(); copy(&mut reader, &mut writer); }
なお、std::io::copy関数が標準ライブラリに含まれているため、前述のように自前でcopy関数を実装する必要はない。(書いてから気づいた)
use std::io; fn main() { let r = io::stdin(); let mut reader = r.lock(); let w = io::stdout(); let mut writer = w.lock(); let _ = io::copy(&mut reader, &mut writer); }
標準ライブラリのどの型にRead、Write traitが実装されているのかは以下を参照すればいい。
参考
Cortex-MターゲットをOpenOCD + GDBでデバッグする(gdb-dashboard or VSCode)
OpenOCDとGDBを使ったarmのCortex-Mターゲットのデバッグ方法についてまとめておく。
個人的に、CUIベースでのデバッグにはgdb-dashboard、GUIベースでのデバッグにはVSCodeを使うのが好みなため、この2つのUIを介してデバッグする方法についてそれぞれ書くことにする。
なお、動作確認環境はArchLinux。
※OpenOCDでのFWの書き込み方法については、"OpenOCD + ST-LinkでFirmware書き込み"も参照。
共通で必要なもの
gdb-dashboardを使う場合も、VSCodeを使う場合も以下がそれぞれ必要になる。 ([]内は動作確認時のバージョン情報)
- OpenOCD [0.10.0]
- arm-none-eabi-gdb [8.1]
- ターゲットに書き込むelfファイル
- Cortex-Mターゲット(この記事ではSTMicroelectronicsのNUCLEO-F401REを使用)
- ST-Linkなどのデバッガ(この記事ではNUCLEO-F401REにオンボードで載っているのでそれを使う)
gdb-dashboard
cyrus-and/gdb-dashboard: Modular visual interface for GDB in Python
CUIのGDBをグラフィカルに表示してくれるPyhtonスクリプト。 CUIからちょっとデバッグしたい時に便利。
インストール
以下の方法でインストールできる。(gdb-dashboardのREADMEではwget
を使っているが、更新に追従できるようにここではgit clone
を使用する)
$ git clone https://github.com/cyrus-and/gdb-dashboard.git $ cp gdb-dashboard/.gdbinit ~/.gdbinit
デバッグ手順
まず、OpenOCDを起動する。
$ openocd -f board/st_nucleo_f4.cfg
-f board/st_nucleo_f4.cfg
の部分はターゲットに応じて適宜変更する。(ArchLinuxの場合は、/usr/share/openocd/scriptsにOpenOCDの設定ファイルが配置されている)
別の端末からGDBを起動し、以下のコマンドを順次打ち込む。
$ arm-none-eabi-gdb ${target_elf_path} >>> target remote localhost:3333 >>> interrupt >>> monitor reset halt >>> load
これで準備が整った。 ここからブレイクポイントを設定するなり、ステップ実行するなりしてデバッグを行う。
以下は、main.rsというファイルの7行目にブレイクポイントを設定してプログラムを実行する例となる。
>>> tbreak main.rs:7 >>> continue
VSCode + Cortex-Debug Extension
Visual Studio Code - Code Editing. Redefined
- VSCode [1.19.0]
- Cortex-Debug Extension [0.1.14]
VSCodeからOpenOCD + GDBでCortex-Mターゲットをデバッグする場合には、Cortex-Debug extensionをインストールするのがおすすめ。 (後述するが、自前でGDBの設定をするとOpenOCDの手動で起動する必要があるが、このextensionを使うとOpenOCDを自動で起動してくれるようになる)
※gdb-dashboardをインストールして~/.gdbinit
が有効になっているとVSCodeのデバッグがうまく動かなくなるので、~/.gdbinit.bk
などにリネームして無効にする。
インストール
VSCodeのデバッグ構成ファイルを用意する
VSCodeでデバッグするためには launch.json というデバッグ構成を記載するための設定ファイルを書く必要がある。
Cortex-Debug extensionを使う際には、以下のように launch.json を書く(launch.json の修正までの手順は"デバッグ | 非公式 - Visual Studio Code Docs"の記事を参考に)
launch.json
{ "version": "0.2.0", "configurations": [ { "type": "cortex-debug", "servertype": "openocd", "request": "launch", "name": "OpenOCD-Debug", "executable": "${ターゲットELFまでのPATH}", "configFiles": [ "board/st_nucleo_f4.cfg" ], "cwd": "${workspaceRoot}", "gdbpath": "arm-none-eabi-gdb", } ] }
※なお、Cortex-Debug extensionはOpenOCDだけではなく、J-Linkにも対応している模様。
デバッグ手順
VSCode (Cortex-Debug Extension無し)
蛇足になるが、VSCodeでCortex-Debug extensionを使わない場合のやり方も残しておく。
※こちらも同様に、gdb-dashboardをインストールして~/.gdbinit
が有効になっているとVSCodeのデバッグがうまく動かなくなるので、~/.gdbinit.bk
などにリネームして無効にする。
VSCodeのデバッグ構成ファイルを用意する
以下の launch.json を用意する。
launch.json
{ "version": "0.2.0", "configurations": [ { "name": "OpenOCD Debug", "type": "gdb", "request": "attach", "executable": "${ターゲットELFまでのPATH}", "remote": true, "target": ":3333", "cwd": "${workspaceRoot}", "gdbpath": "arm-none-eabi-gdb", "autorun": [ "interrupt", "monitor reset halt", "load", ], } ] }
デバッグ手順
まず、OpenOCDを起動する(-f board/st_nucleo_f4.cfg
の部分はターゲットに応じて適宜変更する)
$ openocd -f board/st_nucleo_f4.cfg
OpenOCDが起動できたら、VSCode上でF5キーを押してデバッグを開始する。
参考
systemd環境でのコアダンプ解析
systemd環境下においては、従来(systemd無しの環境)のコアダンプ解析とは勝手が違ってくる。
コアダンプを有効にする
ulimit
コマンドでcore file size
が0になっていないことを確認する。
$ ulimit -a -t: cpu time (seconds) unlimited -f: file size (blocks) unlimited -d: data seg size (kbytes) unlimited -s: stack size (kbytes) 8192 -c: core file size (blocks) unlimited -m: resident set size (kbytes) unlimited -u: processes 63401 -n: file descriptors 1024 -l: locked-in-memory size (kbytes) unlimited -v: address space (kbytes) unlimited -x: file locks unlimited -i: pending signals 63401 -q: bytes in POSIX msg queues 819200 -e: max nice 0 -r: max rt priority 99 -N 15: unlimited
もしも0になっていたら、次のようにしてコアダンプを有効にする。
$ ulimit -c unlimited
ここまでは従来の方法と同じ。
コアダンプの出力場所
コアダンプの出力場所は /proc/sys/kernel/core_pattern を参照するとわかる。 systemdでは、
$ cat /proc/sys/kernel/core_pattern |/usr/lib/systemd/systemd-coredump %P %u %g %s %t %c %e
となっており、 /var/lib/systemd/coredump/ 以下にLZ4で圧縮されて保存される。
保存されるファイル名は core.user-app.1000.adc138b171b140d2bb0a20e628857f04.22015.1515249501000000.lz4
といったものになる。
コアダンプの確認方法
従来の方法だと、次のコマンドでコアダンプの解析が行える。
$ gdb ./user-app core
systemd環境下で前述の方法と同様に行うと、コアダンプの認識に失敗する。
$ gdb ./user-app /var/lib/systemd/coredump/core.user-app.1000.adc138b171b140d2bb0a20e628857f04.22015.1515249501000000.lz4 GNU gdb (GDB) 8.0.1 Copyright (C) 2017 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "x86_64-pc-linux-gnu". Type "show configuration" for configuration details. For bug reporting instructions, please see: <http://www.gnu.org/software/gdb/bugs/>. Find the GDB manual and other documentation resources online at: <http://www.gnu.org/software/gdb/documentation/>. For help, type "help". Type "apropos word" to search for commands related to "word"... Reading symbols from ./user-app...done. "/var/lib/systemd/coredump/core.user-app.1000.adc138b171b140d2bb0a20e628857f04.22015.1515249501000000.lz4" is not a core dump: File format not recognized
これは /var/lib/systemd/coredump/ 以下のコアダンプファイルがLZ4で圧縮されて保存されているためで、LZ4を展開してやればgdbから認識できるようになる。
$ lz4 -d /var/lib/systemd/coredump/core.user-app.1000.adc138b171b140d2bb0a20e628857f04.22015.1515249501000000.lz4 core $ gdb ./user-app core
しかし、毎回コアダンプを展開するのは手間だ。
そこでsystemdでは coredumpctl
というコマンドが用意されている。
coredumpctl の使い方
保存されているコアダンプ一覧を確認する。
$ coredumpctl list TIME PID UID GID SIG COREFILE EXE Fri 2017-11-24 11:23:30 JST 1242 0 0 11 missing /usr/lib/udisks2/udisksd Sun 2017-11-26 00:12:29 JST 1959 1000 1000 5 missing /usr/bin/ibus-daemon Sun 2017-11-26 00:16:42 JST 10271 1000 1000 5 missing /usr/bin/ibus-daemon Fri 2017-12-15 18:42:18 JST 5242 1000 1000 11 missing /usr/share/franz/franz Tue 2017-12-19 22:49:53 JST 2933 1000 1000 11 missing /opt/dropbox/dropbox Thu 2017-12-21 02:14:13 JST 1723 1000 1000 11 missing /usr/share/franz/franz Fri 2017-12-22 02:04:45 JST 9251 1000 1000 11 missing /usr/lib/electron/electron Sun 2018-01-07 01:25:33 JST 6231 1000 1000 8 present /home/ryo/code/workspace/csfml/user-app
gdbを使ってコアダンプを解析する。例えば、前述のリストから /home/ryo/code/workspace/csfml/user-app の解析をする場合には、PID 6231を指定する。
$ coredumpctl gdb 6231