Hatena::ブログ(Diary)

へにゃぺんて@日々勉強のまとめ

2015-11-30

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

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

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


今回は以下の機能を追加しました。

# といっても、まだまだ「一応」な機能ですが。。

(機能を実装した順)


f:id:cupnes:20151201003616g:image

相変わらずGUIは追加していないので、見た目上はあまり変わりませんが、

後述する特権管理によりユーザーアプリケーションカーネルの空間にアクセスすることができなくなったので、

それがわかる様子を示しています。


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

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

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

コマンド書式説明
echoecho STRINGSTRINGを表示する
readlreadl ADDRESSADDRESSの仮想アドレスから4バイト読み出し、結果を表示する
testtest文字列"test"を表示する
ioreadbioreadb ADDRESSADDRESSのIOアドレスから1バイト読み出し、結果を表示する


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

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


また、今回の記事時点のタグを"blog-20151130"という名前で作成しました。

GitHubURLは以下のとおりです。


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


ロック機能

だいたい以下のコミットで追加しました。

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


ここで言う「ロック機能」とは、割り込みを無効化して、ある処理を実行している間、

他の何かに割り込まれることが無いようにする機能です。(なんだか同じことを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()を抜ける。)


省電力機能

だいたい

から

までの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はこれにより、キー入力待ちに入る

システムコール

だいたい

から

までの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を読み出せてしまっている。)

いずれ制限するつもりですが、こちらはまだやり方も調べていない段階です。


さいごに

いつもは記事の内容がそっけないかなと思い、少し頑張って書いてみました。

(勢いで書いたので、自分用のメモにしかなっていないかもしれませんが。。)


こうして書いてみると、カーネルに必要な名前の付く機能は揃ってきていますが、

いずれの機能もまだまだ未熟なので、それぞれの機能を「ちゃんとしたもの」に作り上げていく必要があるかなと思います。


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

Hand Craft MasterHand Craft Master 2015/12/05 16:55  ちょっと感動しています。

 本を読んだだけではプログラムを作れるようにはならなくて、やっぱり自分でコードを書いてみて、初めてちゃんと理解できると思います。
 なので、コンピューターという機械の制御をちゃんと理解するためには、やっぱり自分でOSを作ってみる必要があると思っています。
 しかしながら私の周囲には、OSを作ってみようともせずに「自分はもう既に十分にコンピューターには詳しいぜ」っていう姿勢(勘違い?)の人が多く、少々、残念な思いをしています。
 でも日本のITにも、まだまだ未来への可能性がある。それはここにあるって実感できます。
 私が昔作ったものは、OSと呼んで良いのか微妙なものでしたが、それでもコンピューターという機械の性能を、自在に引き出すにはどうすべきかを理解する、というかノウハウを体得するには十分なものだったと思っています。

 頑張ってください。応援しています。

cupnescupnes 2015/12/05 19:21 ありがとうございます!
そこまで言っていただけるとは、とても励みになります。

私も「自分で作ってみないことには理解できない」との思いからOSを作っています。
(実は、今のリポジトリ名である"OS5"に至る前の"OS"〜"OS4"までは、
本に従って作ってみたり、既存のものの改造から始めてみたりしたのですが、
いまいち「理解できている」という実感ができませんでした。)

今の時代、x86向けの自作OSについては書籍もいくつか出版されており、
「いまさらフルスクラッチで作ることになんの意味があるのか」
なんて言われたりもするのかな、と思っていたのですが、
ここまで応援してくださる人が居るとは、とてもうれしいです。

これからもかんばります!

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

リンク元