連載3-SOLIDのMMU使いこなしー2
第一回で、SOLIDの持つ便利機能を二つご紹介しました。
・NULLポインタアクセス検出
・スタックオーバーフロー検出
今回は、これらの機能の実現方法について、第二回でご紹介したMMU関連機能を使いながら、深堀りをしてご紹介していきます。
1.不当なメモリアクセス
まず、おさらいします。
MMUは、CPUから要求されたアドレスのメモリデータを、バスが解釈できるアドレスに変換するためのテーブルを持っています。
CPUから要求されたアドレス:CPUが解釈しているのは「仮想アドレス(論理アドレスとも呼ぶ)」
バスが解釈できるアドレス:「物理アドレス」
要求された「仮想アドレス」に対応する「物理アドレス」が、テーブルに載っていない場合、対応する物理アドレスに変換できません。
この場合、
「CPU(で動作しているファームウェア)が間違えているに違いない!」
と、MMUは思います。
CPUに連絡してあげなきゃ!
で、「不当アドレスアクセス(データアボート)例外」を発生させます。
MMUの持つアドレス変換テーブルは、それだけではありません。
CPUからアクセス要求される仮想アドレスに対し、アクセス権限についても付属して解釈することができます。
リードのみ許可する、とか、特権モードの時だけアクセスしていいよ、とか。
要求された「仮想アドレス」に対応する「物理アドレス」が、テーブルに載っている場合でも、付属するアクセス属性が一致していないと、対応する物理アドレスに変換できません。
MMUはまた思います。
「CPUに連絡してあげなきゃ!」
で、「不当アドレスアクセス(データアボート)例外」を発生させます。
その例外ベクタで何をするのか、は、ファームウェアの管轄になります。
別スレッドのためのアドレス変換テーブルを登録してもいいです。
そもそもそんなアドレスアクセスする予定なかった、事をプログラマに通知する、でもいいです。
SOLIDは後者です。
「変なアドレスにアクセス来たよ」を、SOLID-IDEの画面で通知します。
アクセス対象アドレスと、アクセスタイプ(リードかライトか)、それからアクセスした命令があるPC値と一緒に表示します。
第一回で、Raspberry Pi4とSOLIDを用いて、NULLポインタアクセスの検出を行ったときのスクリーンショットを再び見てみます。
※Raspberry Pi4のCPUであるBCM2711は、 64ビットのCortex-A72 (ARM v8) ×4なので、アドレスは64ビット表示になります。
アクセス対象アドレスが0番地なら、きっとNULLポインタでアクセスした可能性が高いですね。
そして、それだけでなく、もうひと手間を惜しみません。
その「変なアドレス」が、スタック領域のチョイ先なら、スタック破壊かもしれない、という事を、エラー画面につけたします。
ユーザは、普段、スタックが今どのアドレスにあるのか、異常がない限りあまり気にしません。
いきなり、アドレス0x00000002e0a10ff0に不当なリードアクセスあり!と言われても、ちょっと「え?」となってしまいますよね。
スタック破壊かもしれない、と教えてもらって、その時のスタック範囲やタスクID等をGUIで表示してもらうと、もうそれは一目瞭然ですね。
では、どうやって、MMUはこれらを「不当アドレスアクセスだ」と認識するのでしょうか。
答えは。。。
冒頭に書いたように、
「要求された「仮想アドレス」に対応する「物理アドレス」が、テーブルに載っていない、あるいはアクセス属性が一致しない」
を意図的に作り出しています。
2.NULLポインタアクセス検出実現方法
ここ以降は、Cortex-A9搭載ボード×PARTNER Jet2の組み合わせで見ていきます。
(今回使用するCortex-A9は、32ビットアドレスなので、表示されるアドレスは32ビット表示になります。)
2.1 NULLポインタアクセスするプログラム
まず、サンプルプログラムのroot taskにNULLポインタへのライトアクセスをするコードを書いてみます。
実行してみます。
0番地へのデータアボート例外がライトアクセスで検出されました。
2.2メモリマップデザイナの設定状態
ここでメモリマップデザイナの設定を見てみます。
あれ?
仮想アドレス、0番地付近、登録されてんじゃん!
と思った方、早とちりです。
右横、Descriptionというオプションがあります。
ここで、NOR-FlashROMと登録されています。
ROMです。
リードアクセスのみ、許可されています。
「アクセス属性が一致していない」が作り出され、データアボート例外が発生する、というわけです。
ちなみに、Cortex A9の場合、MMUが識別することができる属性は以下です。
2.3 0番地への不正リードアクセスは?
では、NULLポインタに対しリードアクセスが発生した場合は、捕まえられるのでしょうか。
この例の場合、0番地付近はROMに命令コードがおかれている、というコンフィグレーションで動いています。すなわち、0番地付近に命令フェッチによるメモリリード要求が来ます。
データリードなのか、命令フェッチなのかまでは、MMUではわからないので、リードアクセスに関しては許可します。
したがって、このままではNULLポインタに対してのリードアクセスは、捕まえることができません。
「このままでは」、です。
という事は、
コンフィグレーションを少し考えれば大丈夫です!
0番地付近、通常RTOSが動作しているときは、必要がない事が多いです。
0番地付近にはリセットベクタがありますね。
リセット発生後はMMUがDisableなので、そもそもアドレス変換テーブルは関係ありません。
リセット後、初期化ルーチンでMMUをEnableにする時には、もうプログラムはRAMに展開してRAM上で動いていることが多いです。その際、各ベクタも移動しています。
ですので、アドレス変換テーブルからROM部分は削除しても大丈夫であることが多いです。(もちろん、システムの仕様によります。)
で、もし、リセット発生したら?
またMMUがDisable状態からスタートするので、大丈夫です。
と、いう事で。
ROM部は、アドレス変換テーブルから削除してしまいましょう!
では、NULLポインタ(0番地)をリードしてしまうプログラムを走らせてみます。
NULLポインタへのリードが捕まりました!
3.スタックオーバーフロー検出実現方法
3.1 スタックオーバーフローするプログラム
第一回では、特にタスクを作成せずに、main関数からスタックオーバーフローするような関数をコールしました。
今回は、「スタックオーバーフローあるある」の、
作成タスクに割り当てたスタックが足りない!
を、(サンプルプログラムを拝借して)試してみます。
*あらかじめカーネルコンフィグは生成済です。
タスクの各パラメータは以下です。
では、このタスク内で、スタックをオーバーフローさせてみましょう。
0x8fc00ff8へのライトアクセスで、スタックを突き破っているようです。
左下のメモリウインドウでも、0x8fc0100以前のアドレスはデータを取って来ることができません。
それだけではありません。
「デバッグ例外」ウインドウを良く見てみましょう。
スタックを突き破ってしまったタスクID値も表示されています。
今回、タスク5ですね。
左下のRTOSビュアーを見てみると、タスク5は、Running状態にあり、chilld_taskというエントリ名のタスクであることがわかります。
このタスクのスタックが狭すぎるのか、タスク内関数に問題があるのか、、、
デバッグの際、このように、疑わしい範囲が絞られるのは有難いです。
本機能は、SOLIDのユーザガイド内に、「スタックフェンス」という言葉で記載されています。
よろしければご参照ください。
3.2 メモリマップデザイナの設定状態
メモリマップデザイナの設定を見てみましょう。
OSSTACKという名称で登録されています。
仮想アドレスは、0x8fc00000から4Mbyte分登録されています。
一方、対応する物理アドレスは、NO-ADDRESSとなっており、明示的に記されていません。
これは、OSに任せる、という設定です。
メモリマップ名称や属性については、以下のURLに記載されています。
3.3 MMUアドレス変換テーブルの設定状態
では、OSに任されたこの「OSSTACK」領域ですが、今現在はどのように割ついているでしょうか?
SOLID-IDEは、PARTER Jetの持つコマンドを直接実行できます。
(昔のICEを使っていた方には、こちらの方がむしろ馴染みかもしれません)
MMUコマンド、というコマンドで、今現在のMMU登録状況を見ることができます。
具体的には、仮想アドレスを入力すると、対応する物理アドレス等が表示されます。
入力してみましょう。
現行タスクのスタック範囲である、0x8fc0100以上0x8fc0200未満は、アドレス変換テーブルに登録されているようです。
そして、その前後は登録されていません。
したがって、スタック範囲をオーバーしてしまうと、とたんにMMUがデータアボート例外を発生する、という動きになっていました。
SOLID OSは各タスクのスタック範囲を知っています。
そのため、そのタスクごとに、スタック範囲をアドレス変換テーブルに反映させることが、できているんですね。
そして、そのスタック範囲から少し超えたところには、連続して他のセクション等を配置することはせず、少し隙間を開けます。
こうすることによって、スタック範囲を超えてしまった瞬間に、アドレステーブルに載っていない範囲のアクセス、となる、という事です。
このような動作について、詳しく説明されているURLがありますので、ぜひご覧ください。
3.4 検出できないパターン
先程、
「SOLID OSは各タスクのスタック範囲を知っています。」
と、しれっと書きました。
この機能は、SOLID OSがスタックの範囲を知っていることが前提です。
ユーザが自分でわりあててしまったら、OSに任せない事になるので、検出できません。
先程のプログラム内、タスク生成部を少し変更してみます。
tsk.stkをNULLにすると、OSに任せるという意味です。
ここに具体的な数字を入れてしまうと、このアドレス固定となり、OSの管理対象から外れてしまいます。
したがって、先述のように、タスク毎にそのスタック領域をアドレス変換テーブルに登録する、という制御ができなくなります。
この状態でプログラムを実行してみます。
いやー、そうですよね!
なんか、この、絶望感。。。
いつもいつも、スタックオーバーフローは、訳がわからない現象に化けて発生しますよね!
これ、デバッガつないでいるからまだ例外発生でブレークしますが、普通に走らせてると「謎のリセット」が発生して、そこからデバッグが始まるパターンですよね。。。
これを見てしまうと。
やっぱり、さっきの「この辺でスタックオーバーフローが起きてるんじゃない?」という事を、スタックオーバーフローが発生した瞬間にツールから言ってもらえる事のありがたみが、とてもわかりますね。。。
その他、留意すべき事項について、こちらのURLで解説されています。
よろしければご参照ください。
4.まとめ
最後にスタックオーバーフローが引き起こす、現実逃避したくなるような現象を見たところで、今回は終わります。
こういった、気が遠くなるようなデバッグには、極力直面したくないので、ツールの力を借りれるのはとてもありがたいと思った次第です。
次回はもう一つの便利機能、「アドレスサニタイザ」についてご紹介&深堀りをする予定です。