Operating Systems Development Series
Basic CRT and Code Design
by Mike, 2008, 2009

はじめに

ついにカーネルとハードウェア抽象化レイヤ(HAL)の開発を始める時が来ました。

前回のチュートリアルでは、基本的なカーネル概念をまとめ、さまざまな基本カーネル設計レイアウトを見てきました。また、マイクロカーネルとモノリシックカーネル設計から派生したいくつかの概念を使用するため、オペレーティングシステム用にハイブリッドカーネル設計を開発することにしました。これにより、両方の世界からのいくつかのコンセプトを見ることができます。

このチュートリアルでは、カーネルプログラムの構築を開始し、ハードウェア抽象化レイヤーのライブラリの開発を開始します。現在、私たちは、カーネルとハードウェア抽象化レイヤーを、使用するコンパイラによって、CまたはC++といった、より高度なプログラミング言語で開発できるようにシステムをセットアップしています。

Cコンパイラとの互換性を保つために、C++の代わりにCを使う予定です。しかし、個人的にはCよりもC++の方が好きなので、C++版のソースを開発するかもしれません :)

さて、今週のリストは以下の通りです。

  • 良いコーディングの実践の促進
  • コードの設計とレイアウト
  • データ型と基本的な宣言の抽象化
  • CRT: _null.h
  • CRT: size_t.h
  • CRT: ctype.hとcctype
  • CRT: va_list.hとstdarg.h/csdtarg
  • デモデバッグ用Printfの書き方 (近日中にアップロード予定)
...以上!このチュートリアルは HAL とカーネルの基本的なセットアップをカバーするだけです。

さあ、はじめましょう

始める前に...

これはブートローダの世界からの最初のステップです。ブートローダの中では、移植性やシステム依存性をあまり気にする必要はありませんでした。結局、ブートローダは、その性質上、非常にシステム依存性が高いのです。

これまでブートローダはCやC++で開発されていましたが、これからはカーネルにジャンプします。また、独自のランタイムライブラリやハードウェア抽象化レイヤ(HAL)の始まりでもあり、盛りだくさんですね。

しかし、これで楽になったわけではありません。オペレーティングシステムは非常に大きなサイズになることがあります。このシステムがどれほどの規模になるか分からないからこそ、最初から良いコーディングの仕方を強調する必要があるのです。多くの開発プロジェクトが失敗しています。しかし、それは複雑すぎるからではありません。どんなプロジェクトでも、正しく設計すれば、複雑さを抑えて作ることができるのです。これが次に見てみたいものです...。

パンドラの箱

実は、簡単に言えば、コードは邪悪です。 コードは非常に無秩序で醜くなることがあります。コードとデザインのカオス的で再帰的な性質が、さらに複雑さを増すのです。誤解しないでください、私たちはまだ多くのコードを書き直す必要があります。これは、正しい設計が存在しないためです。これは、プロジェクト全体、特に大規模なプロジェクトを停止させる傾向があり、システムの残りの部分は、この醜く書かれ、設計が不十分なコードのカオス的性質に依存する必要があるためです。このような現象は、システムのある部分から始まり、ソフトウェアの残りの部分にまで広がっていきます。

どうすれば、このような事態を防ぐことができるのでしょうか。

"パンドラの箱 "は邪悪だと言われるが、それは間違いだ。 中身が悪だったんだ 箱はただの箱じゃない"- 匿名希望

コードが素敵な小さな箱の中に収まっている限り、コードが内側でどんなに乱雑になろうが、醜くなろうが、それは問題ではない。このコードが誰にも見えない素敵な小さな箱の中に収まっている限りは。箱の内側には、デーモンやクリーチャー、その他のものが入っていても構いません。結局のところ、私たちが見ているのは箱だけなのです。それがどのように機能するかは気にする必要はなく、単に機能するだけです。

これが、分離と封じ込めの基本です。つまり、カプセル化であり、ソフトウェア工学のほぼすべての基礎となるものです。

私たちはまず、箱の中が何をするのかを書きます。その後、このモジュールが完成したら、箱を閉じて、システムの残りの部分と接続するのです。しかし、閉じた後の箱は絶対に開けてはいけない。カプセル化を解除してしまうので、箱の中の悪をすべて出してしまうことになります。一度箱を開けてしまうと、コンパイルエラー、リンカエラー、ランタイムエラーなど、できるだけ多くのコードに感染し、プロジェクト全体が大混乱に陥ります。

しっかりとした、よく設計されたシステムは、すべてのコンポーネントを、互いに接続され、より大きな箱の中に入れ子にされた隔離された(「カプセル化された」)箱として扱います。

カプセル化は、ソフトウェア工学において非常に重要な概念です。オブジェクト指向のプログラマーでなくとも、カプセル化の概念は存在するのです。

インターフェースとインプリメンテーション

ポンデリングの箱の例で言うと、「インターフェース」が箱で、「インプリメンテーション」が箱の中にあるものということになります。箱のインターフェース(「パブリック」)部分は、その箱から外の世界へ接続するものです。このサブシステム内の他のボックスと我々のボックスを接続するものです。インターフェース自体には、ボックスが外部に公開する関数プロトタイプ、構造体、クラス、その他の定義がすべて含まれており、外部はボックスを使用し、相互作用することができます。これが「インターフェイス」です。このボックスの中にある、モジュールやその関数、クラスルーチンなどを定義する邪悪なコードはすべて、モジュールのインプリメンテーション(実装)です。

各ボックス(「コンポーネント」)は、シンプルでポイントを押さえたインターフェースで構成することが重要である。また、各コンポーネントが何をするのかが明確でなければならない。C言語では、グローバルな名前空間が大量のルーチンで非常に煩雑になることがあります。このため、これらのルーチンやインターフェースを明確に識別できるような名前を付けることが重要です。また、ボックスの実装の詳細(「プライベート」な部分)は、プライベートなメンバとして保持されることを確認する必要があります。この部分をインターフェイスの中に入れてしまうと、ボックスが開放されてしまうので、よくありません(これは悪いことです)。

C言語では、staticキーワードを使うことで、ルーチンを実装の一部として残すことができます。インターフェイスはexternキーワードで作ることができます。C++では、private、publicprotectedキーワードを使用して、クラスを使用することが推奨されています。

準備する

私たちは、上記の概念を用いて、大規模なソフトウェアで良いプログラミングを実践するためのシステムを開発する予定です。

本システムはコンパイラ間の移植性を考慮し、C言語を用いて開発を行いますが、C++を用いることも可能であることを念頭においてください。

私たちは、拡張性・移植性を第一に考えています。このため、ハードウェアに依存する実装はすべて、独自の小さな箱 -HAL (Hardware Abstraction Layer) - の中に隠蔽する予定です。C++のスタートアップランタイムコードはコンパイラに依存しているので、私たちはそれをCRT(C++ランタイム)ライブラリという小さな箱に入れます。これらすべては、システムの他の部分から完全に独立しています。

覚えておいてください。重要なのは、分離することです。一度閉じた箱は決して開けないようにしましょう。

このことを念頭に置いて、私たちのシステムの第一歩を踏み出しましょう...

コードレイアウトと設計

このチュートリアルでは、これまでで最も複雑なデモを行います。そのため、読者の皆さんには、デモのソースを開き、チュートリアルに沿って、すべてをよりよく理解していただきたいと思います。

コード設計

このシリーズで、なぜこのような構造を選択したのかを理解することは、非常に重要です。第一の理由はカプセル化で、各ディレクトリには個別のライブラリモジュールが含まれます。つまり、それぞれのモジュールはポンドラの箱なのです。コードの安定性、構造、移植性を維持するためには、これらのモジュールをできる限り分離しておくことが極めて重要なのです。このため、各モジュールを独立したライブラリモジュールとして扱うことにしました。
Our two stage bootloader (We have already constructed this from our previous tutorials) ======================================= SysBoot\ Stage1\ - Stage1 bootstrap loader Stage2\ - Stage2 KRNLDR bootloder Our System Core ======================================= SysCore\ Debug\ - Pre-Release complete builds Release\ - Release builds Include\ - Standard Library Include directory Lib\ - Standard Library Runtime. Outputs Crtlib.lib or Crtlib.dll. Hal\ - Hardware Abstraction Layer. Outputs Hal.lib or Hal.dll. Kernel\ - Kernel Program. Outputs Krnl32.lib or KRNL32.EXE

ライブラリモジュールとしてビルドする必要がないのは、Include/ディレクトリ内のファイルだけです。 これらは単なるヘッダーファイルなので、インプリメンテーションを含む必要はないはずです。このため、開くべきボックスはありません。

アプリケーションと同様に、C++ランタイムコードを最初に実行されるコードとすることにしました。言い換えれば、ブートローダはカーネルを実行しないのです。代わりにランタイムコード(CRTLIB)を実行し、カーネルのための環境を整え、そしてカーネルを実行するのです。

_null.h

やったー!そろそろチュートリアルの本題に入りましょう。

C++のインクルードについて...

C++をお使いの方は、ライブラリのヘッダーファイルについて興味があるかもしれません。つまり、C++では、*.hというアペンドは削除され、すべてのCヘッダーの前にcが付けられます。つまり、C++では#include <stdlib.h>の代わりに#include <cstdlib>が使われるわけです。しかし、どうすればいいのか、疑問に思うかもしれません。

実はとても簡単です。例えば、stdlib.hと cstdlibがあります。cstdlibは単にstdlib.hを#includeするヘッダーファイルで、それ以上ではありません。私たちのライブラリも同じようにする予定です。

これにより、C言語の開発者はstdlib.hを使用し、C++の開発者はcstdlibを使用することができます。 このようにして、私たちは共に良い習慣を奨励することができるのです。

本題に戻ります。

まず、最初の抽象化はNULLです。ここで言うべきことはそれほど多くはないのですが、ひとつだけ細かいことがあります。NULLの定義方法は、CとC++のどちらを使用しているかによって異なります。

標準的なC言語では、NULLは(void*)0と定義されていますが、C++では、単に0です。 これは、かなり標準的な__cplusplus定数を使用することによって決定することができます。

// Undefines NULL #ifdef NULL # undef NULL #endif #ifdef __cplusplus extern "C" { #endif /* standard NULL declaration */ #define NULL 0 #ifdef __cplusplus } #else /* standard NULL declaration */ #define NULL (void*)0 #endif
このヘッダにはテンプレートに関するものがもっとありますが、これが重要な部分です。他のすべては非常に簡単です。

size_t.h

データ隠蔽について...

パンドラの箱の理論を思い出してください。箱の中のデータ型はインプリメンテーションのディテールにある。size_tはその一つです。 インプリメンテーションの詳細を維持することで、後方互換性を維持する限り、そのデータ型を使用するものに影響を与えることなく、そのデータ型について好きなように変更することができます。

本題に戻る

これについてはあまり話すことはないのですが......。
#ifdef __cplusplus extern "C" { #endif /* standard size_t type */ typedef unsigned size_t; #ifdef __cplusplus } #endif

データ型の隠蔽 - stdint.hとcstdint

前のセクションでは、インターフェイス内のデータ隠蔽の重要性を説きましたが、移植性との関連では重要性を強調しませんでした。

各データ型には、そのサイズが指定されています。しかし、各データ型のサイズは、これがビルドされるコンパイラとシステムに完全に依存します。このため、データ型を標準的なインターフェースの後ろに隠すことが重要です。特に、Size Does Matter(tm)の環境で作業しているためです。

stdint.h

このファイルは約150行とかなり大きなファイルです。しかし、どれもそれほど難しいものではありません。このファイルでは、あるサイズが保証されたさまざまな積分データ型が定義されています。

システム全体で使うことになる、基本的なデータ型を見てみましょう。

typedef signed char int8_t; typedef unsigned char uint8_t; typedef short int16_t; typedef unsigned short uint16_t; typedef int int32_t; typedef unsigned uint32_t; typedef long long int64_t; typedef unsigned long long uint64_t;
32ビットシステム用にコンパイルする場合、上記のデータ型は同じであることが保証されます。つまり、uint8_tは8ビット、uint16_tはWORD(2バイト)のサイズであることが保証されている、というように。データ型のサイズはその名前にエンコードされているので、常にそのサイズを知ることができます。

このファイルにはさらに多くのコードがありますが、そのほとんどは非常に簡単です。

cstdintファイルは stdint.h を単に #include しているだけです。これにより、これらの宣言を2つの方法でインクルードすることができます。

#include <stdint.h> // C #include <cstdint> // C++ only
なぜこのようにしたのか、詳しくはC++ includesについて...の項をご覧ください。

ctype.hとcctype

ctype.hは、文字列中の文字がどのようなタイプであるかを決定するのに役立つマクロのセットです。これは、標準的なASCII文字セットのさまざまなプロパティに従うことによって行われます。asciitable.comから入手することができます。

このヘッダーファイルはいくつかのマクロと定数を含んでいます。

extern char _ctype[]; #define CT_UP 0x01 /* upper case */ #define CT_LOW 0x02 /* lower case */ #define CT_DIG 0x04 /* digit */ #define CT_CTL 0x08 /* control */ #define CT_PUN 0x10 /* punctuation */ #define CT_WHT 0x20 /* white space (space/cr/lf/tab) */ #define CT_HEX 0x40 /* hex digit */ #define CT_SP 0x80 /* hard space (0x20) */ #define isalnum(c) ((_ctype + 1)[(unsigned)(c)] & (CT_UP | CT_LOW | CT_DIG)) #define isalpha(c) ((_ctype + 1)[(unsigned)(c)] & (CT_UP | CT_LOW)) #define iscntrl(c) ((_ctype + 1)[(unsigned)(c)] & (CT_CTL)) #define isdigit(c) ((_ctype + 1)[(unsigned)(c)] & (CT_DIG)) #define isgraph(c) ((_ctype + 1)[(unsigned)(c)] & (CT_PUN | CT_UP | CT_LOW | CT_DIG)) #define islower(c) ((_ctype + 1)[(unsigned)(c)] & (CT_LOW)) #define isprint(c) ((_ctype + 1)[(unsigned)(c)] & (CT_PUN | CT_UP | CT_LOW | CT_DIG | CT_SP)) #define ispunct(c) ((_ctype + 1)[(unsigned)(c)] & (CT_PUN)) #define isspace(c) ((_ctype + 1)[(unsigned)(c)] & (CT_WHT)) #define isupper(c) ((_ctype + 1)[(unsigned)(c)] & (CT_UP)) #define isxdigit(c) ((_ctype + 1)[(unsigned)(c)] & (CT_DIG | CT_HEX)) #define isascii(c) ((unsigned)(c) <= 0x7F) #define toascii(c) ((unsigned)(c) & 0x7F) #define tolower(c) (isupper(c) ? c + 'a' - 'A' : c) #define toupper(c) (islower(c) ? c + 'A' - 'a' : c)
ここまではかなり単純なものです。上記のマクロは、個々の文字を決定したり変更したりするために使用することができます。

C++の場合、ctype.hの代わりにcctypeを使うこともできます。

va_list.hとstdarg

これらは、無名のパラメータにアクセスするためのマクロを含む標準的なヘッダであり、変数の引数リストを白くしています。

va_list.h

va_list.h は,可変長引数リストに使われるデータ型を抽象化したものです.
/* va list parameter list */ typedef unsigned char *va_list;

stdarg.hとcstdarg

これは、これから見る最後の基本的なライブラリインクルードファイルです。CやC++の可変長引数リストで使用するためのマクロが定義されています。

これらのマクロはかなりトリッキーなので、1つずつ見ていきましょう。

VA_SIZE

/* width of stack == width of int */ #define STACKITEM int /* round up width of objects pushed on stack. The expression before the & ensures that we get 0 for objects of size 0. */ #define VA_SIZE(TYPE) \ ((sizeof(TYPE) + sizeof(STACKITEM) - 1) \ & ~(sizeof(STACKITEM) - 1))
これは少しトリッキーです。VA_SIZE はスタックにプッシュされたパラメータのサイズを返します。 C と C++はルーチンにパラメータを渡すためにスタックを使用することを思い出してください。32 ビットマシンでは、各スタック項目は通常 32 ビットです。

va_start

/* &(LASTARG) points to the LEFTMOST argument of the function call (before the ...) */ #define va_start(AP, LASTARG) \ (AP=((va_list)&(LASTARG) + VA_SIZE(LASTARG)))
標準の va_start マクロは 2 つのパラメータを取ります。AP はパラメータリスト(va_list 型)へのポインタ、LASTARG はパラメータリストの最後のパラメータ(・・・の直前のパ ラメータ)である。

このルーチンがすることは、最後のパラメータのアドレスを取得し、そのアドレスにパラメータサイズを追加するだけです。 もしスタックサイズが32であれば、スタック上の最後のパラメータのアドレスに32を追加するだけです。

va_end

/* nothing for va_end */ #define va_end(AP)
ここですることはあまりありません。

va_arg

#define va_arg(AP, TYPE) \ (AP += VA_SIZE(TYPE), *((TYPE *)(AP - VA_SIZE(TYPE))))
va_arg()は、パラメータリストの次のパラメータを返します。 APは、現在作業中のパラメータリストへのポインタを含みます。TYPEにはデータ型(int, char, etc.)を指定します。

あとは、可変パラメータリストポインタ(AP)にデータ型(TYPE)のバイト数を足すだけです。これにより、可変パラメータリストポインタは、リストの次のパラメータを指すようになります。

この後、(ポインタの位置をインクリメントすることによって)今渡したデータを参照解除し、そのデータを返します。

まとめ

このチュートリアルでは、たくさんのことを学びました。読者の中には新しい概念もあるかもしれません。

このチュートリアルは、個人的には書きたいとは思いませんでした。私は、コードに飛び込む前に、いくつかの基本的な地面、理論、および設計概念をカバーするための素晴らしい、良い方法を見つけたかったのです。また、いくつかの基本的な標準ライブラリのヘッダも見て、システムの基本的な構造も見てきました。

基本的な必要事項が解決されたので、次のチュートリアルでは、実際のカーネルとハードウェア抽象化レイヤ (HAL) の構築を始めることにします。エラーと例外処理の理論と概念、割り込み処理、割り込み記述子テーブル(IDT)、プロセッサの例外をトリプルフォルトにならないようにトラップする方法について説明します。また、独自のスーパー1337 BSoDも構築することができます;)