RustのインストールからQEMU上のラズパイ2でベアメタル(OSなし)でプログラムが動作するところまでを解説。
リポジトリはこちら
https://github.com/maueki/baremetal-raspi2-qemu
QEMUインストール
筆者の環境(Debian GNU/Linux)だと以下でインストール可能。
$ sudo apt install qemu-system-arm
Rustのインストール
Rustのインストール・アップデートはrustupでおこなう。
$ curl https://sh.rustup.rs -sSf | sh
すでにrustをインストールしてある場合は以下のコマンドで最新版にしておく。
$ rustup update
クロスビルドツールのインストール
Rustを普通にインストールしただけではインストールしたマシン向けのバイナリしか作ることができない。
本来だとラズパイ2のCPUであるARM(cortex-A7)用のtoolchainをインストールして……となるところだが、そういった面倒な作業を設定ファイルに従って自動で行ってくれるツールcargo-xbuild(xargoのfork)をインストールする。
$ cargo install cargo-xbuild
プロジェクト作成
Rustと一緒にインストールされるcargoを使ってプロジェクトの雛形を作る。
$ cargo new baremetal-raspi2 --bin
$ cd baremetal-raspi2
このプロジェクトで使用されるRustをnightlyに変更しておく。
$ rustup override add nightly
Rustにはstable、unstable、と実験的位置づけのnightlyの3つのバージョンが存在するが、ベアメタルプログラミングする際にはnightlyにしか実装されていない機能を使用する必要があるためだ。
boot.S
いよいよRustでコードを書き始める……と言いたいところだが一番始めに動くコードはアセンブラが都合が良いため、アセンブラで書く。
.section ".text.boot"
.globl _start
_start:
halt:
wfe
b halt
無限ループするだけのコードだ。.section “.text.boot”は後でこのコードをメモリ配置するときの目安となる。
main.rs
さて本来であればboot.Sをビルドするためにbuild.rsを書くところだが、ちょっと面倒なので後回しにして、ここではRustソースにアセンブラファイルを取り込んでしまえるglobal_asm!マクロを使用したmain.rsを用意する。
#![feature(global_asm, asm)]
#![no_std]
#![no_main]
use core::panic::PanicInfo;
global_asm!(include_str!("boot.S"));
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {
unsafe {asm!("" :::: "volatile")}
}
}
ベアメタルなので普段使用しているlibstd(stdモジュール)は使用できない。そのために#[no_std]を書く。
また、通常のようにmain()を書けば自動的にそこがエントリポイントになるということもないため#[no_main]を書いてmain()は書かない。
そして、global_asm!(include_str!(“boot.S”));で先程のboot.Sを取り込む。
panic()は#[no_std]環境でパニックが発生した場合に実行される処理として必要となる。ここでは単純に処理を無限ループに落とすだけなのだが、どうやらRustは副作用なしの無限ループはまずいらしいのでasm!(“” :::: “volatile”)でそれを回避している。
Cargo.toml
Cargo.tomlに以下を追加。
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"
これについては以下参照。
https://os.phil-opp.com/freestanding-rust-binary/#disabling-unwinding
ビルドターゲットファイル作成
xbuildにこのファイルを読み込ませて、クロスビルド環境を自動で用意してもらう。
{
"llvm-target": "armv7-none-eabihf",
"target-endian": "little",
"target-pointer-width": "32",
"target-c-int-width": "32",
"os": "none",
"env": "eabi",
"vendor": "unknown",
"arch": "arm",
"linker": "rust-lld",
"linker-flavor": "ld.lld",
"pre-link-args": {
"ld.lld": [
"--script=linker.ld"
]
},
"data-layout": "e-m:e-p:32:32-i64:64-v128:64:128-a:0:32-n32-S64",
"executables": true,
"relocation-model": "static",
"no-compiler-rt": true
}
指定できる項目については以下参照
-
- rustc_target::spec::Target
- rustc_target::spec::TargetOptions
ポイントとしては”–script=linker.ld”でリンカースクリプト(ッ後述)を指定しているところ。
linker.ldは自前のリンカースクリプトであとで作成する。
リンカースクリプト
セクションについて
linker.ld解説の前にセクションについて少しだけ説明する。
(C/C++同様)Rustではコンパイルされたコード、データはセクションというものに属することになる。
実際に見てみる。
/tmp等に以下のファイルを作って
#[no_mangle]
pub fn hello() -> &'static str {
"Hello World"
}
以下のコマンドでアセンブリを吐かせてみる。
(#[no_mangle]を付けているのは名前マングリングを抑制し結果をわかりやすくするためするため)
$ rustc --emit asm --crate-type lib test.rs
結果はtest.sに出力される。
.text
.file "test.3a1fbbbh-cgu.0"
.section .text.hello,"ax",@progbits
.globl hello
.p2align 4, 0x90
.type hello,@function
hello:
.cfi_startproc
leaq .L__unnamed_1(%rip), %rax
movl $11, %edx
retq
.Lfunc_end0:
.size hello, .Lfunc_end0-hello
.cfi_endproc
.type .L__unnamed_1,@object
.section .rodata..L__unnamed_1,"a",@progbits
.L__unnamed_1:
.ascii "Hello World"
.size .L__unnamed_1, 11
.section ".note.GNU-stack","",@progbits
.sectionでセクションが指定されている。
関数hello()(コード中では.globl hello)はセクション.text.helloで、文字列リテラル”Hello World”はセクション.rodata..L__unnamed_1が指定されていることがわかる。
セクション名は各言語処理系が必要に応じて自由に作ることができる(Rustのようにユーザーが指定できる処理系もある)が、共通で見かけるめぼしいものは以下の4つである。
リンカースクリプトとは
リンカースクリプトはざっくりいうと、上で説明した各セクションのオブジェクトをメモリ上にどのように配置するか決めるためのものだ。
普段は意識しないが、以下のコマンドを実行すると
$ ld --verbose
GCCが使用しているデフォルトのリンカースクリプトが表示される(表示されない環境もあるかも)。
ただしこのデフォルトのリンカースクリプトはOS(筆者の環境だとLinux)上で動作する前提のリンカースクリプトになっているので、ベアメタルで動作するプログラムを作成する場合は使用することができない。
linker.ld作成
というわけで作成した自前のリンカースクリプトlinker.ldが以下となる。
SECTIONS
{
/* Starts at LOADER_ADDR. */
. = 0x8000;
__start = .;
__text_start = .;
.text :
{
KEEP(*(.text.boot))
*(.text)
}
. = ALIGN(4096); /* align to page size */
__text_end = .;
__rodata_start = .;
.rodata :
{
*(.rodata)
}
. = ALIGN(4096); /* align to page size */
__rodata_end = .;
__data_start = .;
.data :
{
*(.data)
}
. = ALIGN(4096); /* align to page size */
__data_end = .;
__bss_start = .;
.bss :
{
bss = .;
*(.bss)
}
. = ALIGN(4096); /* align to page size */
__bss_end = .;
__end = .;
}
RustのリンカーはリンカースクリプトについてGNU ldと互換があるので細かい仕様については$ info ld scriptsを見てもらうのが良いだろう。
ポイントはラズバイ2のCPUは0x8000から読み込みを開始するので、0x8000にsrc/boot.Sで指定したセクション.text.bootを配置していること。これによって起動直後boot.Sの先頭から実行が開始されることになる。
最初のビルドと実行
これでようやくビルドできるようになった。プロジェクトディレクトリ直下で以下のコマンドでビルドする。
$ cargo xbuild --target armv7-none-eabihf.json
なお、linker.ldは編集しても再ビルドしてくれないので要注意。
ビルドできたら以下のコマンドでQEMUを動かしてみる。
$ qemu-system-arm -M raspi2 -kernel target/armv7-none-eabihf/debug/baremetal-raspi2 -d guest_errors -d in_asm
今のところ出力も何もないので実行した命令がわかるようにオプション-d in_asmを指定している。
結果は以下の通り。
----------------
IN:
0x00008000: e320f002 wfe
0x00008004: eafffffd b #0x8000
----------------
IN:
0x00008000: e320f002 wfe
0x00008004: eafffffd b #0x8000
----------------
IN:
0x00008000: e320f002 wfe
0x00008004: eafffffd b #0x8000
----------------
IN:
0x00008000: e320f002 wfe
0x00008004: eafffffd b #0x8000
なんとなくboot.Sの内容が実行されているのがわかると思う。4つ出ているのはラズパイ2にはコアが4つあるため。
Tips
コア1〜3を止める
上に書いたとおりラズパイ2では4つのコアが同時に動き出し0x8000から実行を開始してしまう。これは後々いろいろと都合が悪いので当面の間コア1〜3には寝てて(無限ループして)もらうことにする。
// disable core1-3
mrc p15, 0, r1, c0, c0, 5
and r1, r1, #3
cmp r1, #0
beq 2f
1: wfe
b 1b
2:
コプロからコアのIDを取得してきて0ならラベル2へ、それ以外ならラベル1で無限ループすることになる。
NEONの有効化
上で書いたarmv7-none-eabihf.jsonだが、これに従ってビルドするとSIMD演算器であるNEONコプロセッサを使用するコードが生成される。
しかしラズパイ2の起動時にはNEONコプロセッサは無効になっているため、NEON用のコードを実行しようとすると「未定義命令例外」が発生してしまう。
boot.Sに以下のコードを追加することでNEONコプロセッサが有効となり「未定義命令例外」は発生しなくなる。
// enable NEON/VFP
ldr r0, =(0xF << 20)
mcr p15, 0, r0, c1, c0, 2
mov r3, #0x40000000
vmsr FPEXC, r3
このあとは?
長くなってしまったのでここで一旦終わりとする。
次回は割り込みベクタの設定をおこない、シリアルからの入力をエコーバックするところまでを書きたい。
またベアメタルプログラミングとして以下の記事も書いたので参考にしてもらいたい。
-
- [Rust] ラズパイ2でベアメタルメモリアロケーション
- [Rust] ラズパイ2でベアメタルコンテキストスイッチ