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 = "2024"
name = "rt" # <-
version = "0.1.0"
最初の変更は、リセットハンドラが外部の main 関数を呼び出すようにすることです:
$ head -n13 src/lib.rs
#![no_std]
use core::panic::PanicInfo;
// CHANGED!
#[unsafe(no_mangle)]
pub unsafe extern "C" fn Reset() -> ! {
unsafe extern "Rust" {
safe fn main() -> !;
}
main()
}
また、#![no_main] 属性も削除します。これはライブラリクレートでは効果がないためです。
この段階では別の観点の問いも生じます。
rtライブラリは標準的な panic 時の挙動を提供すべきでしょうか。それとも、#[panic_handler]関数を 提供しない ことで、panic 時の挙動をエンドユーザーが 選べるようにしておくべきでしょうか。この文書ではその問いには立ち入りません。単純化のため、 ダミーの#[panic_handler]関数はrtクレートに残しておきます。ただし、 他の選択肢もあることは読者に伝えておきたいと思います。
2 つ目の変更は、先ほど書いたリンカスクリプトをアプリケーションクレートに提供することです。リンカーは、ライブラリ検索パス (-L) と、リンカーが起動されるディレクトリでリンカスクリプトを検索します。アプリケーションクレートが link.x のコピーを同梱しておく必要はないので、build script を使って rt クレートがリンカスクリプトをライブラリ検索パスに配置するようにします。
$ # `rt` のルートに次の内容で build.rs ファイルを作成する
$ cat build.rs
use std::{env, error::Error, fs::File, io::Write, path::PathBuf};
fn main() -> Result<(), Box<dyn Error>> {
// build directory for this crate
let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());
// extend the library search path
println!("cargo:rustc-link-search={}", out_dir.display());
// put `link.x` in the build directory
File::create(out_dir.join("link.x"))?.write_all(include_bytes!("link.x"))?;
Ok(())
}
これでユーザーは main シンボルを公開するアプリケーションを書き、それを rt クレートにリンクできるようになります。
rt は、プログラムに適切なメモリレイアウトを与える役目を担います。
$ cd ..
$ cargo new --edition 2024 --bin app
$ cd app
$ # `rt` クレートを依存関係として含めるように Cargo.toml を変更する
$ 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;
#[unsafe(no_mangle)]
pub fn main() -> ! {
let _x = 42;
loop {}
}
逆アセンブル結果は似たものになりますが、今度はユーザーの main 関数が含まれます。
$ cargo objdump --bin app -- -d --no-show-raw-insn --print-imm-hex
app: file format elf32-littlearm
Disassembly of section .text:
00000008 <main>:
8: push {r7, lr}
a: mov r7, sp
c: sub sp, #0x4
e: movs r0, #0x2a
10: str r0, [sp]
12: b 0x14 <main+0xc> @ imm = #-0x2
14: b 0x14 <main+0xc> @ imm = #-0x4
00000016 <Reset>:
16: push {r7, lr}
18: mov r7, sp
1a: bl 0x8 <main> @ imm = #-0x16
型安全にする
main インターフェースは機能しますが、間違えやすいものです。たとえば、ユーザーが main
を発散しない関数として書いても、コンパイル時エラーは発生せず、未定義動作になります(コンパイラがプログラムを誤って最適化します)。
シンボルインターフェースの代わりにマクロをユーザーに公開することで、型安全性を追加できます。
rt クレートでは、このマクロを次のように書けます:
$ tail -n12 ../rt/src/lib.rs
#![allow(unused)]
fn main() {
#[macro_export]
macro_rules! entry {
($path:path) => {
#[unsafe(export_name = "main")]
pub unsafe fn __main() -> ! {
// type check the given path
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 のリンカスクリプトが標準の
.bss、.data、.rodata セクションを定義していないため、static 変数や文字列リテラルを使えません。これを直しましょう!
最初のステップは、リンカスクリプトでこれらのセクションを定義することです:
$ # ファイルの一部だけを表示
$ sed -n 25,46p ../rt/link.x
.text :
{
*(.text .text.*);
} > FLASH
/* NEW! */
.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;
#[allow(static_mut_refs)]
fn main() -> ! {
let _x = RODATA;
let _y = unsafe { &BSS };
let _z = unsafe { &DATA };
loop {}
}
しかし、このプログラムを実機で実行してデバッグすると、main に到達した時点で static
変数 BSS と DATA が 0 と 1 になっていないことが分かります。
その代わり、これらの変数にはゴミ値が入っています。問題は、デバイスの電源投入後の RAM の内容が
ランダムであることです。プログラムを QEMU で実行しても、この現象は観測できません。
現状では、プログラムが static 変数に書き込む前にそれを読み取ると、
未定義動作になります。main を呼び出す前にすべての static 変数を初期化して、これを修正しましょう。
RAM の初期化を行うには、リンカスクリプトをもう少し調整する必要があります:
$ # ファイルの一部だけを表示
$ sed -n 25,52p ../rt/link.x
.text :
{
*(.text .text.*);
} > FLASH
/* CHANGED! */
.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 セクションの開始アドレスと終了アドレスにシンボルを対応付けます。これらは後で初期化に使います。
.data : AT(ADDR(.rodata) + SIZEOF(.rodata))
.data セクションの Load Memory Address (LMA) を .rodata
セクションの末尾に設定します。.data には初期値が 0 ではない static 変数が入ります。.data
セクションの Virtual Memory Address (VMA) は RAM 上のどこかにあり、そこが static 変数の
配置場所です。しかし、それらの static 変数の初期値は不揮発性メモリ(Flash)に
配置されていなければなりません。LMA は、それらの初期値が Flash のどこに格納されるかを示します。
_sidata = LOADADDR(.data);
最後に、.data の LMA にシンボルを対応付けます。
初期化コードを使って、.bss セクションをゼロクリアし、.data セクションを初期化します。リンカスクリプトで作成したシンボルは、コードから参照できます。これらのシンボルの アドレス1 が
.bss と .data セクションの境界になります。
純粋な Rust コードで .bss と .data セクションの初期化コードを書くこともできます。実際、
この本の以前の版ではそうしていました。しかし、時間の経過とともに健全性に関するいくつかの疑問が提起されており、
いまでは Rust コードでこれらを初期化することは良い実践とは見なされなくなっています。詳しくは、この本の
なぜ .data と .bss を Rust で初期化しないのか の節を参照してください。
global_asm! マクロを使って初期化コードを書き、リセットハンドラを定義します。
更新されたリセットハンドラは、今度は Thumb-2 アセンブリで書かれており、以下のとおりです:
$ head -n53 ../rt/src/lib.rs
#![allow(unused)]
#![no_std]
fn main() {
use core::panic::PanicInfo;
use core::arch::global_asm;
global_asm!(
".text
.syntax unified
.global _sbss
.global _ebss
.global _sdata
.global _edata
.global _sidata
.global main
.global Reset
.type Reset,%function
.thumb_func
Reset:
_init_bss:
movs r2, #0
ldr r0, =_sbss
ldr r1, =_ebss
1:
cmp r1, r0
beq _init_data
strb r2, [r0]
add r0, #1
b 1b
_init_data:
ldr r0, =_sdata
ldr r1, =_edata
ldr r2, =_sidata
1:
cmp r0, r1
beq _main_trampoline
ldrb r3, [r2]
strb r3, [r0]
add r0, #1
add r2, #1
b 1b
_main_trampoline:
ldr r0, =main
bx r0"
);
}
これで、エンドユーザーは未定義動作に陥ることなく、static 変数を直接的にも間接的にも利用できます!
上のコードでは、メモリ初期化を 1 バイトずつ行いました。
.bssと.dataセクションを、たとえば 4 バイト境界にそろえるよう強制することも可能です。この事実はその後 Rust コードで利用でき、アラインメントチェックを省略しつつワード単位で初期化を行えます。これを どう実現するかに興味があるなら、cortex-m-rtクレートを確認してください。