メモリレイアウト
次のステップは、ターゲットシステムがプログラムを実行できるように、プログラムが適切なメモリレイアウトを 持つことを確認することです。この例では、仮想的な Cortex-M3 マイクロコントローラである LM3S6965 を扱います。プログラムはこのデバイス上で動作する唯一のプロセスになるため、 デバイスの初期化も行わなければなりません。
背景情報
Cortex-M デバイスでは、code memory region の先頭に vector table が存在している必要があります。 ベクタテーブルはポインタの配列です。最初の 2 つのポインタはデバイスの起動に必要で、 残りのポインタは例外に関連しています。ここではひとまずそれらを無視します。
リンカはプログラムの最終的なメモリレイアウトを決定しますが、linker scripts を使うことで ある程度これを制御できます。リンカスクリプトがレイアウトに対して提供する制御の粒度 は、セクション のレベルです。セクションとは、連続したメモリ上に配置された シンボル の集合です。 シンボルは、データ(静的変数)であることもあれば、命令(Rust 関数)であることもあります。
すべてのシンボルには、コンパイラによって名前が割り当てられます。Rust 1.28 の時点では、Rust コンパイラが
シンボルに割り当てる名前は _ZN5krate6module8function17he1dfc17c86fe16daE のような形式で、
これをデマングルすると krate::module::function::he1dfc17c86fe16da になります。ここで
krate::module::function は関数または変数のパスであり、he1dfc17c86fe16da は何らかの
ハッシュです。Rust コンパイラは各シンボルをそれぞれ固有のセクションに配置します。たとえば、先ほどの
シンボルは .text._ZN5krate6module8function17he1dfc17c86fe16daE という名前のセクションに
配置されます。
コンパイラが生成するこれらのシンボル名とセクション名は、Rust コンパイラの異なるリリース間で 一定に保たれる保証はありません。しかし、言語仕様により、以下の属性を通じてシンボル名と セクション配置を制御できます。
#[unsafe(export_name = "foo")]はシンボル名をfooに設定します。#[unsafe(no_mangle)]は、関数または変数名(完全なパスではなく)をそのシンボル名として使うことを意味します。#[unsafe(no_mangle)] fn bar()はbarという名前のシンボルを生成します。#[unsafe(link_section = ".bar")]は、そのシンボルを.barという名前のセクションに配置します。
これらの属性により、プログラムの安定した ABI を公開し、それをリンカスクリプトで利用できます。 これらの属性にはそれぞれ、プログラム内で守らなければならない安全性上の保証があります。具体的には次のとおりです。
#[unsafe(no_mangle)]は、crate、module、変数または 関数名や引数に関係なく、シンボル名を固定します。この属性を使うと、同じ名前のシンボルを 複数作成できてしまい、未定義動作につながる可能性があります。ユーザーは、そのシンボルが ほかのものと衝突しないことを保証しなければなりません。#[unsafe(export_name = "foo")]もシンボル名を固定します。ユーザーは、 選択したシンボル名(この場合はfoo)がプログラム内のほかのどのシンボルとも衝突しないことを保証しなければなりません。#[unsafe(link_section = ".bar")]は、データやコードを、それらを想定していないメモリセクションに 配置してしまう可能性があります。たとえば、可変データを読み取り専用領域に置いたり、ターゲットデバイス上に存在しない メモリ領域に置いたりすることがあります。
Rust 側
前述のとおり、Cortex-M デバイスでは、ベクタテーブルの最初の 2 つのエントリを 埋める必要があります。1 つ目であるスタックポインタの初期値は、リンカスクリプトだけで 設定できます。2 つ目であるリセットベクタは、Rust コードで作成し、 リンカスクリプトを使って正しい位置に配置する必要があります。
リセットベクタは、リセットハンドラを指すポインタです。リセットハンドラは、システムリセット後、
または最初の電源投入後にデバイスが実行する関数です。リセットハンドラは常にハードウェアの
コールスタックにおける最初のスタックフレームです。そのため、そこから return する先の
スタックフレームがほかに存在せず、そこからの return は未定義動作です。リセットハンドラが
決して return しないことは、それを発散関数にすることで強制できます。発散関数とは、
シグネチャが fn(/* .. */) -> ! の関数です。
#![allow(unused)]
fn main() {
#[unsafe(no_mangle)]
pub unsafe extern "C" fn Reset() -> ! {
let _x = 42;
// can't return so we go into an infinite loop here
loop {}
}
// The reset vector, a pointer into the reset handler
#[unsafe(link_section = ".vector_table.reset_vector")]
#[unsafe(no_mangle)]
pub static RESET_VECTOR: unsafe extern "C" fn() -> ! = Reset;
}
ここではハードウェアが特定の形式を期待しているため、extern "C" を使って、この関数を
不安定な Rust ABI ではなく C ABI でコンパイラに生成させることで、それに従っています。
リンカスクリプトからリセットハンドラとリセットベクタを参照するには、それらが安定した
シンボル名を持つ必要があるため、#[unsafe(no_mangle)] を使います。RESET_VECTOR の位置は
細かく制御する必要があるので、既知のセクション .vector_table.reset_vector に配置します。
リセットハンドラ自身である Reset の正確な位置は重要ではありません。こちらは単に、
コンパイラがデフォルトで生成するセクションのままにしておきます。
リンカは、入力オブジェクトファイルの一覧をたどる際に、内部リンケージを持つシンボル
(内部シンボルとも呼ばれます)を無視します。そのため、これら 2 つのシンボルには外部リンケージが
必要です。Rust でシンボルを外部にする唯一の方法は、対応するアイテムを public(pub)にし、
かつ 到達可能 にすることです(アイテムと crate のルートの間に private な module があってはいけません)。
リンカスクリプト側
ベクタテーブルを正しい位置に配置する最小限のリンカスクリプトを以下に示します。 順に見ていきましょう。
$ cat link.x
/* Memory layout of the LM3S6965 microcontroller */
/* 1K = 1 KiBi = 1024 bytes */
MEMORY
{
FLASH : ORIGIN = 0x00000000, LENGTH = 256K
RAM : ORIGIN = 0x20000000, LENGTH = 64K
}
/* The entry point is the reset handler */
ENTRY(Reset);
EXTERN(RESET_VECTOR);
SECTIONS
{
.vector_table ORIGIN(FLASH) :
{
/* First entry: initial Stack Pointer value */
LONG(ORIGIN(RAM) + LENGTH(RAM));
/* Second entry: reset vector */
KEEP(*(.vector_table.reset_vector));
} > FLASH
.text :
{
*(.text .text.*);
} > FLASH
/DISCARD/ :
{
*(.ARM.exidx .ARM.exidx.*);
}
}
MEMORY
リンカスクリプトのこのセクションでは、ターゲット内のメモリブロックの位置とサイズを記述します。
FLASH と RAM という 2 つのメモリブロックが定義されています。これらはターゲットで利用可能な
物理メモリに対応しています。ここで使われている値は、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 の後に配置します。
出力 .vector_table セクションには次のものが含まれます:
/* First entry: initial Stack Pointer value */
LONG(ORIGIN(RAM) + LENGTH(RAM));
(コール)スタックは RAM の末尾に配置します(スタックは フルディセンディング であり、小さい
アドレスの方向へ成長します)。そのため、RAM の末尾アドレスが初期スタックポインタ(SP)値として使用されます。
そのアドレスは、RAM メモリブロックに対して入力した情報を使って、リンカスクリプト自身の中で計算されます。
/* Second entry: reset vector */
KEEP(*(.vector_table.reset_vector));
次に、KEEP を使って、.vector_table.reset_vector という名前のすべての入力セクションを
初期 SP 値の直後にリンカが挿入するよう強制します。そのセクションに配置されているシンボルは
RESET_VECTOR だけなので、これにより実質的に RESET_VECTOR がベクターテーブルの 2 番目に配置されます。
出力 .text セクションには次のものが含まれます:
*(.text .text.*);
これには、.text および .text.* という名前のすべての入力セクションが含まれます。未使用の
セクションをリンカに破棄させるため、ここでは KEEP を使っていないことに注意してください。
最後に、特別な /DISCARD/ セクションを使って、
*(.ARM.exidx .ARM.exidx.*);
という名前の入力セクションを破棄します。これらのセクションは例外処理に関連していますが、panic 時に スタックアンワインドは行わず、Flash メモリの容量も消費するため、単に破棄します。
すべてをまとめる
これでアプリケーションをリンクできます。参考までに、完全な Rust プログラムは次のとおりです:
#![allow(unused)]
#![no_main]
#![no_std]
fn main() {
use core::panic::PanicInfo;
// The reset handler
#[unsafe(no_mangle)]
pub unsafe extern "C" fn Reset() -> ! {
let _x = 42;
// can't return so we go into an infinite loop here
loop {}
}
// The reset vector, a pointer into the reset handler
#[unsafe(link_section = ".vector_table.reset_vector")]
#[unsafe(no_mangle)]
pub static RESET_VECTOR: unsafe extern "C" fn() -> ! = Reset;
#[panic_handler]
fn panic(_panic: &PanicInfo<'_>) -> ! {
loop {}
}
}
リンカに自分たちのリンカスクリプトを使わせるために、リンクプロセスを少し調整する必要があります。これは
rustc に -C link-arg フラグを渡すことで行います。これは cargo-rustc または
cargo-build で行えます。
重要: このコマンドを実行する前に、前のセクションの最後で追加した .cargo/config.toml ファイルがあることを
確認してください。
cargo-rustc サブコマンドを使う場合:
$ cargo rustc -- -C link-arg=-Tlink.x
あるいは、.cargo/config.toml で rustflags を設定して、引き続き
cargo-build サブコマンドを使うこともできます。ここでは後者を使います。こちらの方が
cargo-binutils との統合がよりうまく行えるためです。
# .cargo/config.toml を次の内容になるように変更する
$ cat .cargo/config.toml
[target.thumbv7m-none-eabi]
rustflags = ["-C", "link-arg=-Tlink.x"]
[build]
target = "thumbv7m-none-eabi"
[target.thumbv7m-none-eabi] の部分は、これらのフラグがそのターゲットへクロスコンパイルするときにのみ使用されることを示しています。
確認する
では、出力バイナリを調べて、メモリレイアウトが意図したとおりになっていることを確認しましょう
(これには cargo-binutils が必要です):
$ cargo objdump --bin app -- -d --no-show-raw-insn --print-imm-hex
app: file format elf32-littlearm
Disassembly of section .text:
00000008 <Reset>:
8: push {r7, lr}
a: mov r7, sp
c: sub sp, #0x4
e: movs r0, #0x2a
10: str r0, [sp]
12: b 0x14 <Reset+0xc> @ imm = #-0x2
14: b 0x14 <Reset+0xc> @ imm = #-0x4
これは .text セクションの逆アセンブル結果です。Reset という名前のリセットハンドラが、
アドレス 0x8 に配置されていることがわかります。
$ cargo objdump --bin app -- -s --section .vector_table
app: file format elf32-littlearm
Contents of section .vector_table:
0000 00000120 09000000 ... ....
これは .vector_table セクションの内容を示しています。セクションがアドレス 0x0 から始まり、
セクションの最初のワードが 0x2001_0000 であることがわかります(objdump の出力は
リトルエンディアン形式です)。これは初期 SP 値であり、RAM の末尾アドレスと一致します。2 番目の
ワードは 0x9 です。これはリセットハンドラの thumb モード アドレスです。関数を
thumb モードで実行する場合、そのアドレスの最下位ビットは 1 に設定されます。
テストする
このプログラムは有効な LM3S6965 プログラムです。仮想マイクロコントローラ(QEMU)上で実行して 動作をテストできます。
$ # このプログラムはブロックします
$ 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 *) 0x2000fff4
(gdb) quit