# 第6章: メモリ保護 MPU(Memory Protection Unit:アクセスしてよいメモリの範囲を CPU が見張る仕組み)を使ってタスク同士のメモリを守り、暴走を検出します。 :::{admonition} この章で分かること: アプリが落ちても、OS が落ちないのはなぜ? :class: tip PC では、アプリが 1 つ落ちても、OS やほかのアプリは動き続けます。 OS が、おかしなことをしたプログラムだけを終わらせているからです。 この章では、ゼロ除算や、存在しないアドレスへの書き込みで CPU が起こす例外 (フォールト)を OS が受け止めて、起こしたタスクだけを止めます。 ほかのタスクは何事もなかったように動き続けます。 ::: ## サンプルコード - ディレクトリ: `docs/os-on-arduino/code/06_memory_protection` - ビルドと書き込み: ```bash uv run pio run -d 06_memory_protection -t upload uv run pio device monitor -b 115200 ``` コードを見る: [docs/os-on-arduino/code/06_memory_protection](https://github.com/iory/learning-os-from-arduino/tree/main/docs/os-on-arduino/code/06_memory_protection) ## ソースコード この章のスケッチです。ファイル名をクリックすると開きます。コードは原稿リポジトリから自動で同期されているので、ここに出ているものが常に最新です。 :::{dropdown} `src/main.cpp` — 141 行 :icon: code [GitHub で開く](https://github.com/iory/learning-os-from-arduino/blob/main/docs/os-on-arduino/code/06_memory_protection/src/main.cpp) ```{literalinclude} ../../../../code/06_memory_protection/src/main.cpp :language: cpp :linenos: ``` ::: :::{dropdown} `include/os_fault.h` — 46 行 :icon: code [GitHub で開く](https://github.com/iory/learning-os-from-arduino/blob/main/docs/os-on-arduino/code/06_memory_protection/include/os_fault.h) ```{literalinclude} ../../../../code/06_memory_protection/include/os_fault.h :language: cpp :linenos: ``` ::: :::{dropdown} `include/os_kernel.h` — 74 行 :icon: code [GitHub で開く](https://github.com/iory/learning-os-from-arduino/blob/main/docs/os-on-arduino/code/06_memory_protection/include/os_kernel.h) ```{literalinclude} ../../../../code/06_memory_protection/include/os_kernel.h :language: cpp :linenos: ``` ::: :::{dropdown} `src/os_fault.cpp` — 170 行 :icon: code [GitHub で開く](https://github.com/iory/learning-os-from-arduino/blob/main/docs/os-on-arduino/code/06_memory_protection/src/os_fault.cpp) ```{literalinclude} ../../../../code/06_memory_protection/src/os_fault.cpp :language: cpp :linenos: ``` ::: :::{dropdown} `src/os_kernel.cpp` — 219 行 :icon: code [GitHub で開く](https://github.com/iory/learning-os-from-arduino/blob/main/docs/os-on-arduino/code/06_memory_protection/src/os_kernel.cpp) ```{literalinclude} ../../../../code/06_memory_protection/src/os_kernel.cpp :language: cpp :linenos: ``` ::: ## 実機での出力 Arduino UNO R4 WiFi の実機で実際に取得した出力です(macOS / Windows の 両方で同じ結果を確認しています)。手元の出力と見比べてください。 `d` を送ってゼロ除算タスクを作ると、そのタスクだけが `TERMINATED` になり、`Heartbeat` は動き続けます。 コマンドは 1 文字です。`d` はゼロ除算を起こすタスク(`Crash_Div`)を、`m` は 5 秒後に 不正なアドレス(`0xFFFFFFFF`)へ書き込むタスク(`Crash_Mem`)を作ります。`p` はタスクの 一覧を表示します。 ```{figure} ../_static/ch06_fault.gif :name: fig-ch06-fault :width: 100% `p` でタスクを見て、`d` でゼロ除算を起こしたところ。fault のあとも `Heartbeat` が途切れず、`Crash_Div` だけが `TERMINATED` になります ``` ```text ID NAME STATE 0 idle READY 1 LED BLOCKED 2 Heartbeat BLOCKED 3 Shell RUNNING [Heartbeat] 2 Creating div-by-zero crash task... [Crash] Dividing by zero in 3 seconds... [Heartbeat] 3 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!! FAULT DETECTED !!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Task 4 (Crash_Div) crashed! Reason: Divide by zero PC at fault: 0x41BA Task terminated. Other tasks continue. [Heartbeat] 4 [Heartbeat] 5 ID NAME STATE 0 idle READY 1 LED BLOCKED 2 Heartbeat BLOCKED 3 Shell RUNNING 4 Crash_Div TERMINATED [Heartbeat] 6 [Heartbeat] 7 [Heartbeat] 8 [Heartbeat] 9 ``` `PC at fault` の PC(プログラムカウンタ)は、落ちた命令のアドレスです。 ### シミュレータで見る 実機が無くても、同じファームウェアを {doc}`シミュレータ <../getting-started/simulator>` で動かすとこう見えます。 ```{figure} ../_static/sim_ch06_fault.gif :name: fig-ch06-sim :width: 80% `d` を送ると `Crash_Div` タスクがゼロ除算でフォールトを起こし、`FAULT DETECTED` と原因が出ます。そのあとも L(Heartbeat)の点滅と `[Heartbeat]` の出力は続きます。(シミュレータで撮影) ``` (ch06-break)= ## 壊してみる **予想してから**答えを開いてください。1 つめはコードを変えずに、 ブラウザ版の第6章で そのまま試せます。2 つめはコードを変えてビルドし、`firmware.elf` を {ref}`ブラウザ版にドロップ ` します。 :::{note} 答えに載せた出力は、ブラウザ版と同じ QEMU で実際に動かして取ったものです。 実機ではまだ確かめていません。 ::: ### 1. 続けて 5 回落とす `d` を 5 回続けて送り、しばらく待ってから `p` でタスクの一覧を出します。 **予想:** `FAULT DETECTED` は何回出るでしょう。 :::{dropdown} 答え **4 回しか出ません。** 5 回目も `Creating div-by-zero crash task...` とは表示されますが、 そのあとの `[Crash] Dividing by zero in 3 seconds...` が出ません。 ```text Creating div-by-zero crash task... [Crash] Dividing by zero in 3 seconds... Creating div-by-zero crash task... [Crash] Dividing by zero in 3 seconds... [Heartbeat] 1 Creating div-by-zero crash task... [Crash] Dividing by zero in 3 seconds... Creating div-by-zero crash task... [Crash] Dividing by zero in 3 seconds... Creating div-by-zero crash task... [Heartbeat] 2 (FAULT DETECTED が 4 回) ID NAME STATE 0 idle READY 1 LED BLOCKED 2 Heartbeat BLOCKED 3 Shell RUNNING 4 Crash_Div TERMINATED 5 Crash_Div TERMINATED 6 Crash_Div TERMINATED 7 Crash_Div TERMINATED ``` タスクを入れておく表 `g_tasks` は `MAX_TASKS` = 8 個ぶんしかありません。 起動時の 4 つ(idle・LED・Heartbeat・Shell)で半分を使い、残りの 4 つを `Crash_Div` が埋めます。`TERMINATED` になったタスクの場所は再利用されないので、 5 回目の `os_create_task()` は -1 を返して何もしません。シェルは戻り値を 見ていないので、「作ります」とだけ言って、失敗に気づきません。 タスクが終わるたびに場所が減っていくので、この OS は、落ちたタスクを作り直し続けるような 使い方には向きません。 ::: ### 2. ゼロ除算の見張りを外す `src/os_fault.cpp` の `fault_init()` で、ゼロ除算を例外にする設定をコメントにします。 ```diff - SCB->CCR |= SCB_CCR_DIV_0_TRP_Msk; + // SCB->CCR |= SCB_CCR_DIV_0_TRP_Msk; ``` **予想:** `d` を送ると、`100 / 0` はどうなるでしょう。 :::{dropdown} 答え **`FAULT DETECTED` が出ません。** それなのに、`p` で見ると `Crash_Div` は `TERMINATED` になっています。 ```text Creating div-by-zero crash task... [Crash] Dividing by zero in 3 seconds... [Heartbeat] 1 [Heartbeat] 2 [Heartbeat] 3 ID NAME STATE 0 idle READY 1 LED BLOCKED 2 Heartbeat BLOCKED 3 Shell RUNNING 4 Crash_Div TERMINATED ``` Cortex-M の割り算命令は、この設定(`CCR.DIV_0_TRP`)が無いと、0 で割っても 例外を起こさず、答えを 0 にして先へ進みます ([Arm の解説「Divide and Conquer」](https://developer.arm.com/community/arm-community-blogs/b/architectures-and-processors-blog/posts/divide-and-conquer))。 `c` が 0 になったまま `task_crash_divzero()` は最後まで走って return し、 タスクが return したときの受け皿 `task_exit()` が `TERMINATED` にしています。 エラーを出さずに、間違った答えで先へ進んでしまうわけです。 `fault_init()` がわざわざこの設定を入れているのは、このためです。 ::: ## つまずきやすいポイント (読者からの質問に応じて随時追記します。質問は [GitHub Issues](https://github.com/iory/build-your-own-arduino-rtos/issues) へどうぞ)