Help us understand the problem. What is going on with this article?

[Rust] ラズパイ2でベアメタルプログラミング入門(QEMU編)

More than 1 year has passed since last update.

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でコードを書き始める……と言いたいところだが一番始めに動くコードはアセンブラが都合が良いため、アセンブラで書く。

src/boot.S
.section ".text.boot"

.globl _start
_start:
halt:
        wfe
        b halt

無限ループするだけのコードだ。.section ".text.boot"は後でこのコードをメモリ配置するときの目安となる。

main.rs

さて本来であればboot.Sをビルドするためにbuild.rsを書くところだが、ちょっと面倒なので後回しにして、ここではRustソースにアセンブラファイルを取り込んでしまえるglobal_asm!マクロを使用したmain.rsを用意する。

src/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に以下を追加。

Cargo.toml
[profile.dev]
panic = "abort"

[profile.release]
panic = "abort"

これについては以下参照。
https://os.phil-opp.com/freestanding-rust-binary/#disabling-unwinding

ビルドターゲットファイル作成

xbuildにこのファイルを読み込ませて、クロスビルド環境を自動で用意してもらう。

armv7-none-eabihf.json
{
    "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
}

指定できる項目については以下参照

ポイントとしては"--script=linker.ld"でリンカースクリプト(ッ後述)を指定しているところ。
linker.ldは自前のリンカースクリプトであとで作成する。

リンカースクリプト

セクションについて

linker.ld解説の前にセクションについて少しだけ説明する。
(C/C++同様)Rustではコンパイルされたコード、データはセクションというものに属することになる。

実際に見てみる。
/tmp等に以下のファイルを作って

test.rs
#[no_mangle]
pub fn hello() -> &'static str {
    "Hello World"
}

以下のコマンドでアセンブリを吐かせてみる。
(#[no_mangle]を付けているのは名前マングリングを抑制し結果をわかりやすくするためするため)

$ rustc --emit asm --crate-type lib test.rs

結果はtest.sに出力される。

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つである。

セクション名 役割
.text 実行コード領域
.rodata (文字列などの)固定データ領域
.data 初期化済みデータ領域
.bss 初期値ゼロのデータ領域

リンカースクリプトとは

リンカースクリプトはざっくりいうと、上で説明した各セクションのオブジェクトをメモリ上にどのように配置するか決めるためのものだ。

普段は意識しないが、以下のコマンドを実行すると

$ ld --verbose

GCCが使用しているデフォルトのリンカースクリプトが表示される(表示されない環境もあるかも)。
ただしこのデフォルトのリンカースクリプトはOS(筆者の環境だとLinux)上で動作する前提のリンカースクリプトになっているので、ベアメタルで動作するプログラムを作成する場合は使用することができない。

linker.ld作成

というわけで作成した自前のリンカースクリプト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には寝てて(無限ループして)もらうことにする。

src/boot.S
        // 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コプロセッサが有効となり「未定義命令例外」は発生しなくなる。

src/boot.S
        // enable NEON/VFP
        ldr r0, =(0xF << 20)
        mcr p15, 0, r0, c1, c0, 2
        mov r3, #0x40000000
        vmsr FPEXC, r3

このあとは?

長くなってしまったのでここで一旦終わりとする。
次回は割り込みベクタの設定をおこない、シリアルからの入力をエコーバックするところまでを書きたい。

またベアメタルプログラミングとして以下の記事も書いたので参考にしてもらいたい。

参考URL

https://os.phil-opp.com/

itage
ITAGEは「IT」のAGENCYになることを夢、目標として進化、変化していきます。「It’s It Agency」
http://www.itage.co.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした