メモリレイアウト
次のステップは、ターゲットシステムがプログラムを実行できるように、プログラムに正しいメモリレイアウトを持たせることです。 例では、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つのメモリブロックが定義されています。FLASH
とRAM
です。これらは、ターゲットで利用可能な物理メモリと関連しています。
ここで使用されている値は、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