メモリレイアウト

次のステップは、ターゲットシステムがプログラムを実行できるように、プログラムに正しいメモリレイアウトを持たせることです。 例では、LM3S6965という仮想のCortex-M3マイクロコントローラを取り扱います。 私達のプログラムは、デバイス上で動作する唯一のプロセスになります。そのため、デバイスの初期化も面倒を見る必要があります。

背景となる情報

Cortex-Mデバイスは、コードメモリ領域の開始地点にベクタテーブルがあること、を要求します。 ベクタテーブルはポインタの配列です。最初の2つのポインタは、デバイスが起動するときに必要です。 残りのポインタは例外に関係するもので、今は無視します。

リンカは、プログラムの最終的なメモリレイアウトを決定します。しかし、リンカスクリプトを使うことで、メモリレイアウトを制御できます。 リンカスクリプトによる制御の粒度は、セクションレベルです。セクションは、連続したメモリに置かれるシンボルの集まりです。 ここで、シンボルはデータ(静的変数)か命令(Rustの関数)になります。

全てのシンボルは、コンパイラによって割り当てられた名前を持ちます。Rust 1.28以降では、Rustコンパイラは、 _ZN5krate6module8function17he1dfc17c86fe16daE、のような形式でシンボル名を割り当てます。 このシンボルは、krate::module::function::he1dfc17c86fe16daにデマングルできます。 ここで、krate::module::functionは、関数か変数のパスです。そして、he1dfc17c86fe16daは何らかのハッシュです。 Rustコンパイラは、各シンボルをシンボル固有のセクションに配置します。例えば、上述したシンボルは、 .text._ZN5krate6module8function17he1dfc17c86fe16daEというセクションの配置されます。

コンパイラが生成したシンボル名とセクション名は、Rustコンパイラのリリースごとに変わる可能性があります。 しかし、次のアトリビュートを使って、シンボル名やセクション配置を制御することができます。

  • #[export_name = "foo"]は、シンボル名をfooに設定します。
  • #[no_mangle]は、関数名や変数名を(フルパスではなく)シンボル名として使うことを意味します。#[no_mangle] fn bar()は、barというシンボル名を生成します。
  • #[link_section = ".bar"]は、シンボルを.barという名前のセクションに配置します。

これらのアトリビュートにより、プログラムの安定的なABIを公開することができ、リンカスクリプトで利用することができます。

Rust側

上述の通り、Cortex-Mデバイスに対して、ベクタテーブルの最初の2つのエントリを配置する必要があります。 1つ目は、スタックポインタの初期値で、リンカスクリプトだけを使って配置することができます。 2つ目のリセットベクタは、Rustのコードを作成する必要があり、リンカスクリプトを使って正しく配置しなければなりません。

リセットベクタは、リセットハンドラのポインタです。リセットハンドラは、デバイスがシステムリセットの後、もしくは、 最初に電源が入った後に実行する関数です。リセットハンドラは、常にハードウェアコールスタックの最初のスタックフレームになります。 戻るためのスタックフレームがないため、リセットハンドラから戻ることは、未定義動作です。 発散関数のマーキングを行うことで、リセットハンドラが決して戻らないように強制できます。 発散関数は、fn(/* .. */) -> !というシグネチャがついた関数です。


#![allow(unused)]
fn main() {
#[no_mangle]
pub unsafe extern "C" fn Reset() -> ! {
    let _x = 42;

    // 戻れないため、ここで無限ループに入ります
    loop {}
}

// リセットベクタは、リセットハンドラへのポインタです
#[link_section = ".vector_table.reset_vector"]
#[no_mangle]
pub static RESET_VECTOR: unsafe extern "C" fn() -> ! = Reset;

}

ここで、ハードウェアは、特定の形式を期待しています。これに従うため、extern "C"を使うことで、コンパイラがこの関数をC ABIを使うように指示します。 そうしなければ、安定していないRust ABIが使われます。

リンカスクリプトからリセットハンドラとリセットベクタを参照するために、#[no_mangle]を使って安定したシンボル名を与えます。 RESET_VECTORの位置を細かく制御しなければなりません。そこで、.vector_table.reset_vectorと呼ばれるセクションに配置します。 リセットハンドラであるReset自身の正確な位置は重要ではありません。これに対しては、デフォルトでコンパイラが生成するセクションを使用します。

また、入力のオブジェクトファイルを解析する間、リンカは、内部シンボルと呼ばれる内部リンケージのシンボルを無視します。 そこで、2つのシンボルが外部リンケージを持つようにする必要があります。Rustでシンボルを外部向けにする唯一の方法は、 関連するアイテムをpublic (pub) にして、到達可能(アイテムとクレートのトップレベルとの間にプライベートなモジュールがない)なものにすることです。

リンカスクリプト側

下記に、正しい位置にベクタテーブルを配置する最小限のリンカスクリプトを示します。 全体に目を通してみましょう。

$ cat link.x
/* LM3S6965マイクロコントローラのメモリレイアウト */
/* 1K = 1 KiBi = 1024バイト */
MEMORY
{
  FLASH : ORIGIN = 0x00000000, LENGTH = 256K
  RAM : ORIGIN = 0x20000000, LENGTH = 64K
}

/* エントリポイントはリセットハンドラです */
ENTRY(Reset);

EXTERN(RESET_VECTOR);

SECTIONS
{
  .vector_table ORIGIN(FLASH) :
  {
    /* 1つ目のエントリ。スタックポインタの初期値 */
    LONG(ORIGIN(RAM) + LENGTH(RAM));

    /* 2つ目のエントリ。リセットベクタ */
    KEEP(*(.vector_table.reset_vector));
  } > FLASH

  .text :
  {
    *(.text .text.*);
  } > FLASH

  /DISCARD/ :
  {
    *(.ARM.exidx.*);
  }
}

MEMORY

リンカスクリプトのこのセクションは、ターゲット内のメモリブロックの位置とサイズを記述します。 2つのメモリブロックが定義されています。FLASHRAMです。これらは、ターゲットで利用可能な物理メモリと関連しています。 ここで使用されている値は、LM3S6965マイクロコントローラのものです。

ENTRY

ここでは、リンカにResetというシンボル名を持つリセットハンドラが、プログラムのエントリポイントであることを教えています。 リンカは、不要なセクションを積極的に破棄します。リンカは、エントリポイントと、エントリポイント関数から呼ばれる関数を使用されると考え、 破棄しなくなります。この行がないと、リンカは、Reset関数と、そこから呼ばれる全ての関数を破棄するでしょう。

EXTERN

リンカは怠け者です。エントリポイントから再帰的に参照されるシンボルが全て見つかった時点で、入力オブジェクトファイルの解析を停止します。 EXTERNにより、他の参照されるシンボルが全て見つかった後でも、リンカはEXTERNの引数が見つかるまで探し続けます。 基本、エントリポイントから呼ばれないシンボルが出力バイナリで必要な場合、KEEPと関連付けてEXTERNを使う必要があります。

SECTIONS

ここでは、入力オブジェクトファイル内のセクション(入力セクション)がどのように出力オブジェクトファイルのセクション(出力セクション)に配置されるのか、 もしくは破棄されるのか、を説明します。 2つの出力セクションを定義します。

  .vector_table ORIGIN(FLASH) : { /* .. */ } > FLASH

.vector_tableは、ベクタテーブルを含んでおり、FLASHメモリの開始地点に配置されます。

  .text : { /* .. */ } > FLASH

そして、.textは、プログラムのサブルーチンを含んでおり、FLASHのどこかに配置されます。開始アドレスは指定されていませんが、 リンカは直前の出力セクションである.vector_tableの後ろに、このセクションを配置するでしょう。

出力セクションの.vecotr_tableは、次のものを含んでいます。

    /* 1つ目のエントリ。スタックポインタの初期値 */
    LONG(ORIGIN(RAM) + LENGTH(RAM));

(コール)スタックをRAMの最後に配置します。スタックは、完全な降順です。すなわち、小さいアドレスに向かって伸びます。 そのため、RAMの最後のアドレスをスタックポインタ(SPモード)の初期値として使用します。 このアドレスは、リンカスクリプト内でRAMメモリブロックに入力した情報を使って、計算されます。

    /* 2つ目のエントリ。リセットベクタ */
    KEEP(*(.vector_table.reset_vector));

次に、SPの初期値の直後に.vector_table.reset_vectorと名付けられた全ての入力セクションがリンカによって挿入されるように、KEEPを使います。 RESET_VECTORがこのセクションに配置される唯一のシンボルです。これは、ベクタテーブルの2つ目にRESET_VECTORを配置するのに効率的な方法です。

出力セクションの.textは、次の内容を含んでいます。

    *(.text .text.*);

これは、.text.text.*という名前の入力セクションを全て含んでいます。 リンカが不必要なセクションを破棄しないようにさせるための、KEEPを、ここでは使わないことに留意して下さい。

最後に、破棄用の特別な/DISCARD/セクションを使います。

    *(.ARM.exidx.*);

.ARM.exidx.*という入力セクションを破棄します。これらのセクションは、例外処理に関連したものですが、 パニック時のスタック巻き戻しを行わないのと、これらのセクションはFlashメモリの容量を使うため、単に破棄します。

1つにまとめる

これで、アプリケーションをリンクできます。参考用に、完全なRustプログラムを示します。


#![allow(unused)]
#![no_main]
#![no_std]

fn main() {
use core::panic::PanicInfo;

// リセットハンドラ
#[no_mangle]
pub unsafe extern "C" fn Reset() -> ! {
    let _x = 42;

    // 戻れないため、ここで無限ループに入ります
    loop {}
}

// リセットベクタは、リセットハンドラへのポインタです
#[link_section = ".vector_table.reset_vector"]
#[no_mangle]
pub static RESET_VECTOR: unsafe extern "C" fn() -> ! = Reset;

#[panic_handler]
fn panic(_panic: &PanicInfo<'_>) -> ! {
    loop {}
}
}

私達のリンカスクリプトを使うために、リンカプロセスに手を加えなければなりません。これは、rustc-C link-argフラグを渡すことで達成できます。 しかし、2つのやり方があります。下記のようにcargo-rustcサブコマンドをcargo-buildの代わりに使用することができます。

重要:このコマンドを実行する前に、前回のセクションの最後に追加した.cargo/configファイルがあることを確認して下さい。

$ cargo rustc -- -C link-arg=-Tlink.x

もしくは、.cargo/configにrustflagsを設定し、cargo-buildサブコマンドを使い続けることもできます。 cargo-binutilsとの統合がやりやすいため、2つ目の方法を使います。

# .cargo/configを次の内容で修正します
$ cat .cargo/config
[target.thumbv7m-none-eabi]
rustflags = ["-C", "link-arg=-Tlink.x"]

[build]
target = "thumbv7m-none-eabi"

[target.thumbv7m-none-eabi]の部分は、このフラグがターゲット向けのクロスコンパイル時のみ有効であることを意味しています。

調査

それでは、望み通りのメモリレイアウトになっているか確認するため、出力バイナリを調査してみましょう。

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

app:	file format ELF32-arm-little

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

これは.textセクションの逆アセンブリです。Resetというリセットハンドラが0x8番地に位置していることがわかります。

$ cargo objdump --bin app -- -s -section .vector_table

app:	file format ELF32-arm-little

Contents of section .vector_table:
 0000 00000120 09000000                    ... ....

これは、.vector_tableセクションの内容を示しています。セクションは0x0番地から開始しており、セクションの1つ目のワードは、0x2001_0000であることがわかります (objdumpはリトリエンディアン形式で出力します)。これはSPの初期値で、RAMの最後のアドレスと一致します。 2つ目のワードは0x9です。これは、リセットハンドラのthumbモードアドレスです。 関数がthumbモードで実行される場合、そのアドレスの1ビット目は1に設定されます。

テスト

このプログラムは、有効なLM3S6965プログラムです。このプログラムをテストするため、仮想のマイクロコントローラ(QEMU)で実行できます。

$ # this program will block
$ qemu-system-arm \
      -cpu cortex-m3 \
      -machine lm3s6965evb \
      -gdb tcp::3333 \
      -S \
      -nographic \
      -kernel target/thumbv7m-none-eabi/debug/app
$ # 別ターミナル
$ arm-none-eabi-gdb -q target/thumbv7m-none-eabi/debug/app
Reading symbols from target/thumbv7m-none-eabi/debug/app...done.

(gdb) target remote :3333
Remote debugging using :3333
Reset () at src/main.rs:8
8       pub unsafe extern "C" fn Reset() -> ! {

(gdb) # SPがベクタテーブルにプログラムした初期値を持っています
(gdb) print/x $sp
$1 = 0x20010000

(gdb) step
9           let _x = 42;

(gdb) step
12          loop {}

(gdb) # 次にスタック変数の`_x`を調査します
(gdb) print _x
$2 = 42

(gdb) print &_x
$3 = (i32 *) 0x2000fffc

(gdb) quit