mainインタフェース

現在、最小限の動くプログラムがあります。しかし、エンドユーザーが安全なプログラムをビルドできるようにパッケージを作る必要があります。 このセクションでは、標準のRustプログラムが使うようなmainインタフェースを実装します。

まず最初に、バイナリクレートをライブラリクレートに変換します。

$ mv src/main.rs src/lib.rs

そして、クレートを「runtime」を意味するrtという名前に変えます。

$ sed -i s/app/rt/ Cargo.toml

$ head -n4 Cargo.toml
[package]
edition = "2018"
name = "rt" # <-
version = "0.1.0"

最初の変更は、リセットハンドから外部のmain関数を呼び出すようにすることです。

$ head -n13 src/lib.rs
#![no_std]

use core::panic::PanicInfo;

// 変更しました!
#[no_mangle]
pub unsafe extern "C" fn Reset() -> ! {
    extern "Rust" {
        fn main() -> !;
    }

    main()
}

#![no_main]アトリビュートも取り除いています。このアトリビュートはライブラリクレートには効果がありません。

ここで、直交する疑問が湧きます。rtライブラリは標準のパニック動作を提供すべきでしょうか?それとも、 #[panic_handler]関数を提供せずに、ユーザーがパニック動作を選べるように残しておくべきでしょうか? 本ドキュメントでは、この疑問を深堀りせず、単純化のためにrtクレートにダミーの#[panic_handler]関数を残しておきます。 しかしながら、他の選択肢があることを読者に伝えておきます。

2つ目の変更は、これまでに書いたリンカスクリプトを、アプリケーションクレートに提供することです。 リンカがライブラリサーチパス(-L)とリンカを呼び出したディレクトリから、リンカスクリプトを探すことはご存知でしょう。 アプリケーションクレートがlink.xのコピーを持たなくて済むように、ビルドスクリプトを使ってrtクレートが、 ライブラリサーチパスにリンカスクリプトを置くようにします。

$ # `rt`のルートディレクトリに、以下の内容でbuild.rsを作ります
$ cat build.rs
use std::{env, error::Error, fs::File, io::Write, path::PathBuf};

fn main() -> Result<(), Box<Error>> {
    // このクレート用のビルドディレクトリです
    let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());

    // ライブラリサーチパスを追加します
    println!("cargo:rustc-link-search={}", out_dir.display());

    // `link.x`をビルドディレクトリに置きます
    File::create(out_dir.join("link.x"))?.write_all(include_bytes!("link.x"))?;

    Ok(())
}

これで、ユーザーはmainシンボルを公開するアプリケーションを書くことができ、rtクレートとリンクすることができます。 rtは、アプリケーションプログラムに正しいメモリレイアウトを提供します。

$ cd ..

$ cargo new --edition 2018 --bin app

$ cd app

$ # Cargo.tomlを`rt`クレートとの依存関係を持つように修正します
$ tail -n2 Cargo.toml
[dependencies]
rt = { path = "../rt" }
$ # デフォルトターゲットとリンカ呼び出しを微調整した設定ファイルをコピーします
$ cp -r ../rt/.cargo .

$ # `main.rs`の内容を下記の通り変更します
$ cat src/main.rs
#![no_std]
#![no_main]

extern crate rt;

#[no_mangle]
pub fn main() -> ! {
    let _x = 42;

    loop {}
}

逆アセンブリの結果は似ていますが、ここではユーザーのmain関数を含んでいます。

$ cargo objdump --bin app -- -d -no-show-raw-insn

app:	file format ELF32-arm-little

Disassembly of section .text:
main:
	sub	sp, #4
	movs	r0, #42
	str	r0, [sp]
	b	#-2 <main+0x8>
	b	#-4 <main+0x8>

Reset:
	bl	#-14
	trap

型安全にする

mainインタフェースは機能しますが、簡単に誤った使い方ができてしまいます。 例えば、ユーザーは発散しない関数としてmain関数を書くかもしれません。その結果、コンパイルエラーは発生しませんが、 未定義動作になるでしょう(コンパイラはプログラムに誤った最適化を行います)。

シンボルインタフェースではなくマクロをユーザーに公開することで、型安全性を追加することができます。 rtクレートに、次のマクロを書くことができます。

$ tail -n12 ../rt/src/lib.rs

#![allow(unused)]
fn main() {
#[macro_export]
macro_rules! entry {
    ($path:path) => {
        #[export_name = "main"]
        pub unsafe fn __main() -> ! {
            // 与えられたパスの型チェック
            let f: fn() -> ! = $path;

            f()
        }
    }
}
}

そして、アプリケーション作成者は、このマクロを次のように呼び出します。

$ cat src/main.rs
#![no_std]
#![no_main]

use rt::entry;

entry!(main);

fn main() -> ! {
    let _x = 42;

    loop {}
}

今度は、アプリケーション作成者は、mainのシグネチャをfn()のような発散しない関数に変更すると、 エラーに遭遇するでしょう。

main前の生活

rtは良さそうに見えますが、まだ機能が完全ではありません!rtクレートに対して書かれたアプリケーションは、 static変数や文字列リテラルを使うことができません。rtのリンカスクリプトが、 標準の.bss.data.rodataセクションを定義していないからです。これを直していきましょう!

最初のステップはリンカスクリプトに下記のセクションを定義することです。

$ # ファイルの一部のみを見せます
$ sed -n 25,46p ../rt/link.x
  .text :
  {
    *(.text .text.*);
  } > FLASH

  /* 追加! */
  .rodata :
  {
    *(.rodata .rodata.*);
  } > FLASH

  .bss :
  {
    *(.bss .bss.*);
  } > RAM

  .data :
  {
    *(.data .data.*);
  } > RAM

  /DISCARD/ :

これらは入力セクションを単に再度エスクポートし、各メモリ領域のどこに出力セクションが置かれるかを指定しているだけです。

これらの変更で、下記のプログラムがコンパイル可能になります。

#![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 {}
}

しかし、実際のハードウェア上でプログラムを実行し、デバッグすると、mainに到達した時点で、 static変数のBSSDATAが、01になっていないことに気づくでしょう。 代わりに、これらの変数はゴミデータを持っています。この問題は、デバイスの電源投入時、RAMがランダムなデータを持つためです。 プログラムをQEMUで実行すると、この現象は観測できません。

実は、プログラムがstatic変数に書き込みを行う前に、その変数を読むことは、未定義動作です。 mainを呼ぶ前に、全てのstatic変数を初期化するように修正しましょう。

RAM初期化のために、リンカスクリプトをさらに微修正しなければなりません。

$ # ファイルの一部のみを見せます
$ sed -n 25,52p ../rt/link.x
  .text :
  {
    *(.text .text.*);
  } > FLASH

  /* 変更! */
  .rodata :
  {
    *(.rodata .rodata.*);
  } > FLASH

  .bss :
  {
    _sbss = .;
    *(.bss .bss.*);
    _ebss = .;
  } > RAM

  .data : AT(ADDR(.rodata) + SIZEOF(.rodata))
  {
    _sdata = .;
    *(.data .data.*);
    _edata = .;
  } > RAM

  _sidata = LOADADDR(.data);

  /DISCARD/ :

変更内容を詳細に見ていきましょう。

    _sbss = .;
    _ebss = .;
    _sdata = .;
    _edata = .;

シンボルを.bssセクションと.dataセクションの開始アドレスと終了アドレスに関連付けます。 これらは後ほど、Rustコードで使用します。

  .data : AT(ADDR(.rodata) + SIZEOF(.rodata))

.rodataセクションの終わりに、.dataセクションのロードメモリアドレス(LMA; Load Memory Address)を設定します。 .dataはゼロでない初期値をもったstatic変数が含まれています。.dataセクションの仮想メモリアドレス(VMA; Virtual Memory Address)は、 RAMのどこかにあります。これは、static変数が配置されている場所です。 しかし、これらのstatic変数の初期値は、不揮発性メモリ(Flash)に割り当てられなければなりません。 LMAは、これらの初期値が格納されるFlashの場所を示しています。

  _sidata = LOADADDR(.data);

最後に、.dataセクションのLMAをシンボルに関連付けます。

Rust側では、.bssセクションをゼロクリアし、.dataセクションを初期化します。Rustコードからリンカスクリプトで作成したシンボルを参照できます。 これらのシンボルのアドレス1は、.bssセクションと.dataセクションの境界になります。

リセットハンドラを、次のように更新します。

$ head -n32 ../rt/src/lib.rs
#![no_std]

use core::panic::PanicInfo;
use core::ptr;

#[no_mangle]
pub unsafe extern "C" fn Reset() -> ! {
    // 追加!
    // 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()
}

これで、未定義動作なしに、エンドユーザーは直接的にも間接的にもstatic変数を使うことができます。

上記のコードでは、メモリ初期化をバイト単位の方法で初期化しています。.bssセクションと.dataセクションを例えば4バイトでアライメントすることが可能です。 このことは、アライメントチェックなしにワード単位の初期化を行うために、Rustコードで使うことができます。 どのようにやるのか興味がある場合、cortex-m-rtクレートをチェックして下さい。

1

ここで使っているリンカスクリプトシンボルのアドレスを使用する必要があるということは、混乱を招きやすく、直感的ではありません。 この奇妙さについての詳細な説明は、ここにあります。