システムコールと特権管理などを追加しました

メモリ管理を追加しました - へにゃぺんて@日々勉強のまとめ

こちらの記事からの続きで、OSづくりの記事です。


今回は以下の機能を追加しました。
# といっても、まだまだ「一応」な機能ですが。。

(機能を実装した順)


相変わらずGUIは追加していないので、見た目上はあまり変わりませんが、
後述する特権管理によりユーザーアプリケーションはカーネルの空間にアクセスすることができなくなったので、
それがわかる様子を示しています。


上記のキャプチャではshellとuptimeの2つのアプリケーションが動作しています。

アプリケーション 説明
shell プロンプトを表示し、ユーザ入力に応じて結果を出力する
uptime 右上で起動時間(秒)を16進で表示する


また、上記のキャプチャで登場するコマンドは以下のとおりです。

コマンド 書式 説明
echo echo STRING STRINGを表示する
readl readl ADDRESS ADDRESSの仮想アドレスから4バイト読み出し、結果を表示する
test test 文字列"test"を表示する
ioreadb ioreadb ADDRESS ADDRESSのIOアドレスから1バイト読み出し、結果を表示する

ソースコードはいつもどおり、GitHubからダウンロードできます。

例によって、ビルド方法なども上記のリポジトリにあるREADMEを参照してください。


また、今回の記事時点のタグを"blog-20151130"という名前で作成しました。
GitHubのURLは以下のとおりです。


それでは、以降は今回追加した機能について説明します。

ロック機能

だいたい以下のコミットで追加しました。
(下に行くほど新しい。半角スペース区切りで先頭7文字はハッシュ値、その後がコミットタイトル)

  • 98f2dfe x86: EFlagsレジスタ値取得マクロ追加
  • 0004e73 lock: ロック機能追加
  • 1c7b44d lock: sti/cliからカーネルのロック機能を使うよう修正


ここで言う「ロック機能」とは、割り込みを無効化して、ある処理を実行している間、
他の何かに割り込まれることが無いようにする機能です。(なんだか同じことを2回言っている気がする。。)


これまでもロックの機能はあったのですが、それはCPUの割り込み有効命令(sti)/無効命令(cli)をマクロ化しただけでした。
ただし、これだと入れ子になった時に問題となるので、入れ子も大丈夫なロック機能を追加しました。
(例えば汎用的に呼ばれる関数(*1)の内部でclistiによるロックがあると、
その関数を呼ぶ上位でclistiでロックしようとしても、(*1)の関数内部のstiでロックが解除されてしまう)


まあ、「いまさら」な機能なのですが、ロック機能を追加するのをサボり、
意図せず解除されてしまったロックを再ロックするcliが至るところにありました。(特にkernel/main.c)


そんなわけで、以下のロック用関数を追加しました。

  • void kern_lock(unsigned char *if_bit)
  • void kern_unlock(unsigned char *if_bit)


kern_lock()がロックする関数で、CPUのEFlagsレジスタで「現在割り込みが有効か無効か」を確認し、
引数で指定したポインタにその結果を格納します。
そして、「現在割り込みが有効」であった場合は、cli命令で割り込みを無効化(ロック)します。


kern_unlock()がロックを解除する関数で、kern_lock()で割り込みの有効/無効状態を保存したポインタを引数で受け取ります。
kern_unlock()は引数で受け取った割り込み状態から、kern_lock()を呼び出した時のロック状態を復帰します。
(kern_lock()時にロックされていた場合、何もせずkern_unlock()を抜け、
kern_lock()時にロックされていなかった場合、sti命令でロックを解除してからkern_unlock()を抜ける。)

省電力機能

だいたい

から

  • 5beab8c con: キー入力待ちのタスクスリープに対応

までの32コミットで追加しました。


これもいささか衝撃的な事実なのですが、実はこれまで、私のカーネルでは
CPUはhlt命令は実行せず、常にフル稼働していました。


なんだかサボっていた機能だったのですが、あまりにもあんまりなので、
せっかくスケジューラを実装したのだからと、「暇なときにはhltで次のイベントを待機する」ように実装しました。


そして、この「暇なときにはhltで次のイベントを待機する」を実現するために、以下の3つを追加しました。
1. ランキューの追加
2. カーネルタスクの追加
3. イベント待ちAPI追加


1つ目のランキューは、実行中&実行待ちタスクのキューです。私の現状のカーネルでは実行中のタスクもランキューに繋がれます。
(ここでの「タスク」はカーネルでの管理の単位で、現状はタスク=アプリです。)
なお、現状、ランキューはタスクのリンクリストで、デキューすることがないので、エンキューのみAPIがある状態です。


2つ目のカーネルタスクは、ランキューに何もタスクが無いときに実行されるタスクで、
このカーネルタスクがhltを呼び出すことで、「何もすることがないときにはhlt」が実現できます。


3つ目のイベント待ちAPIは、タスクが「次にXXXが発生したら起こして」とカーネルに伝えるためのAPIで、
現状は以下のAPIがあります。

  • void wakeup_after_msec(unsigned int msec)
    • 呼び出したタスクをランキューから外し、指定された時間(ミリ秒)経過後にタスクを再びランキューにエンキューする
    • uptimeはこれにより、時間経過待ちに入る
  • void wakeup_after_event(unsigned char event_type)
    • 呼び出したタスクをランキューから外し、指定されたイベント発生時にタスクを再びランキューにエンキューする
    • 現状はキー入力イベントのみ対応
    • shellはこれにより、キー入力待ちに入る

システムコール

だいたい

  • 3ff8a02 intr: ソフトウェア割り込み(0x80)の動作確認

から

  • fc6086a intr: ソフトウェア割り込み動作確認用の記述を削除

までの21コミットで追加しました。


これはLinuxと同じですが、割り込み番号0x80をシステムコール用のソフトウェア割り込みとして使用しています。
なお、現状、私のカーネルではシステムコールの引数はレジスタEAX、EBX、ECX、EDXで渡します。
EAXがシステムコール番号で、それ以外の3つはシステムコールのパラメータです。そして、システムコールの戻り地はEAXで返します。


また、現在は以下のシステムコールを用意していますが、決定ではありません。

  • SYSCALL_TIMER_GET_GLOBAL_COUNTER
    • 10ms周期のタイマー割り込みでインクリメントされるグローバルカウンタの値を取得する
  • SYSCALL_SCHED_WAKEUP_MSEC
  • SYSCALL_CON_GET_CURSOR_POS_Y
    • コンソール上のカーソルのY座標を返す
  • SYSCALL_CON_PUT_STR
    • コンソールへの文字列出力
  • SYSCALL_CON_PUT_STR_POS
    • コンソールへの文字列出力(座標指定)
  • SYSCALL_CON_DUMP_HEX
    • コンソールへ16進で数値をダンプ
  • SYSCALL_CON_DUMP_HEX_POS
    • コンソールへ16進で数値をダンプ(座標指定)
  • SYSCALL_CON_GET_LINE
    • コンソール上の1行の入力を取得


システムコールの粒度が歪なのは、現状のshellとuptimeが参照する
カーネル空間のシンボルを、そのままシステムコール化したためです。


とはいえ、これにより、アプリケーションはカーネルのシンボルを参照せずに
単体でビルドすることができるようになりました。


あと、システムコールの実装中に、割り込みと例外の入り口と出口で
コンテキスト(汎用レジスタ・システムレジスタ)の退避と復帰をしていないことに気づき、

  • 456275b intr,excp: 入口/出口で汎用レジスタの退避/復帰するよう修正

のコミットで修正しています。


よくこれまで動いていたものだと思うのですが、
そもそも、shellもuptimeも大した処理をしておらず、
タイムスライスのほどんどを何もせず待っているだけで過ごしているので、
キー入力とかが割り込んできても、「何かの作業中」であることが無かったのだと思います。

特権管理

  • 09b9f08 excp: 割り込み番号0から20までで未定義だった例外ハンドラを追加

から

までの6コミットで追加しました。


これまでは、すべてのアプリは最高レベルの特権(特権レベル0)で動いていたのですが、
アプリは最低の特権レベル3で動作するように修正しました。


修正は以下のとおりです。
1. 特権レベル3用のコード用とデータ用のセグメントディスクリプタをGDTに追加
2. 各アプリケーションのTSSの初期値を修正
3. 各アプリケーションのPDEとPTEの特権レベルを3へ修正
4. 割り込み0x80(システムコール)の特権レベルを3へ修正


1つ目のセグメントディスクリプタ追加について、現状のカーネルではセグメンテーションは使用しておらず、
コード用のセグメントもデータ用のセグメントも32ビットの全メモリ空間を指しています。
(インテルのソフトウェアデベロッパーズマニュアルで言うところの「フラットなメモリモデル」)
そこへ、同じく32ビットの全メモリ空間を指す特権レベル3のセグメントディスクリプタを2つ追加しました。


2つ目のTSSの初期値修正は、特権レベル0用のスタックポインタを設定するSS0とESP0の初期値と、
通常のセグメントレジスタ(CS,DS,SS,ES,FS,GS)の初期値を修正しています。
ここで、通常のセグメントレジスタには1つ目に追加した特権レベル3のセグメントディスクリプタを指定しています。


3つ目と4つ目は上記の説明文以上のことはしていません。


この修正により、キャプチャしたGIFアニメ(上記)で示すように、
カーネル空間(0x20000000より小さいアドレス)へはアプリケーションはアクセスできなくなりました。


現状、アクセス違反の例外(ページフォルト)時はエラーコードと例外要因のアドレスをダンプして
停止するようになっています。
(上記のキャプチャで"readl 7e00"して固まっている箇所です。)


あと、上の方で掲載したGIFのキャプチャを見てもらうとわかるのですが、
IOアドレスのアクセスは、現状では制限できていません。
("ioreadb 21"で、アプリケーションからPICのIMRを読み出せてしまっている。)
いずれ制限するつもりですが、こちらはまだやり方も調べていない段階です。

さいごに

いつもは記事の内容がそっけないかなと思い、少し頑張って書いてみました。
(勢いで書いたので、自分用のメモにしかなっていないかもしれませんが。。)


こうして書いてみると、カーネルに必要な名前の付く機能は揃ってきていますが、
いずれの機能もまだまだ未熟なので、それぞれの機能を「ちゃんとしたもの」に作り上げていく必要があるかなと思います。


楽しんでやることが第一なので、今後ものんびりと作っていきたいと思います。