本稿について
先週Githubに公開したラズベリーパイ用Haribote OSについて書いた前回のエントリー、趣味の自作OSという非常にニッチで注目されにくい分野の記事ながら、数千のアクセスをいただきました。また、ツイッターでは、旧交を温めるきっかけになったり、新たに自作OSを趣味・仕事とされている方々とフォローし合う機会を得たりと、得がたい経験になりました。
せっかくですので記録も兼ねてラズベリーパイに移植するにあたって行った変更点を紹介したいと思います。なお、本稿ではラズベリーパイに移植したHaribote OSをRPiHariboteと呼ぶことにします。
注意事項:あくまで筆者にとってOS開発は趣味の範疇の上に勉強中の身のため、内容については不正確な点もあると思います。そのような点が見つかった場合の指摘、または、さらによい方法の提案などしていただける場合は、喜んで参考にさせていただきます。なお、本記事はあくまで上記プログラムの解説であり、ARMの解説記事ではありません。また、記事の内容の正確性や実機での動作の再現性についてはいずれも保証できませんし、本記事の内容を使って起こるいかなる損害についても責任をもちません。
本題にもどります。最初はセグメントからページングへの変更についてです。
セグメントのページングによる置き換え
PCとラズベリーパイの違い
オリジナルのHaribote OSでは、セグメントを使うことによりメモリ領域ををカーネルとコンソール、また、OS側とアプリケーション側とで分割して使用しています。また、セグメントによりメモリの保護、CPUの特権の管理、割り込み先の指定、なども行っています。しかし、ラズベリーパイ(ARM)ではそもそもセグメントがありません。
これらの機能のうち特権の管理はCPSR(Current Processor Status Register)、割り込み先の指定は割り込みベクターでのジャンプ先指定で行いますが、残りはページングを使うというのがラズベリーパイ上での現実的な解決策になります。
RPiHariboteで使われているページング方法
前項でこの記事はARM自体の解説ではないと書いたばかりですが、この先の部分で必要なためARMv6のページングについて以下少しだけ説明しておきます。
ラズベリーパイで使われているARMv6では、1MBのセクション、または、16KB(ラージページ)/4KB(スモールページ)のページ単位のページング選択できます。
アドレス変換はスモールページの場合は以下のような手準で行われます。
- 仮想アドレスの上位12ビットを使って、変換テーブルからページテーブルのアドレスを見つける
- 仮想アドレスの12ビット目から19ビット目を使い、ページテーブルから、ページのアドレスを見つける
- 仮想アドレスの下位12ビットを使い、物理アドレスを求める
- 物理アドレスのデータをアクセスする
図に書くとこのようになります
この図はスモールページのページテーブルを使った場合ですが、セクション単位のマッピングを行う場合は変換テーブルが直接データのあるメモリー領域(セクション)を指定します。
仮想アドレスから物理アドレスへの変換はCPUが自動で行うので、OSがページングを使って仮想アドレスを使うには、
- 変換テーブルの構築
- ページテーブルの構築
- 仮想アドレスのイネーブル
という手順を経ることになります。また、異なる変換テーブル・ベース・アドレス(TTBR)に切り替えることで、仮想アドレス空間を切り替えることができます。
また、各テーブルのエントリーにはアクセス制御のビット(AP)があり、領域毎にユーザーモード・特権モード毎にアクセス権限を設定できます。
- AP=0b00: アクセス不可
- AP=0b01: 特権モードのみ読み書きアクセス可能
- AP=0b10: 特権モードは読み書きアクセス可能、ユーザーモードは読み出しのみ
- AP=0b11: 特権モード・ユーザーモード共に読み書きアクセス可能
ARMにはAP以外のアクセス制御もありますが、RPiHariboteでは使用していません。
メモリーマップ
タスクごとの仮想アドレス
RPiHariboteでは大きくわけて、TASK_A用仮想アドレスマップとコンソール用仮想アドレスマップをもっています。
RPiHariboteでは仮想メモリー空間を大きく二つにわけて、0x00000000-0x7FFFFFFFをOS用、0x80000000より上ををアプリケーション用にわりあてています。
TASK_A用仮想アドレスマップ
TASK_AはHariboteOSの画像表示やキー入力うけつけなどを行うタスクで、Hariboteのカーネルのような役目をおっています。
このタスク用の変換テーブルでは、0x00000000-0x7FFFFFFFの領域のみを物理アドレスにそのまま(変換なしで)対応させています。このうち実際に対応する物理アドレスがあるのは0x1FFFFFFFまでの領域だけです。実メモリがない0x20000000以上の領域がアクセスできるようにしてあるのは、ペリフェラルのインターフェースやMailbox *1のアドレスが0x20000000-0x7FFFFFFFの領域にマッピングされているためです。
また、0x7FFFFFFF以下では特権モードのみがアクセスできるように(AP=0b01)、0x80000000以上はアクセス不可に(AP=0b00)設定してあります。いまのところセクション・テーブルによる変換のみを行い、ページ・テーブルは使っていません。
コンソールタスク用仮想アドレスマップ
コンソールタスクは各コンソールウインドウ用のタスクですがアプリケーションの実行にも使われます。コンソールタスク用マップではアプリケーション用の領域をそれぞれわりあてる必要があるため、呼び出されたコンソールの数だけ仮想アドレスマップと変換テーブルが作成されます。
アプリケーションとOSの間のデータのシェアのため仮想アドレス空間の0x00000000-0x7FFFFFFFの部分はTASK_A用の変換テーブルとまったく同じです。アプリケーション用の領域は今の所先頭の64MBのみ(0x80000000-0x83FFFFFF)を使用しており、その部分のみにページをわりあてています。これは仮想アドレスにページが割り振ってあるだけで、物理メモリーアドレスが割り当てられるまでは、実メモリーは消費しません。ページサイズはスモールページ(4KB)です。
RPiHariboteではこの64MBの領域にアプリケーションのコード部分とスタックとヒープの領域を設定します。実メモリの割り当てはアプリケーションがロードされる際に行われます。
実際の処理
TASK_A用の変換テーブルの設定
オリジナルのHariboteOSでセグメントの設定をしていたinit_gdtidt関数は、RPiHariboteではページングの設定をするように完全に書き換えられています。
https://github.com/moizumi99/RPiHaribote/blob/master/haribote/bridge.c
ここでは0x00200000から始まる4096x4byteの領域に対しgenerate_section_table関数を呼び出し、4GBの全領域をカバーするセクションテーブルを作成します。またAPによるアクセス制限も同時に設定します。
コンソールタスク用の変換テーブルの設定
RPiHariboteではコンソール用の変換テーブルは、各コンソールが作られる際に同時に作成されます。コンソールが作られると、bootapck.c内のopen_constaskが呼び出され、他の処理に加えて以下の処理が行われます。
- memman_alloc_4kを使って16KBの領域をセクションテーブル用に確保
- この領域に上記のgenerate_section_tableを使ってTASK_A用と同じセクション・テーブルを設定。
- 64KBの領域をページテーブル用に確保(仮想アドレス領域64MB分)
- この領域にgenerate_page_tableを使ってページテーブルを設定(この時点ではページのエントリーがあるだけで物理メモリが設定されていないので、AP=0b00としてアクセスを禁止)
次に、実際にアプリケーションがSDCARDからロードされる際に、実メモリー上にmemman_alloc_4kでアプリケーションのコード、スタック、ヒープ領域を確保し、対応する仮想アドレスのページを設定します。この一連の処理はconsole.c内のcmd_app内で行っています。
オリジナルのHariboteではスタックとヒープのサイズはMakefile内で設定できるようになっていましたが、RPiHariboteでは今の所決め打ちのサイズになっています。仮想アドレスと実メモリ上に確保されるアドレスの対応は以下のようになっています。
内容 | 仮想アドレス | 確保される実メモリサイズ |
---|---|---|
実行コード | 0x80000000 | アプリケーションのファイルサイズ |
スタック | 0x81000000 | 2MB |
ヒープ | 0x82000000 | 2MB |
アプリケーション終了後は、ページをアクセス不可に再設定し、実メモリ上に確保した領域を開放します。
仮想アドレスのイネーブル
この項の内容はdwelch氏の以下のコードを元にしました
https://github.com/dwelch67/raspberrypi/blob/master/mmu/novectors.s
TTBRの設定やMMU(Memory Management Unit)のイネーブルはCからではできないのでアセンブラで行います。処理はasm_func.S (オリジナルには無かったRPiHaribote独自ファイル)内のstart_MMUで行います。具体的にはr0にTTBRのアドレス、r1にMMUのオン・オフの設定を入れ、start_MMUにジャンプすると
- キャッシュの内容を破棄
- TLB(変換テーブルのキャッシュのようなもの)を破棄
- TTBRを設定
- MMUをオン・オフ
という処理が行われ、これにより仮想アドレスが有効化されます。
タスクスイッチ
タスクスイッチと同時に仮想アドレスのスイッチも行われます。
具体的には各タスク毎にTTBRのアドレスをメモリ上のOS用領域内に保存しておき、タスクスイッチの際に読み出し、上記のstart_MMUを使って有効なTTBRを書き換えています。OSが使用する領域の仮想アドレスは常に物理メモリーアドレスに一致しているので、テーブルの変換によりOS側の動作が影響を受けることはありません。
そのほかの変更
ARMでは変換テーブルとページテーブルは16KBアラインする必要があるので、RPiHaribote内ではmemman_alloc_4kは16KB単位でメモリー領域を確保するように変更されています。(名前と動作が食い違ってしまっており、また他の部分では16KBでは無駄がでるので、対策を検討中)。
OSがアプリケーション内で設定したデータをアクセスできるように、仮想アドレスを物理アドレスに変換するv2pという関数を作成しました。ウインドウのバッファのアドレスなどはこの関数を用いて物理アドレスに変換され、OS側に渡されます。
効果
このようにしてページングによる仮想アドレスを設定する事により、RPiHariboteでは以下のような機能を実装しました
これらはオリジナルのHariboteではセグメントを用いて実現していた物です
まとめ
Haribote OSをラズベリーパイに移植する際に行った変更のうち、セグメントをページングで置き換えた部分に関して説明しました
*1:Video coreなどの設定を行うためのインターフェース