OSカーネルを0から作り始めてみた

  • 105
    いいね
  • 0
    コメント

1. 概要

OSカーネル1をフルスクラッチで(0から)作り始めてみました。
本稿では下記について記載します。

  • カーネルを自作し始めた背景
  • カーネル自作における方針
  • 本稿執筆時カーネルの機能概要
  • カーネルの実行方法
  • プロセスを起動してみる
  • 今後の課題

2. 背景

私がカーネルを自作し始めた理由は次の2点です。

1.コンピュータがどの様に動いてるのか知りたかった
2.使用しているOSに不満があった

2.1 コンピュータがどの様に動いてるのか知りたかった

かれこれ10年以上前に遡りますが、私が大学に入学した頃、VisualBasicやBasic、Cなどの言語を使って簡単なソフトウェアを作ることが出来ましたが、どうして簡単なコードでウィンドウが表示できるのか、どうしてprintf文を書けばコンソールに文字が出力できるのか、コンピュータはいったいどんな風に動いてるのか全く分からず、不思議で仕方がありませんでした。
そこで私は、どのようにコンピュータが動いているのかを知る事が出来るであろうOSに興味を持ち、自分で作ってみたいと思うようになりました。
色々と勉強を始め、作り始めてはすぐ0から作り直しを繰り返し、全く形になるような物は作れていませんでしたが、漸く最近になって本格的に(また)作り始めました。

2.2 使用しているOSに不満があった

私はWindows3.1、95、98、98SE、ME、XP、Vista、7、8.1、10とRedHat、Fedora、CentOS、Ubuntu、etc.と色々使ってきましたが、どれも次の不満がありました。

  • 起動が遅い
  • 起動していると段々処理が遅くなる、よく固まる
  • いざ使おうとするとアップデートが始まり、処理が遅くなる上に時間が掛かる
  • 勝手に再起動する事がある
  • 設定が分かりづらく難しい、隠れた設定がある(レジストリなど)
  • ちょっと昔のコンピュータだとスペック不足でインストールできない、しても動かない
  • etc

最近のOSでは割と解消されてたり、私自身の使い方が下手だったりするのかもしれませんが、兎に角、私にとっては使い難いところがあり、それならば自分で作ってみようと考えました。

3. 方針

OSを自作するにあたって、私は次の方針を決めました。

1.OSでなく、まずカーネルを作る
2.ソースコードを公開する

3.1 OSでなく、まずカーネルを作る

OS自作すると意気込みましたが、無数にあるハードウェアのドライバを作る必要がある等、OSはかなり膨大でとても一人で作り切れません。作ろうとしても、本当に自分専用のOSになってしまいます。

そこでまず、OSの核となる部分、カーネルだけを作る事にしました。(勿論、OSを作るといったらカーネルからになりますが)
既存OSの不満から安心安全・高信頼のOS実現を目指す為、そして、後からいろんなモジュールを付加する事でOSを構成できるよう、マイクロカーネル2型のカーネルを作ります。

3.2 ソースコードを公開する

私以外にも、既存OSに不満がある、OSを作ってみたい、という方がいて、少しでも参考としてもらえたり、そのベースとして使ってもらう事ができる様に、MITライセンス3でソースコードを公開します。

https://ja.osdn.net/projects/mochi/

4. 機能概要

現在カーネルはPC/AT互換機4で動作し、次の機能を持ちます。

  • メモリ管理
  • 割込み管理
  • タイマ管理
  • プロセス管理
  • デバッグ制御

以下では、対応するソースコードファイルを示しています。
興味が湧いたらチラ見してみて下さい。C言語、アセンブリ言語で作っています。
リンク先は公開しているOSDN5のサイト内へのページです。
本稿記載時のリビジョンはd4c9d86です。

4.1 メモリ管理

メモリ管理機能では次の事ができます。

  • GDT(Global Descriptor Table)6の管理
    • GDT初期化
    • CPUへのGDT登録
    • GDTエントリの追加
  • 物理メモリ領域の管理
    • 4KiB単位でのメモリ領域割当/解放

ソースコード参照先:
 src/kernel/MemMng/MemMngGdt.c
 src/kernel/MemMng/MemMngArea.c

4.2 割込み管理

割込み管理機能では次の事ができます。

  • 割込みハンドラの管理
    • 割込みハンドラの登録
  • IDT(Interrupt Descriptor Table)7の管理
    • IDT初期化
    • CPUへのIDT登録
    • IDTエントリの追加
  • PIC(Programmable Interrupt Controller)8の制御
    • PIC初期化
    • EOI制御
    • 割込みマスク制御

ソースコード参照先:
 src/kernel/IntMng/IntMngHdl.c
 src/kernel/IntMng/IntMngIdt.c
 src/kernel/IntMng/IntMngPic.c

4.3 タイマ管理

タイマ管理機能では次の事ができます。

  • PIT(Programmable Interval Timer)9の管理
    • PITの初期化

ソースコード参照先:
 src/kernel/TimerMng/TimerMngPit.c

4.4 プロセス管理

プロセス管理機能では次の事ができます。

  • TSS(Task State Segment)10の管理
    • TSS初期化
    • CPUへのTSS登録
    • TSS設定
  • タスクの管理
    • タスク(プロセス)の追加
    • コンテキスト取得/設定
  • スケジューリング
    • タスクスイッチ

ソースコード参照先:
 src/kernel/ProcMng/ProcMngTss.c
 src/kernel/ProcMng/ProcMngTask.c
 src/kernel/ProcMng/ProcMngSched.c

4.5 デバッグ制御

デバッグ制御機能では次の事ができます。

  • トレースログ管理
    • 画面へのトレースログ出力

ソースコード参照先:
 src/kernel/Debug/DebugLog.c

5. 実行方法

必要な環境は次の通りです。

  • x86、Linux
  • partx
  • GNU Binutils(ld, gcc, as, ar)
  • vmware player
  • git(Webページから直接ダウンロードの場合は必要無し)

下記コマンドを実行して、gitリポジトリをクローンします。

$ git clone git://git.osdn.net/gitroot/mochi/master.git

下記コマンドでコンパイルとイメージファイル(master/build/mochi.img)を作ります。
(注:loopデバイスを使用したりしています。一度Makefileを覗いて何をしているか把握する事をお勧めします。何か良からぬ事が起きてしまっても謝る事しかできません。)

$ cd master/build
$ make image

master/vm/vmware/mochi.vmxをvmwareで起動すると上記で作ったイメージファイルを
HDDイメージとして仮想PCが起動し、カーネルが実行できます。
(私の環境ではWindows上でvmwareを起動しています)

カーネル実行

デバッグオプションをつけている為、デバッグトレースが画面に表示されるようになっています。

6. プロセスを起動してみる

5.にて起動したカーネルは、アイドルプロセスが永遠動く状態です。
それではつまらないので、永遠数字をカウントする2つのプロセスを起動させてみます。

プロセス用にソースコードファイルTest.cを作成します。

src/kernel/Test.c
#include <stdint.h>

static uint32_t gTestCounterA = 0;
static uint32_t gTestCounterB = 0;

void TestTaskA( void )
{
    char     c;
    uint32_t num;
    uint32_t digit;

    while ( 1 ) {
        /* 16進数の1桁毎に繰り返し */
        for ( digit = 0; digit < 8; digit++ ) {
            /* 数取得 */
            num = ( gTestCounterA >> ( digit * 4 ) ) & 0xF;

            /* 数判定 */
            if ( ( 0 <= num ) && ( num <= 9 ) ) {
                /* 0~9 */

                /* 文字変換 */
                c = '0' + num;

            } else {
                /* A~F */

                /* 文字変換 */
                c = 'A' + num - 0xA;
            }

            /* 文字出力 */
            *( ( char * ) ( 0xB80A0 + 136 - digit * 2 ) ) = c;
        }

        /* カウンタインクリメント */
        gTestCounterA++;
    }
}

void TestTaskB( void )
{
    char     c;
    uint32_t num;
    uint32_t digit;

    while ( 1 ) {
        /* 16進数の1桁毎に繰り返し */
        for ( digit = 0; digit < 8; digit++ ) {
            /* 数取得 */
            num = ( gTestCounterB >> ( digit * 4 ) ) & 0xF;

            /* 数判定 */
            if ( ( 0 <= num ) && ( num <= 9 ) ) {
                /* 0~9 */

                /* 文字変換 */
                c = '0' + num;

            } else {
                /* A~F */

                /* 文字変換 */
                c = 'A' + num - 0xA;
            }

            /* 文字出力 */
            *( ( char * ) ( 0xB8140 + 136 - digit * 2 ) ) = c;
        }

        /* カウンタインクリメント */
        gTestCounterB++;
    }
}

上記ソースコードでは、2つのプロセスTestTaskATestTaskBを定義しています。
2つのプロセスの違いは、

  • カウンタ変数gTestCounterAgTestCounterB
  • /* 文字出力 */の次行に記載されている16進数値0xB80A00xB8140

だけです。

カウンタ変数は、符号無し4バイト(uint32_t)なので、文字列で表すと8文字です。
一桁づつ画面に表示させる為、for ( digit = 0; digit < 8; digit++ ) {で8回ループしています。
num = ( gTestCounterB >> ( digit * 4 ) ) & 0xF;にて一桁の値をローカル変数numに代入し、/* 数判定 */の次行のif文内でnumで表される数値を文字コードに変換してローカル変数cに代入しています。

/* 文字出力 */の次行で、ローカル変数cに代入した文字コードを画面に出力する為にVRAMに格納しています。
本カーネルが動作している時、ビデオモードは80x25のテキストモードに設定しています。
このモードで動作している時、物理メモリの0xB8000からの領域はVRAMとして使用でき、文字コード1byteと文字属性(文字色・背景色など)1byteの計2byteで1文字を画面に出力できるようになっています。
0xB80A0は、2行目の先頭アドレス、0xB8140は、3行目の先頭アドレスを表しています。(1行は、80文字の各々2byteで表現されるので、80*2=0xA0。これを0xB8000に足したアドレスが2行目の先頭アドレスとなります。)
続く136は、その行の先頭から68文字目である事を表しています。(68*2=136)

Test.cはMakefileに記載してコンパイルに含めるようにしておきます。

src/kernel/Makefile
(…省略…)

# ソースコード
SRCS = InitCtrl/InitCtrlInit.c \
       MemMng/MemMngInit.c     \
       MemMng/MemMngGdt.c      \
       MemMng/MemMngArea.c     \
       IntMng/IntMngInit.c     \
       IntMng/IntMngIdt.c      \
       IntMng/IntMngHdl.c      \
       IntMng/IntMngPic.c      \
       TimerMng/TimerMngInit.c \
       TimerMng/TimerMngPit.c  \
       ProcMng/ProcMngInit.c   \
       ProcMng/ProcMngTss.c    \
       ProcMng/ProcMngTask.c   \
       ProcMng/ProcMngSched.c  \
       Debug/DebugInit.c       \
       Debug/DebugLog.c        \
       Test.c

(…省略…)

次に、下記の通りsrc/kernel/InitCtrl/InitCtrlInit.cの割込み有効化を行う直前に、TestTaskA、TestTaskBをドライバタスクとして登録(ProcMngTaskAdd())させます。
割込み有効化を行うと、スケジューラが動き出し、登録したプロセスが動き始めます。

src/kernel/InitCtrl/InitCtrlInit.c
    /* タイマ管理モジュール初期化 */
    TimerMngInit();

    /* テスト */
    ProcMngTaskAdd( PROCMNG_TASK_TYPE_DRIVER,
                    TestTaskA                 );
    ProcMngTaskAdd( PROCMNG_TASK_TYPE_DRIVER,
                    TestTaskB                 );

    /* 割込み有効化 */
    IntMngPicEnable();
    IA32InstructionSti();

(補足:TestTaskA()とTestTaskB()のextern宣言を忘れずに)

以上でコンパイルして起動させると、次の図の黄色枠で囲ったところに2つのカウンタがカウントアップしていくのがわかります。

プロセス起動

gif.gif

こちらのコードは下記にて公開しています。ご参考下さい。
Rev:313e322

7. 今後の課題

上述の通り、まだ、簡単なプロセスをマルチタスクスケジューリングする事しかできません。
ページングによるメモリ管理も行っていないなど色々機能が無いためプロセスは全ての物理メモリを操作できて何でもできてしまう、と同時にAPIも無いため何もできません。

色々と機能を望んでしまうとTODOが溢れて途方に暮れてしまうので、少しづつ着実に機能追加していこうと考えています。例えば、

  • ページングによるメモリ管理
  • ELF形式の実行ファイルからプロセス起動
  • ブルースクリーンならぬレッドスクリーン(一般保護例外発生時などのデバッグ用画面出力)
  • etc

最後に

長々とお読み頂き有難う御座いました。
Qiitaでの投稿は初めてなので色々と間違いがあるかもしれません。
ほんの少しでも皆さまのお役に立てる事ができたら幸いです。

Mochi(Twitter: https://twitter.com/master_mochi