※この記事は以下の記事に続きます
STM32マイコンでArm v7-Mの特権状態まわりについて勉強した【後編】
※最初、「特権モード」「非特権モード」と書いていたのですが、これとは別の概念の「スレッドモード」「ハンドラモード」と混同してしまいかねず、不適切な表記だったと思ったので、「特権/非特権状態」に修正しました。
紛らわしいことを書いてすみませんでした。
概要
以下の本を読み進めて、5章プロセス切り替えでカーネル(特権状態)←→アプリケーション(非特権状態)の切り替えを行うところまで実装しました。
(めちゃくちゃ参考にさせていただいております、ありがとうございます)
Rustで始める自作組込みOS入門
本の中に「特権状態」「非特権状態」やSVCallといったOSっぽい(?)概念が出てきたので、ついでにそこを深く掘り下げてドキュメントを読み、実装することで勉強しようと思いました。
目指すゴールとしては
- 非特権状態で実行するアプリケーションからはGPIO操作をできない(=Lチカができない)ようにする
- アプリケーションからGPIOを操作する(=LEDをつける)場合は、SVCallでカーネルにそ操作してもらう
というように、非特権状態のアプリケーションからSVCallをしてLチカをすることとしました。
やってみると結構色々やらないといけないことがあったので、備忘録かねて分割して記事を書くことにしました。
本記事では【前編】として、「非特権状態で実行するアプリケーションからはGPIO操作をできない」ようにすることとしました。
ですので、主に特権状態そのものについてや、メモリアクセス制限をかけるためのMPU(Memory Protection Unit)について書きます。
###環境
- パソコン:MacOS Mojave 10.14.6
- 使用言語:Rust
- ボード:STM32 NUCLEO-F446RE
- エディタ:VSCode
- デバッグ環境: VSCode(Cortex-Debugプラグイン導入)
背景知識:やや組込み初心者(基礎の基礎は知ってるものの基礎以上はわからない)
以前に書いた以下の記事と同じ環境です。
STM32マイコンのレジスタを叩いてLチカしようとしたらどハマりした
かつ、LEDの点灯にはにはこの記事で紹介したled.rs
を使用します。
##学んだこと
特権状態(privileged mode)、およびそれを使う上で必要なMPU(Memory Protection Unit)についてマニュアルを中心に資料を読んで調べました。
今回は主にarm v7-Mのドキュメントを中心に読み込みます。
Arm®v7-M Architecture Reference Manual
###特権状態について
まずは特権状態(privileged mode)と非特権状態(unprivileged mode)について調べます。
arm-v7のドキュメントで見つけた、「A3.6.1 Privilege level access controls for data access」という項目にたどり着いたのでそこを読み進めました。
軽くざっくりと理解すると
- 各メモリ領域について
- 全アクセス拒否
- 特権状態でのアクセスのみ許可
- 全アクセス許可
を定めることができる
- アクセス違反は(MemManage)例外を引き起こす
-
MPU(Memory Protection Unit)が実装されており有効になっている時のみ、特権/非特権によるアクセス制限をかけられる
(Noteの部分に書いてあります)
という感じでした。
(もっと細かく書いてあったのですが必要部分以外読み飛ばしています・・・後日読んで理解し次第追記します)
3.にある通り、特権状態を利用してアクセス保護を行うにはMPUを有効化しないとダメそうでした。
「実装されている」に関しては、使用するF446REボードのマニュアルを読み、MPUが実装されていることを確認しました。
次に「有効にする」必要があるため、Note部分に貼ってあるリンクから「B3.5 Protected Memory System Architecture, PMSAv7」に飛んでMPUについて理解します。
###MPUについて
というわけでMPU(Memory Protection Unit)を有効にし、GPIOの操作をするメモリ領域を特権状態からしかアクセスできないようにします。
(理解しないといけない項目が多すぎたので、目標を達成することを優先しざっくりと理解することとしました。
ので間違った内容・理解があると思います・・・ご指摘いただけると非常にありがたいです)
Arm v7-MのMPUについてはマニュアルの「Protected Memory System Architecture, PMSAv7」に書いてあります。
このマニュアルだけではほとんど理解できいなかったのですが、以下のWebページは具体的なコードもあり、参考になりました。
Setting up the Cortex-M3/4 (ARMv7-M) Memory Protection Unit (MPU) - Stickey Bits -Powered by Feabhas
ざっくり理解すると
- メモリ空間の一部をregionとして設定し、それぞれのregionについてアクセス権などの設定が行える
- regionはこちらで指定してやる必要がある
- MPUを実装しているか否か、およびMPUで管理できるregionの数はプロセッサの実装によって異なる
(ただしArm v7-Mの場合8regionのようです)
また、「B3.5.4 Register support for PMSAv7 in the SCS」にMPUの操作を行うレジスタのアドレスとその詳細が記載されています。
アドレス | 名前 | 読み書き属性 | 説明 |
---|---|---|---|
0xE000ED90 | MPU_TYPE | RO | 実装されているMPUの情報を返す |
0xE000ED94 | MPU_CTRL | RW | MPUの設定をする |
0xE000ED98 | MPU_RNR | RW | RBAR/RASRレジスタで設定するresionの番号を指定する |
0xE000ED9C | MPU_RBAR | RW | regionの開始アドレスを設定する |
0xE000EDA0 | MPU_RASR | RW | regionのサイズやアクセス権限を設定する |
(エイリアス?についても書いてあったのですが省略しています・・・・) |
それぞれ軽く説明すると
####MPU_TYPE
実装されているMPUの情報を返します。
特に、readした時に[15:8]のビットに入っている値はMPUが管理可能なregion数を表しており、MPUがプロセッサに実装されていない場合ここに0が入ります。
Cortex-M4を採用しているボードであれば大丈夫?だとは思うのですが、念の為以下のようなコードで値を読み出して0出ないことを確認しておくと安心です。
let mpu_type_value = read_volatile(0xE000_ED90 as *const u32);
hprintln!("value: {:b}",mpu_type_value);
二進数であれば10000000000
,10進数であれば2048
になるはずです。
MPU_CTRL
MPUの設定を行います。特に有効/無効の切り替えはこのレジスタの[0]ビットに値を書き込んで行います。
ただし、マニュアルのP.B3-636のNoteにある通り、こいつをenableする前にMPU_RASRをちゃんと初期化してあげないといけないみたいです。
(始めこれを正しく指定せずMPUを有効していたのですが、変に制限がかかるらしく非特権状態への切り替え時にHardFaultが発生しました)
MPU_RNR (Region Number Register)
以下二つのレジスタで指定するregionの番号をここで指定します。
例えば、番号3のregionについて領域や権限を設定したい場合、まずこのレジスタの[7:0]に3を書き込み、その後MPU_RBAR
とMPU_RASR
への値の書き込みを行います。
MPU_RBAR (Region Base Address Register)
MPU_RNR
で指定した番号のregionに割り当てるメモリ領域の開始アドレスを、region番号とともに指定します。
入力する値は、[31:5]に開始アドレス(32bit)の下5btを削ったもの、[4:0]に(MPU_RNR
に指定したのと同じ)region番号です。
MPU_RASR (Region Attribute and Size Register)
MPU_RNR
で指定した番号のregionに割り当てるメモリのサイズや、そこのアクセス権限などについて指定します。
指定するregionに対し、非特権状態のアクセスを許すか許さないかの設定もこのレジスタへの書き込みによって行います。
サイズは[5:1]に5ビット数で指定し、2^(指定した数+1)Byteがregionに対し割り当てられます。
これより、今回実装する必要のある操作としては
- CPUのメモリマップを見つつ、regionにメモリ領域を割り当てる
(MPU_RBAR
にその領域の開始アドレスを、MPU_RASR
にその領域のサイズを指定してやる必要があります。) - それぞれのregionに対し、アクセス権を決める
- MPUを有効にする
という感じになります。
##実装
###実装
Rustで始める自作組込みOS入門の5章で特権/非特権の切り替えは実装しているので、実装する必要があるのはMPUを有効にするコードのみです。
led.rs
と同様、ファイルを分けて実装します。
ということでmpu.rs
を作り、以下のように実装します。
use core::ptr::{write_volatile, read_volatile};
use cortex_m_semihosting::hprintln;
const MPU_TYPE_ADDR : usize = 0xE000_ED90;
const MPU_CTRL_ADDR : usize = 0xE000_ED94;
const MPU_RNR_ADDR : usize = 0xE000_ED98;
const MPU_RASR_ADDR : usize = 0xE000_EDA0;
const MPU_RBAR_ADDR : usize = 0xE000_ED9C;
pub fn init() {
hprintln!("enabling MPU");
unsafe {
//disable MPU
write_volatile(MPU_CTRL_ADDR as *mut u32, 0x0000_0000);
// setting on regions ( 8 regions )
write_volatile(MPU_RNR_ADDR as *mut u32, 0x0000_0000);
write_volatile(MPU_RBAR_ADDR as *mut u32, 0x0000_0010);
// XN AP TEX S C B SRD SIZE EN
write_volatile(MPU_RASR_ADDR as *mut u32, 0b000_0_0_011_00_001_0_0_0_00000000_00_11100_1);
write_volatile(MPU_RNR_ADDR as *mut u32, 0x0000_0001);
write_volatile(MPU_RBAR_ADDR as *mut u32, 0x2000_0011);
// XN AP TEX S C B SRD SIZE EN
write_volatile(MPU_RASR_ADDR as *mut u32, 0b000_0_0_011_00_001_0_0_0_00000000_00_11100_1);
write_volatile(MPU_RNR_ADDR as *mut u32, 0x0000_0002);
write_volatile(MPU_RBAR_ADDR as *mut u32, 0x4000_0012);
// XN AP TEX S C B SRD SIZE EN
write_volatile(MPU_RASR_ADDR as *mut u32, 0b000_0_0_001_00_001_0_0_0_00000000_00_11100_1);
write_volatile(MPU_RNR_ADDR as *mut u32, 0x0000_0003);
write_volatile(MPU_RBAR_ADDR as *mut u32, 0x6000_0013);
// XN AP TEX S C B SRD SIZE EN
write_volatile(MPU_RASR_ADDR as *mut u32, 0b000_0_0_011_00_001_0_0_0_00000000_00_11100_1);
write_volatile(MPU_RNR_ADDR as *mut u32, 0x0000_0004);
write_volatile(MPU_RBAR_ADDR as *mut u32, 0x8000_0014);
// XN AP TEX S C B SRD SIZE EN
write_volatile(MPU_RASR_ADDR as *mut u32, 0b000_0_0_011_00_001_0_0_0_00000000_00_11100_1);
write_volatile(MPU_RNR_ADDR as *mut u32, 0x0000_0005);
write_volatile(MPU_RBAR_ADDR as *mut u32, 0xA000_0015);
// XN AP TEX S C B SRD SIZE EN
write_volatile(MPU_RASR_ADDR as *mut u32, 0b000_0_0_011_00_001_0_0_0_00000000_00_11100_1);
write_volatile(MPU_RNR_ADDR as *mut u32, 0x0000_0006);
write_volatile(MPU_RBAR_ADDR as *mut u32, 0xD000_0016);
// XN AP TEX S C B SRD SIZE EN
write_volatile(MPU_RASR_ADDR as *mut u32, 0b000_0_0_011_00_001_0_0_0_00000000_00_11100_1);
write_volatile(MPU_RNR_ADDR as *mut u32, 0x0000_0007);
write_volatile(MPU_RBAR_ADDR as *mut u32, 0xE000_0017);
// XN AP TEX S C B SRD SIZE EN
write_volatile(MPU_RASR_ADDR as *mut u32, 0b000_0_0_011_00_001_0_0_0_00000000_00_11100_1);
//enable MPU
write_volatile(MPU_CTRL_ADDR as *mut u32, 0x0000_0001);
}
}
ざっくり何を書いたかを説明すると
write_volatile(MPU_CTRL_ADDR as *mut u32, 0x0000_0001);
と
write_volatile(MPU_CTRL_ADDR as *mut u32, 0x0000_0001);
各種設定前にMPUを無効化し、設定完了後に有効化しています。
MPU_CTRL
の[0]がenable/disableを設定するビットなので、ここに1を書いてやっています。
[2]と[1]にも設定項目があり、権限周りの設定が変わるのですが、今回は省略します。
write_volatile(MPU_RNR_ADDR as *mut u32, 0x0000_0002);
write_volatile(MPU_RBAR_ADDR as *mut u32, 0x4000_0012);
// XN AP TEX S C B SRD SIZE EN
write_volatile(MPU_RASR_ADDR as *mut u32, 0b000_0_0_001_00_001_0_0_0_00000000_00_11100_1);
このひとかたまりで1regionについての設定を行なっており、mpu.rs
ではこれを8regionに対して行なっています。
わかりやすいよう、region2に関して設定しているコードを抜き出してきました。またMPU_RASR
に入れる値がわかりやすいよう、ここだけ2進数にし、コメントにフィールド名を振っています。
8つのregionは使用しているF446REボードのリファレンスマニュアル↓P.65にあるメモリマップを元に、8つある512MBの領域をそれぞれ割り当てることにしました。
リファレンスマニュアル
SIZEは全て512MBなので512 * 1024 * 1024 = 2^29から28(11100)を指定しています。
今回はGPIOへのアクセスを禁止したいので、0x4000_0000-0x5FFF_FFFFの領域へのアクセスを特権状態での実行時のみに制限します。
上のコードにある通り、region2にはペリフェラル領域が割り当たっているので、非特権状態からのアクセスを禁止するように指定します。
アクセス制限はMPU_RASR
のAP
フィールド
全ての値の意味はArm v7-MのマニュアルのP.B3-642に書いてありますが、一部抜粋すると000
でアクセス全拒否、001
で特権状態のみ読み書き許可、011
で特権/非特権関わらず読み書き許可、になります。
このため、ペリフェラル領域のregionについては001
を指定して非特権状態からのアクセスを拒否し、他のregionは011
にしてアクセス拒否をしないようにしてやっています。
(それってどうやのって感じではありますが・・・)
その他の項目については先ほども紹介した以下のサイトに掲載されていた指定に倣っています。
Setting up the Cortex-M3/4 (ARMv7-M) Memory Protection Unit (MPU) - Stickey Bits -Powered by Feabhas
あとはmain.rs
の中に、このmpu.rs
を利用するコードを書きます
...
mod mpu;
...
#[no_mangle]
pub unsafe extern "C" fn Reset() -> ! {
...
mpu::init();
...
###実行・検証
さてこれで非特権状態で実行するプログラムからGPIO操作ができなくなったのでしょうか。
検証のため、特権状態で実行するReset()
と、非特権状態で実行するapp_main()
について以下のようにしました。
(SVCall()
など省略している部分は「Rustで始める自作組込みOS入門 5章の成果物とほぼ同じです)
...
#[no_mangle]
pub unsafe extern "C" fn Reset() -> ! {
...
mpu::init();
asm!(
"
msr psp, r0
svc 0
"
::"{r0}"(ptr):"r4", "r5", "r6", "r8", "r9", "r10", "r11":"volatile");
hprintln!("Kernel").unwrap();
led::init();
led::turn_on();
loop {}
}
extern "C" fn app_main() -> ! {
hprintln!("App!").unwrap();
led::init();
led::turn_on();
unsafe { asm!("svc 0"::::"volatile"); }
loop {}
}
このコードで実行したところ、LEDは光らず、デバッガで確認するとHardFaultが発生していました。
続いて、app_main()
内のled点灯処理を行う関数をコメントアウトします
...
extern "C" fn app_main() -> ! {
hprintln!("App!").unwrap();
//led::init();
//led::turn_on();
unsafe { asm!("svc 0"::::"volatile"); }
loop {}
}
実行したところ、Reset()
に実行権が戻ってきたあと無事LEDが点灯しました。
ということで、目標であった非特権状態でのGPIOレジスタへのアクセス制限をかけることができました、めでたしめでたし
##〆
今後やること:
- 特権状態についてもっと理解する(詳細を読み飛ばしたので)
- MPUについてもっと理解する(詳細を読み飛ばしたので)
- SVCallでGPIOをいじれるようにする
OSに共通する勉強をしようと思ったら結局Arm特有の問題になってしまった
後編書けるんか・・・・?
##参考文献