結局またハマってしもたんやな、スタックだけに
※この記事は以下の記事から続くものです。
STM32マイコンでArm v7-Mの特権状態まわりについて勉強した【前編】
概要
以下の本を読み進めて、5章プロセス切り替えでカーネル(特権状態)←→アプリケーション(非特権状態)の切り替えを行うところまで実装しました。
(めちゃくちゃ参考にさせていただいております、ありがとうございます)
Rustで始める自作組込みOS入門
本の中に「特権状態」「非特権状態」やSVCallといったOSっぽい(?)概念が出てきたので、ついでにそこを深く掘り下げてドキュメントを読み、実装することで勉強しようと思いました。
目指すゴールとしては
- 非特権状態で実行するアプリケーションからはGPIO操作をできない(=Lチカができない)ようにする
- アプリケーションからGPIOを操作する(=LEDをつける)場合は、SVCallでカーネルにそ操作してもらう
というように、非特権状態のアプリケーションからSVCallをしてLチカをすることとしました。
やってみると結構色々やらないといけないことがあったので、備忘録かねて分割して記事を書くことにしました。
本記事では【後編】として、「非特権状態で実行するアプリケーションからSVCallしてGPIOを代わりに操作してもらう」ようにすることとしました。
ですので、主にSVCallやそのハンドラについて書きます。
環境
パソコン:MacOS Mojave 10.14.6
使用言語:Rust
ボード:STM32 NUCLEO-F446RE
エディタ:VSCode
デバッグ環境: VSCode(Cortex-Debugプラグイン導入) 背景知識:やや組込み初心者(基礎の基礎は知ってるものの基礎以上はわからない)
以前に書いた以下の記事と同じ環境です。
STM32マイコンのレジスタを叩いてLチカしようとしたらどハマりした
かつ、LEDの点灯にはにはこの記事で紹介したled.rsを使用します。
##学んだこと
###SVCallについて
####SVCall(SuperVisor Call)
SVCall(SuperVisor Call、スーパバイザーコール)は、アプリケーションなど非特権のプログラムが、特権状態で実行されるカーネルに対し、特権状態を必要とする操作・サービスを行うよう要求するものです。
Arm v7-Mでは、SVC
命令が用意されており、この命令を実行することでSVCall
例外を発生させて割り込みをかけ、特権を持つカーネルプログラムに実行権を渡すことができます。
####SVC
命令
svc
命令については、A7.7.178に説明が掲載されています。
svc
命令を実行する際には、以下のようにインラインアセンブラを用い、1つの数字を引数として与えてやった上で実行します。
以下のコードでは、0が与えてやっている引数です。
unsafe { asm!("svc 0"::::"volatile"); }
(これvolatile指定する必要あるんかな)
使用しているCortex-M4で用いられている16ビットのThumb命令セットにおいては、svc
命令は以下のビット列になります。
(図:ARMv7-M Architecture Reference Manual P.A7-409より)
このうち、imm8のフィールドに先ほど与えた引数が含まれます。
このimm8のフィールドの使い方があまりわからず困っていたのですが、以下のページでarmのドキュメントを検索し、
http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0471ej/index.html
「svc」で出てくるドキュメントのうち、以下のものの記述がわかりやすかったです。
(多分あまり関係ない、古いツールの使い方マニュアルなのですがArm v7-MのSVCについて書いてたので・・・)
RealView® Compilation Tools バージョン 4.0デベロッパガイ ド
このドキュメントの6.2.8にSVCについて詳しく書かれた項目があります。
そのうち、「呼び出すSVCの決定」というところにこのような説明文があります。
SVC ハンドラは起動時に、 どのSVCが呼び出されているかを特定する必要があります。 この情報は、 図 6-4 に示すように命令自体の0 ~ 23 ビットに保存するか、1つの整数レジスタに渡されます。
またこのようにも書いてあります。
SVC ハンドラは、 まず例外を発生させたSVC命令をレジスタにロードする必要があります。
(ドキュメント内では32bitのarm命令について書かれており、その場合imm
は24ビットになっています)
ここから、imm
はSVCallの目的を識別するために使うものであること、値を読むにはSVC
を発生させた命令を読み込む(か、あらかじめレジスタに値を入れておいてそれを読む)かをしないといけないようです。
今回はせっかくなのでこのSVC
命令に値を埋め込むことにしました。
ここからしばらく調べたのですが、やはりこのimmの値がどこか特定のレジスタ・メモリに保存されるわけではなさそうなので、SVC
を発生させた命令を取得してそこから値を得ることとします。
また、上のドキュメントではlr
(リンクレジスタ)からsvc
命令の値を読み取るやり方が載っていますが、うまくいかなかったので他の方法を探す必要がありました。
###例外処理と例外処理からの復帰について
実装をするときに今回もだいぶはまってしまったのですが、SVCallで例外処理が始まるとき、またそこから復帰するときの処理・動作がわかっていなかったためでした。
Rustで始める自作組込みOS入門」の記述と重複してしまうところもあり恐縮なのですが、自分なりに理解したことを説明します。
####スタック・スタックポインタ
メモリの一領域に、プログラム実行時の一時変数などを保存するためのスタック領域というものが設けられています。
スタックについての細かな説明は割愛しますが、データを保存する際はすでにあるものの上に積み重ね、出すときは一番上にあるものから順に出すというようなものです。
そして、このスタック領域の一番上(一番最後に入ったデータの位置、かつ、次にデータが入るべき場所の一つ下の位置)を差すポインタをスタックポインタ(SP,Stack Pointer)といいます。
Arm v7-Mの場合は、主にカーネルのプログラムが使用するMSP(Main Stack Pointer)と、主にアプリケーションのプログラムが使用するPSP(Process Stack Pointer)の二つを同時に保持し、どちらを使うかを切り替えるということができます。
また注意点として、このアーキテクチャにおいてはスタックに溜まるデータが増えるについてスタックポインタの値が小さくなるということも重要です。
例えば、最初スタックポインタが0xF8を差していたとします。この状態で32bitの数値をスタックに格納すると、スタックポインタは0xF8から0xF4とその値が減る方向に動き、スタックに入れた数値はは0xF4のアドレスに格納されます。
このスタックポインタが例外発生時および例外からの復帰時に非常に重要なのですが、それに気づかずテキトーにプログラムを書いていたため見事沼にはまってしまいました。
####例外発生時
SVC
などの例外が発生したとき、例外処理が終わった後例外発生前の状況が復帰できるよう、プロセッサ持っているレジスタの一部がメモリに自動的に退避されます。
SVC
命令を実行すると設定したSVCallのハンドラに実行権が移りますが、その前にプロセッサがレジスタの値を、以下の図のようにスタック内に退避してくれます。
(図:ARM v7-M Architecture Reference Manual Figure.B1-3)
Original SPと指された位置がもともとスタックポインタがあった場所、そこから(SVCなど)例外発生により、レジスタの情報がスタックに格納され、その後New SPと指された場所にスタックポインタの差す先が移動します。
これらのデータは、SVCを呼び出したプログラムがMSPを使っている場合はMSPの、PSPを使っている場合にはPSPの差す領域に格納されます。
上の図にあるように、退避されるデータの中には復帰後に使うプログラムカウンタの値、つまり例外を発生させるSVC
の次の命令のアドレスが格納されています。
そのため、SVC
命令のargを取得する際は、ここをのぞいて命令を取っててやると良さそうです。
例外発生時の詳しい動作については、Arv b7-MのマニュアルのB1.5.6「Exception entry behabior」に記載があります。
####例外復帰時
例外復帰時の動作については、Arm v7-MマニュアルのB1.5.8に書かれています。
そこには、次のように書かれています。
An exception return occurs when the processor is in Handler mode and one of the following instructions loads a value of 0xFXXXXXXX into the PC:
• POP/LDM that includes loading the PC.
• LDR with PC as a destination.
• BX with any register.
つまり、上に書いてある3つの命令のうちどれかによって、PC(プログラムカウンタ)に0xFXXXXXXXを書き込もうとすると、それをきっかけに例外からの復帰処理が始まるようです。
(今回はRustで始める自作組込みOS入門」に倣い、bx
で復帰することにします。)
bx
命令はarm v7-MマニュアルのA7.7.20に説明があります。
引数に与えるレジスタの値にジャンプする(つまりその値をPCに読み込ませる)、かつ命令セットの切り替えを行うことができる、かつ例外からの復帰処理に使うことができる命令です。
(ただarm v7-MではARM命令セットはサポートされていないため実質命令セットの切り替えは行えない、らしいです)
bx
命令などでPCに0xFXXXXXXXを書き込もうとすると、プロセッサがそれを阻止し、代わりに書き込もうとした値をEXC_RETURN
として、その値に応じて実行モードの切り替えと復帰時・復帰後に利用するスタックの指定を行います。
「復帰時」に利用すると書きましたが、例外処理開始時にスタックから値を退避させたのと同じく、例外復帰時にはスタックに退避した値をレジスタに戻します。
その際、レジスタに戻す値(復帰時に実行する命令のアドレスも含む)を取ってくるスタックがここで選ぶスタックであり、この意味で「復帰時」に使用されます。
つまり、EXC_RETURN
の値でプロセススタックを使用するよう指定した場合、プロセススタックに格納された退避データを元にレジスタを復帰させます。
そして僕がハマってしまったポイントでありかつ当たり前のことなのですが、スタックはレジスタの退避データ以外のデータ格納の目的でも用いられます。
つまり、SVCallのハンドラなどの中で何か処理を行う場合、MSPやPSPが全く別の変数を指してしまっている可能性がありますし、
自動的に例外発生時に保存した退避データのところまでポインタを動かしてくれる機能もありません。
全く別のデータを指していた場合、それを元に復帰処理を行い、わけのわからないところに飛んでしまったりFaultが発生したりします。
ので、何かしらの処理を行ってbx
などで例外から復帰する際、復帰後に使うスタックのスタックポインタが退避データの先頭を確かに指していなければいけません。
今回は例外処理をメインスタックで、アプリケーションの処理をプロセススタックで行っており、例外処理中にPSPは動かないので、特にハマることはないと思います。
メインスタックで例外処理を行い、そのあとメインスタックを使う処理に復帰する際などには、処理の前にMSPの値を保持し、bx
での復帰前に戻してやるようなことが必要です。(これを一生懸命やろうとして無限にハマってました。今回の実装では結果的にいらなかったんですが・・・)
というかそのためにスタック切り替えができるのであってそこでハマるような使い方をしてる時点でおかしいのだろうか・・・つらい
EXC_RETURN
の値とそれに応じたプロセッサの対応は、マニュアルのTableB1-8にまとめられています。
今回は、アプリケーションをPSPを使うスレッドモード、で実行したいため、例外復帰時には0xFFFFFFFDをbx
に与えるレジスタに格納すると良さそうです。
ちなみに、本当は例外発生時に退避されないレジスタの値は関数実装の方で退避させ、復帰前には復元してやる必要があるのですが今回はそれを行っていません。
お行儀が悪いですが、復元しなくても大丈夫なほどレジスタを使わないというかとりあえずそれで動いたのでとりあえずパスしました・・・・
###PendSV
スーパバイザーコールを行うもう一つの手段として、PendSVを利用するものがあります。(今回は使用しません)
PendSVについては、Arm v7-MマニュアルのP.A2-33に説明が掲載されています。
これによると、PendSVはISCR
(Innterruupt Control and State Register)の指定ビットに値をセットすることでSVCallをPending(保留中)状態にできるみたいです。
(ICSR
についてはB3.2.4に説明があります。)
つまり、SVCallはSVC命令を呼び出すとすぐ割り込みが発生して(他に優先度の高い割り込みがない場合)SVCallハンドラに実行権が移るのに対し、PendSVはその場ではすぐに移らず、プロセッサが処理可能な状態になった段階でPendSV例外が発生し、割り込みがPendSVハンドラに実行権が移るということみたいですね(?)
SVCallを同期呼び出し、PendSVを非同期呼び出しと説明している文もありました。
・・・すみません、正直あまりよくわかりませんでした
##実装
実装は、「Rustで始める自作組込みOS入門」の「5.プロセス切り替え」までの実装が終わっており、
かつこの記事で書いたledモジュールの実装と、この記事で書いた手順によるMPUの有効化が行われている状態から始めるものとします。
(結構わからんままにあれやこれやと試しながら書いたので、あまりいい実装じゃないかもしれないです)
まず、SVCall例外が発生した時にプロセッサから呼び出されるSVCall
関数を、main.rs
内に書きました。
#[no_mangle]
pub unsafe extern "C" fn SVCall() {
asm!(
"
cmp lr, #0xfffffff9
bne to_handler
mov r0, #1
msr CONTROL, r0
movw lr, #0xfffd
movt lr, #0xffff
bx lr
to_handler:
blx r2
"
::"{r2}"(svc::SvcHandlerMain as u32)::"volatile");
}
アプリに実行権を戻すところは「Rustで始める自作組込みOS入門」に掲載されていたものと同じコードを使用しています。
また、カーネルプログラムに実行権を写すところでは、本においては[to_kernel」であったところは「to_handler」とし、(後述する)新しく作るsvc
モジュールのsvc::SvcHandlerMain
関数にblx
で飛ばすことにしました。
rustのasm!
マクロでレジスタr2にSvcHandlerMainへのポインタを格納しており、blx
ではそのポインタを使用しています。
(r0-r3は例外発生時に退避されるし復帰時に復元されるのでここでこうやって使って大丈夫・・・・だと思っています)
このblx
実行時点では、例外からの復帰処理は起こりません。
続いて、svc.rs
を新しく作り、以下のような実装をしました。
use super::led;
use cortex_m_semihosting::hprintln;
static mut svc_inst : usize = 0;
pub unsafe extern "C" fn SvcHandlerMain() {
// save
asm!(
"
mrs r1, psp
ldr r3, [r1, #24]
ldr r2, [r3, #-2]
":"={r1}"(return_psp_val),"={r2}"(svc_inst):::"volatile"
);
let svc_arg = svc_inst as u8;
hprintln!("svc hander arg: {}",svc_arg);
//handler
match svc_arg {
1 =>{
hprintln!("switching led val").unwrap();
led::switch();
}
_ => hprintln!("the handler for the arg is not defined").unwrap()
}
//returning from svc exception
asm!(
"
mov r0, #1
msr CONTROL, r0
movw lr, #0xfffd
movt lr, #0xffff
bx lr
"::"{r1}"(return_psp_val)::"volatile"
);
}
pub fn switch_led() {
unsafe { asm!("svc 1"::::"volatile"); }
}
少しコードの説明をします。
asm!(
"
mrs r1, psp
ldr r3, [r1, #24]
ldr r2, [r3, #-2]
":"={r1}"(return_psp_val),"={r2}"(svc_inst):::"volatile"
);
let svc_arg = svc_inst as u8;
この部分で、例外発生時にレジスタ情報を格納したプロセススタックから、例外を発生させたSVC
命令を読み出し、argを取得しています。
psp
はシステムで利用する特殊なレジスタ(Application Program Status Register (APSR)というらしいです)の一つであり、
通常通りldr
命令で他のレジスタに値を読んだり、あるいはレジスタ相対アドレッシングに直接利用したりということはできないようです。
ですので、特殊レジスタから汎用レジスタに値を移すmrs
命令でr1レジスタに一度値を移し、その後r1からの相対アドレッシングでSVC
命令を読み出します
一つ目のldr
では、プロセススタックに格納されている復帰アドレスをメモリから読み出し、r3レジスタに格納しています。
上に掲載した図の通り、復帰アドレス(Return Address)はスタックポインタに対し +0x18(= +24)の位置にあるので、即値に24を指定してロードしています。
二つ目のldr
では、直前の命令で読み出したアドレスを元にSVC
例外を発生させたSVC
命令を読み出し、r2レジスタに格納しています。
r3レジスタには復帰アドレスが格納されているので、SVC
命令はこのアドレスの直前にあります。
また、Arm v7-Mで利用するThumb命令セットは16ビット長なので、1つ前の命令を読み出すには即値に-4ではなく-2を指定します。
match svc_arg {
1 =>{
hprintln!("switching led val").unwrap();
led::switch();
}
_ => hprintln!("the handler for the arg is not defined").unwrap()
}
前の段階で読み出したSVC
命令のsvc_arg
とし、この値でパターンマッチをしています。
今回は値が1ならばLEDのOn/Off切り替え、そうでない場合はメッセージを出すようにしました。
//returning from svc exception
asm!(
"
mov r0, #1
msr CONTROL, r0
movw lr, #0xfffd
movt lr, #0xffff
bx lr
"::"{r1}"(return_psp_val)::"volatile"
);
SVCallで要求された処理を行った後、例外処理から復帰しアプリケーションに実行権を戻す部分です。
この部分も結局本に掲載されていたコードとほぼ同じです。
もしスタックポインタが動いてしまっている場合、bx
で復帰する前にスタックポインタの値を適切なものに戻してやる必要がある点に注意が必要です。
(前述の通り、今回はPSPが動かないので不要です)
この実装により、app_main()
から概ね以下のような流れでled::switch
を呼び出します。
上の図では破線矢印が非特権状態での、実線矢印が特権状態での関数呼び出しを表しています。
実行・検証
では、ここまでできたので実際にボード上でプログラムを実行してみたいと思います。
検証用のアプリケーションコード
検証のために非特権状態で実行するアプリケーションを以下のようにします。
extern "C" fn app_main() -> ! {
loop {
unsafe{ asm!("wfi"::::"volatile");}
led::switch();
svc::switch_led();
unsafe{ asm!("svc 0"::::"volatile"); }
}
}
wfi
命令は割り込み待ちのスリープ状態に入るための命令です。
reset関数で1秒毎にSysTick
割り込みを起こすよう設定をしているので、このloopがおおよそ1秒に1度実行されるようになっています。
led::switch()
はGPIOを直接操作する、svc::switch_led
はSVCallして特権状態に切り替えてからGPIOの操作をするようにしています。
検証時にはこの二つの関数のうち片方をコメントアウトします。
MPUが有効である確認
まず、アプリケーションから直接LEDにアクセスすることができないことを確かめます。
これを確かめるために、led::switch()
関数でLEDの点滅を操作しようとしてみます
extern "C" fn app_main() -> ! {
...
led::switch();
//svc::switch_led();
...
}
これを実行すると、led::switch()
の実行段階でHardFault
が発生しました。
これより、非特権状態のアプリケーションからペリフェラルのレジスタに直接アクセスはできなくなっていることがわかります。
SVCのでのペリフェラル操作、およびargを識別している確認
ではいよいよ、svc
を実行してSVCallによってカーネルにLEDを操作してもらうようにします。
またargを正しく識別できていることも確認できるよう、arg=0で2回目のSVCallをするようにします。
extern "C" fn app_main() -> ! {
...
//led::switch();
svc::switch_led();
unsafe{ asm!("svc 0"::::"volatile"); }
...
}
これを実行した結果、コンソール出力が以下のようになりました。
期待通り、0と1のargを識別できていること、またarg=1の時のみLEDの操作をしようとしていることがわかります。
やったぜ!めでたしめでたし
〆
前編書いてる時、後編かける実装できるのか・・・?って思ってたので後編までかけてよかったなあという感じです
最初の方は結構色々やっていて記事書きがいがあるかなあと思ってたのですが、色々整理してるうちに結局もともと本に載っていたコードとあんまり変わらなくなった気がします、ええんか・・・・