ARM Cortex-R向けRustのサンプル実装について調査してみる
はじめに
たまたまいい資料が見つかったので(参考資料)を見つけましたのでそれについて内容をまとめてみました。
前提事項
実装に関しては以下の18c2337の実装をもとに記事を作成しました。
参考資料
- Github上のリポジトリ
- Ferrous Systems上のサイト
概要
Ferrous Systemsによればミッションクリティカルかつセーフティクリティカルなリアルタイムシステム用にARM Cortex-Mの実装を参考に実装したそうです。明示的には記載されていませんが[
safety-critical-rust-consortium]([
safety-critical-rust-consortium] "https://github.com/rustfoundation/safety-critical-rust-consortium")の活動ともリンクしている可能性が高いです。
リポジトリの構成
リポジトリの構成としては以下のようになっています。
- .cargo
- .github
- .vscode
- aarch32-cpu … キャッシュやMMU、割り込みの操作などの実装を内包、いわゆるHAL層、Cortex-Mでいうところのcortex-mクレート
- aarch32-rt-macros … aarch32-rtの実装をもとに作成したマクロを内包
- aarch32-rt … ベクターテーブル等の実装あり、Cortex-Mでいうところのcortex-m-rtクレート
- arm-targets
- examples - サンプルアプリ
- .gitattributes
- .gitignore
- Cargo.toml
- LICENSE-APACHE
- LICENSE-MIT
- README.md
- justfile
- tests.sh
対応環境
'examples'を除いている見ると現状はMSP3-AN536とversatileabというボードをターゲットとしておりQEMU上でも動作可能であるようです。今回はMSP2-AN506の実装を深掘りしていこうと思います。
実装
エントリーポイントとベクターテーブル
リンカスクリプト(aarch32-rt/link.x)を見たところ、エントリーポイントはstartとなっているようです。
aarch32-rt/link.x
INCLUDE memory.x
ENTRY(_start);
EXTERN(_vector_table);
EXTERN(_start);
EXTERN(_default_handler);
~(中略)~
/* Weak aliases for ASM default handlers */
PROVIDE(_start = _default_start);
PROVIDE(_asm_undefined_handler = _asm_default_undefined_handler);
PROVIDE(_asm_svc_handler = _asm_default_svc_handler);
PROVIDE(_asm_prefetch_abort_handler = _asm_default_prefetch_abort_handler);
PROVIDE(_asm_data_abort_handler = _asm_default_data_abort_handler);
PROVIDE(_asm_irq_handler = _asm_default_irq_handler);
PROVIDE(_asm_fiq_handler = _asm_default_fiq_handler);
/* Weak aliases for C default handlers */
PROVIDE(_undefined_handler = _default_handler);
PROVIDE(_svc_handler = _default_handler);
PROVIDE(_prefetch_abort_handler = _default_handler);
PROVIDE(_data_abort_handler = _default_handler);
PROVIDE(_irq_handler = _default_handler);
/* There is no default C-language FIQ handler */
そこから検索するとベクターテーブルもみつかりました。
aarch32-rt/src/lib.rs
// The Interrupt Vector Table, and some default assembly-language handler.
// Needs to be aligned to 5bits/2^5 to be stored correctly in VBAR
#[cfg(target_arch = "arm")]
core::arch::global_asm!(
r#"
.section .vector_table,"ax",%progbits
.arm
.global _vector_table
.type _vector_table, %function
.align 5
_vector_table:
ldr pc, =_start
ldr pc, =_asm_undefined_handler
ldr pc, =_asm_svc_handler
ldr pc, =_asm_prefetch_abort_handler
ldr pc, =_asm_data_abort_handler
nop
ldr pc, =_asm_irq_handler
ldr pc, =_asm_fiq_handler
.size _vector_table, . - _vector_table
"#
);
スタートアップコード
スタートアップコードはベクターテーブルと同じaarch32-rt/src/lib.rs内にあります。この中でEL2としての設定、EL1の設定を終えkmainなるシンボル(おそらくEL1のエントリーポイント、kernel main)にジャンプしています。
aarch32-rt/src/lib.rs
// Start-up code for Armv8-R.
//
// There's only one Armv8-R CPU (the Cortex-R52) and the FPU is mandatory, so we
// always enable it.
//
// We boot into EL2, set up a stack pointer, and run `kmain` in EL1.
#[cfg(arm_architecture = "v8-r")]
core::arch::global_asm!(
r#"
// Work around https://github.com/rust-lang/rust/issues/127269
.fpu vfp2
.section .text.default_start
.arm
.global _default_start
.type _default_start, %function
_default_start:
// Are we in EL2? If not, skip the EL2 setup portion
mrs r0, cpsr
and r0, r0, 0x1F
cmp r0, {cpsr_mode_hyp}
bne 1f
// Set stack pointer
ldr sp, =_hyp_stack
// Set the HVBAR (for EL2) to _vector_table
ldr r1, =_vector_table
mcr p15, 4, r1, c12, c0, 0
// Configure HACTLR to let us enter EL1
mrc p15, 4, r1, c1, c0, 1
mov r2, {hactlr_bits}
orr r1, r1, r2
mcr p15, 4, r1, c1, c0, 1
// Program the SPSR - enter system mode (0x1F) in Arm mode with IRQ, FIQ masked
mov r1, {sys_mode}
msr spsr_hyp, r1
adr r1, 1f
msr elr_hyp, r1
dsb
isb
eret
1:
// Set up stacks.
bl _stack_setup_preallocated
// Set the VBAR (for EL1) to _vector_table. NB: This isn't required on
// Armv7-R because that only supports 'low' (default) or 'high'.
ldr r0, =_vector_table
mcr p15, 0, r0, c12, c0, 0
// Init .data and .bss
bl _init_segments
"#,
fpu_enable!(),
r#"
// Zero all registers before calling kmain
mov r0, 0
mov r1, 0
mov r2, 0
mov r3, 0
mov r4, 0
mov r5, 0
mov r6, 0
mov r7, 0
mov r8, 0
mov r9, 0
mov r10, 0
mov r11, 0
mov r12, 0
// Jump to application
bl kmain
// In case the application returns, loop forever
b .
.size _default_start, . - _default_start
"#,
cpsr_mode_hyp = const ProcessorMode::Hyp as u8,
hactlr_bits = const {
Hactlr::new_with_raw_value(0)
.with_cpuactlr(true)
.with_cdbgdci(true)
.with_flashifregionr(true)
.with_periphpregionr(true)
.with_qosr(true)
.with_bustimeoutr(true)
.with_intmonr(true)
.with_err(true)
.with_testr1(true)
.raw_value()
},
sys_mode = const {
Cpsr::new_with_raw_value(0)
.with_mode(ProcessorMode::Sys)
.with_i(true)
.with_f(true)
.raw_value()
}
);
kmain自体はユーザが作成することを期待しています。
aarch32-rt/src/lib.rs
//! ## C-Compatible Functions
//!
//! ### Main Function
//!
//! The symbol `kmain` should be an `extern "C"` function. It is called in SYS
//! mode after all the global variables have been initialised. There is no
//! default - this function is mandatory.
//!
//! ```rust
//! #[unsafe(no_mangle)]
//! extern "C" fn kmain() -> ! {
//! loop { }
//! }
//! ```
//!
//! You can also create a 'kmain' function by using the `#[entry]` attribute on
//! a normal Rust function. The function will be renamed in such a way that the
//! start-up assembly code can find it, but normal Rust code cannot. Therefore
//! you can be assured that the function will only be called once (unless someone
//! resorts to `unsafe` Rust to import the `kmain` symbol as an `extern "C" fn`).
サンプルアプリ
examples/mps3-an536/src/bin内に多数あります。今回は一例としてexamples/mps3-an536/src/bin/el2_hello.rsについてみていきたいと思います。
このソースはEL2で実行するためのサンプルでありおそらくハイパーバイザーを作成することを意図してその設定の仕方などを記載していると思われます。
先ほど説明した通り、すでにスタートアップルーチン自体は存在していましたがこのサンプルではEL2の特殊な設定が必要なため、startをオーバーライドして定義しています。ここからkmainにジャンプしてkmainからリターンしたら無限ループに入る実装となっています。
examples/mps3-an536/src/bin/el2_hello.rs
// Provide a custom `_start` function that sets us up in EL2 mode, with a
// stack.
//
// Unlike the default routine, it does not initialise any other stacks, or
// switch to EL1 mode.
core::arch::global_asm!(
r#"
// Work around https://github.com/rust-lang/rust/issues/127269
.fpu vfp3-d16
.section .text.start
.global _start
.type _start, %function
_start:
// Set stack pointer
ldr sp, =_hyp_stack
// Set the HVBAR (for EL2) to _vector_table
ldr r1, =_vector_table
mcr p15, 4, r1, c12, c0, 0
// Configure HACTLR to let us enter EL1
mrc p15, 4, r1, c1, c0, 1
mov r2, {hactlr_bits}
orr r1, r1, r2
mcr p15, 4, r1, c1, c0, 1
// Init .data and .bss
bl _init_segments
// Allow VFP coprocessor access
mrc p15, 0, r0, c1, c0, 2
orr r0, r0, #0xF00000
mcr p15, 0, r0, c1, c0, 2
// Enable VFP
mov r0, #0x40000000
vmsr fpexc, r0
// Zero all registers before calling kmain
mov r0, 0
mov r1, 0
mov r2, 0
mov r3, 0
mov r4, 0
mov r5, 0
mov r6, 0
mov r7, 0
mov r8, 0
mov r9, 0
mov r10, 0
mov r11, 0
mov r12, 0
// Jump to application
bl kmain
// In case the application returns, loop forever
b .
.size _start, . - _start
"#,
hactlr_bits = const {
Hactlr::new_with_raw_value(0)
.with_cpuactlr(true)
.with_cdbgdci(true)
.with_flashifregionr(true)
.with_periphpregionr(true)
.with_qosr(true)
.with_bustimeoutr(true)
.with_intmonr(true)
.with_err(true)
.with_testr1(true)
.raw_value()
},
);
Discussion