Operating Systems Development Series | |
Prepare for the Kernel part 2
はじめにようこそ!:)前回のチュートリアルでは、プロテクトモードでの基本的な VGA プログラミングについて説明し、さらに 1337 のデモも作りました! このチュートリアルは、皆さんが待ち望んでいたものです。このチュートリアルは、以前のすべてのコードの上に直接構築され、1 MB のマークでカーネルをロードし、カーネルを実行します。 カーネルは私たちのOSの最も重要な部分です。カーネル...この謎の敵については、以前にも少しお話しましたね?これから数回のチュートリアルで、デザイン、構造、開発など、カーネルについてもっと詳しく説明します。 今、私たちはすでにすべてのセットアップを終えています...カーネルをロードして、Stage 2 にサヨナラするときが来ました! 注意:このチュートリアルは、ブートローダ 3 と 4 のチュートリアルの基本的な理解が必要です。ここでは、すべてを詳しく説明しますが、すべてのコンセプトは、ブートローダ 3 と 4 のチュートリアルで詳しく説明されています。もし、これらのチュートリアルを読んでいない場合は、まずこれらのチュートリアルを見てください。 これらのチュートリアルを読んでいれば、このチュートリアルはそれほど難しくはないはずです。 準備はいいですか? 基本的なカーネルスタブこれがこれから読み込むカーネルです。さて、ここには何もありません。次のセクションでこのプログラムを大きく発展させます。 すべて32ビットであることに注意してください。甘いだろ?これで16ビットの世界から完全に抜け出せます。 とりあえず、カーネルに到達したら、システムを停止させるだけです。 このファイルはおそらく、この後のシリーズでは一切使用しないことに注意してください。むしろ、32ビットC++コンパイラを使用する予定です。カーネルイメージをメモリにロードした後、カーネルエントルーチンのためにメモリ内のファイルをパースし、第2ステージのブートローダから直接Cのmain()ルーチンを呼び出します。 クールでしょう?言い換えれば、スタブファイルやプログラムなしで、第2ステージのブートローダから直接C++の世界に入ることができるのです。しかし、出発点が必要です。このため、このチュートリアルでは、基本的なスタブファイルを使用して、テストと動作のデモを行います。 次の数回のチュートリアルでは、コンパイラを立ち上げて動作させ、それを代わりに使用する予定です。しかし、今、私たちは自分たちを先取りしているのです ;) フロッピーインターフェースやったー!ステージ2を終わらせる時が来ました!カーネルをロードするために、もう一度FAT12を横断する必要があります。 しかし、その前にディスクからセクタを取得しなければなりません。このコードはブートローダと全く同じで、BIOS INT 0x13を使用してディスクからセクタをロードします。 このチュートリアルは完全な復習でもあるので、各ルーチンをセクションに分け、何が起こっているのかを正確に説明します。 セクタの読み込み - BIOS INT 0x13セクタのロードに必要なことは、ブートローダ 3 で説明しました。チュートリアルを振り返って、BIOS Interrupt 0x13 function 2を使ってセクタを読み込むことができることを思い出してください。さて、それでは。ここで問題なのは、保護モードに入る前にセクタをロードしなければならないことです。もし、プロテクトモードからBIOS割り込みを呼び出そうとすると、プロセッサはトリプルフォルトになります、覚えていますか?とにかく、割り込みは何だったのでしょうか?そうですね...。
INT 0x13/AH=0x02 - DISK : セクタをメモリに読み込む
を返します。 これはそんなに難しいことではありません。しかし、ブートローダーのチュートリアルを思い出してください。つまり、セクタ、トラック、ヘッド番号を追跡し、トラックを超えてセクタをロードしようとしないことを確認する必要があります。つまり、1つのトラックには18のセクタがあることを思い出してください。セクタ番号を18より大きく設定すると、コントローラが故障し、プロセッサがトリプルフォルトになります。 OK...1トラックあたり18セクタです。各セクタは512バイトであることを忘れないでください。また、片面80トラックであることも覚えておいてください。 それじゃ!これらの情報はすべて...トラックあたりのセクタ数、トラック数、ヘッド数、セクタの大きさは、完全にディスクそのものに依存します。セクタは512バイトである必要はないことを思い出してください。 OEMパラメータ・ブロックにすべてを記述しています。 これは見覚えがあるはずです!各メンバーはチュートリアル5で説明しました。ここでのすべての詳細な説明はチュートリアルをご覧ください。 これで、ディスクからメモリ上の任意の場所に任意の数のセクタをロードできるメソッドができました。 しかし、すぐに問題にぶつかります。ロードしたいセクタは決まっています。しかし、BIOSのINT 0x13はセクタを扱うことができないのです。しかし、BIOSのINT 0x13はセクタには対応していません。 では、これが何か関係があるのでしょうか?もし私たちがセクタ20をロードしたいと想像してください。1トラックには18セクタしかありませんから、この数字を直接使うことはできません。現在のトラックの20番目のセクターから読み込もうとすると、そのセクターは存在しないので、フロッピーコントローラは失敗し、プロセッサはトリプルフォルトになります。20番目のセクタを読み出すには、Track 2 Sector 2, Head 0を読み出す必要があるのです。 つまり、読み込むセクタを指定するには、リニアなセクタ番号を、ディスク上の正確なシリンダー、トラック、セクタの位置に変換する必要があるということです。 ちょっと待ってください、CHSからLBAへの変換ルーチンを覚えていますか? LBAからCHSへの変換見覚えはありませんか?リニアブロックアドレッシング(LBA)は単にディスク上のインデックスされた場所を表します。最初のブロックが0、2番目のブロックが1です。つまり、LBAは0から始まるセクタ番号を表すだけで、各「ブロック」は1つの「セクタ」なのです。とにかく...このセクタ番号(LBA)をディスク上の正確なシリンダ/ヘッド/セクタの位置に変換する方法を探さなければなりません。Bootloaders 4のチュートリアルにあった、この方法を覚えていますか? 読者の中には、このコードはかなりトリッキーだと言う人がいました。そこで、ここで詳しく説明します。 まず、フォーラムラをもう一度見てみましょう。 なるほど!これはかなり簡単でしょう?論理セクタ "が実際のセクタ番号です。論理セクタ/トラックあたりのセクタは、上記のすべての式の中にあることに注意してください。 この除算はすべての式の中にあるので、その結果を保存して他の2つの式に使用することができます。 これを例にしてみましょう。20番目のセクタはトラック2、セクタ2だといいましたね。 では、この式を試してみましょう。 私たちは絶対数(2)--嗚呼、セクタ2!--を保持するだけです。LBAアドレス指定は0から始まるので、ここで1を加える必要があることに注意してください。 基本式「論理セクタ/トラックあたりのセクタ」はこれらの式のすべてにあることを忘れないでください。 この例では単に1.111111111111111111111111(上の式ではさらに1を加えています。 整数を扱うので、これは単に1なのです)です。 OEMブロックでは、1気筒あたり2個のヘッドを指定したことを思い出してください。これまでのところ、これはヘッド1のセクター2を示しています。 素晴らしい - しかし、我々はどのようなトラックにしている? これは、上記と全く同じ式であることに注意してください。唯一の違いは、その単純な操作です。 とにかく...式に従うと、次のようになります。論理セクタ20は、セクタ2トラック2ヘッド0にあることになります。 さて、それではこれらの式をコードに適用してみましょう。 LBACHSの説明です。詳細 さて、このルーチンは1つのパラメータを取ります。この式(論理セクタ/トラックあたりのセクタ数)は3つの式の一部であることに注意してください。これを何度も再計算するよりも、一度だけ計算し、その結果を他のすべての計算で使用する方が効率的です...このルーチンはこのように動作します。 今、AXはトラック操作ごとの論理セクタ/セクタを含んでいます。 セクタ1から開始(論理セクタ/セクタ・パー・トラックの+1を覚えていますか?) DXをクリアします。AXにはまだ論理セクタ/トラックあたりのセクタの結果が入っています。 さて、次は計算式です。 絶対ヘッド = (論理セクタ / トラックあたりのセクタ数) MOD個のヘッド数 絶対トラック = 論理セクタ / (トラックあたりのセクタ数 * ヘッド数) 乗算の結果は、ヘッド数で除算されます。つまり、この2つの違いは演算の仕方だけで、1つは除算、もう1つは除算の余り(モジュラス)なのです。 では、余り(MOD)と除算結果の両方を返せる命令は何でしょう?DIVです! (論理セクタ数/トラックあたりのセクタ数)はAXのままなので、あとはシリンダあたりのヘッド数で割ればいいだけ...ということを覚えておいてください。 アブソリュートヘッドとアブソリュートトラックの式は非常に似ています。実際に違うのは動作だけです。この単純なDIV命令は、DXとAXの両方を設定します。AXはHeadsPerCylinderのDIVを格納し、DXは同じ操作のREMAINDER(Modolus)を格納します。 これで少しはすっきりしたでしょうか。そうでない場合は、私に教えてください;) CHSをLBAに変換するこれは、よりシンプルです。
セクタの読み込みさて、これでセクタを読み込むためのすべてが揃いました。このコードもブートローダと全く同じです。さて、ここでは5回セクタを読み込もうとしています。 レジスタはスタックに格納されます。開始セクタはリニアなセクタ番号です(AXに格納)。 BIOSのINT 0x13を使用しているので、ディスクから読み込む前にこれをCHSに変換する必要があります。 そこで、LBAからCHSへの変換ルーチンを使用しています。ここで、absoluteTrackにはトラック番号が、absoluteSectorにはトラック内のセクタが、absoluteHeadにはヘッド番号が入ります。 これらはすべて、LBA から CHA への変換ルーチンによって設定されたものです。 さて、セクタを読み込むための設定を行い、BIOSに読み込むように要求します。簡単にするために、実行中のBIOS INT 0x13ルーチンをもう一度見てみましょう。
INT 0x13/AH=0x02 - DISK : セクタをメモリに読み込む 上のコードの実行方法と比較してみてください。かなりシンプルでしょう? 書き込むバッファはES:BXにあり、INT 0x13はバッファとして参照されていることを思い出してください。このルーチンにES:BXを渡したので、この場所がセクタをロードする場所となります。 BIOS INT 0x13関数2は、エラーがあればキャリーフラグ(CF)をセットします。 エラーがあれば、カウンターをデクリメントし(5回試すようにループをセットしたのを覚えていますか)、再試行します! ...これは再起動します。 5回とも失敗した場合(CX=0、Zeroフラグセット)、INT 0x18命令にフォールダウンします。 ...これでコンピュータはリブートされます。 キャリーフラグがセットされていない場合(CF=0)、jnz命令はエラーがなかったことを示すので、ここでジャンプします。セクタは正常に読み込まれました。 あとは、レジスタを復元して、次のセクタに行くだけです。難しいことではありません :)ES:BXにはセクタをロードするアドレスが含まれているので、次のセクタに行くにはBXをセクタごとにバイト単位でインクリメントする必要があることに注意してください。 AXには読み出し開始のセクタが入っていましたので、これもインクリメントする必要があります。 以上です。このルーチンの完全な説明については、Bootloaders 4を参照してください。 フロッピー16.incデモの例では、すべてのフロッピーアクセス・ルーチンはFloppy16.inc に含まれています。FAT12 インタフェースやった--セクタをロードできるぞ。Woohoo... :( ご承知のように、これではたいしたことはできません。次に必要なことは、「ファイル」の基本的な定義と、「ファイル」とは何かということを作ることです。これは、ファイル・システムによって行われます。ファイルシステムは非常に複雑になることがあります。このコードがどのように動作するかを完全に理解するために、このコードを説明する間、ブートローダ4を参照してください。 定数Fat12のパースでは、ルート・ディレクトリ・テーブルとFATテーブルをロードするための場所が必要になります。簡単にするために、これらの場所を定数の後ろに隠しておきましょう。ルート・ディレクトリ・テーブルを0x2e00に、FATを0x2c00にロードすることにします。FAT_SEGとROOT_SEGは、セグメントレジスタにロードするために使用されます。 FAT12をトラバースするご存知のように、OSのコードの中には単に醜いものがあります。ファイルシステムのコードもその一つだと私は考えています。これが、このレビューのようなチュートリアルで、このコードについて説明しようと思った理由の1つです。FAT12 のコードは基本的にブートローダと同じですが、メインプログラムとの依存関係を少なくするために、修正することにしました。このため、ここで詳しく説明することにしました。注意:ここでは、FAT12について詳しく説明しません。詳細はブートローダ4チュートリアルを参照してください。 とにかく、ご存知のように、FAT12をトラバースするために、最初にロードする必要があるのは、ルートディレクトリ・テーブルです。 Root Directory Tableのロードディスクの構造です。
ルートディレクトリ・テーブルは、FATのリザーブドセクタの直後に位置することを思い出してください。 ルート・ディレクトリ・テーブルを読み込むには、メモリ内で現在必要のない場所を探し、そこにコピーする必要があります。とりあえず、0x7E00 (リアルモード: 0x7E0:0) を選びました。これはブートローダのすぐ上にあり、上書きしたことがないのでまだメモリ内にあります。 ここで、重要なコンセプトがあります。これは、物理的にどこに何があるかを追跡する必要があるため、非常に悪いことです。そこで、低レベルメモリーマネージャの出番です。詳しくは後ほど... まず、現在のレジスタの状態を保存します。そうしないと、それを使っている他のプログラムに影響を与えるので、非常にまずい。 これで、ルートディレクトリテーブルのサイズがわかり、ロードするセクタの数がわかります。 ブートローダ4で説明したように、各エントリは32バイトの大きさです。FAT12フォーマットのディスクに新しいファイルを追加すると、Windowsは自動的にルートディレクトリに追加し、OEMパラメータブロックの bpbRootEntriesバイトオフセット変数に追加します。 ほら、Windowsは親切でしょう?) つまり...各エントリのサイズが32バイトだとすると、32バイトにルートディレクトリの数を掛ければ、ルートディレクトリテーブルに何バイトあるかわかるということです。しかし、セクタ数が必要なので、この結果をセクタ数で割る必要があります。 よし、これでAX=ルートディレクトリがとるセクタの数だ。さて、次は開始位置を探さなければなりません。 ブートローダ4から思い出してください:ルートディレクトリのテーブルは、ディスク上のFATと予約セクタの両方の後に右です。 上のディスク構造表を見て、ルートディレクトリのテーブルがどこにあるのか確認してください。 つまり...FATのセクタ数を求め、それを予約セクタに加えれば、ディスク上の正確な位置がわかるということです。 さて、読み込むべきセクタ数と正確な開始セクタがわかったので、読み込んでみましょう! ROOT_SEG:0 に読み込むために、seg:offset ロケーションを設定したことに注意してください。 次は、FATの読み込みです FATの読み込みBootloaders 4 で、FAT12 フォーマットのディスク構造について説明しました。 時間をさかのぼって(tm)、もう一度見てみましょう。ディスクの構造です。
FATは1つまたは2つあることを思い出してください。また、ディスク上の予約セクタのすぐ後にあることにも注目してください。これは見慣れた光景でしょう。 まず、ロードするセクタの数を知る必要があります。ディスクの構造をもう一度見てください。FAT の数 (および FAT ごとのセクタ数) は OEM パラメーター・ブロックに格納されています。したがって、総セクタ数を得るには、これらを掛け合わせればよいのです。 さて、FATの前にある予約セクタを考慮する必要があります... やったー!さて、CXはロードするセクタの数を含んでいるので、セクタをロードするためにルーチンを呼び出します! これがすべてです。 ファイルの検索ファイルを検索するには、検索対象のファイル名が必要です。DOSでは8.3命名規則(8バイトのファイル名、3文字の拡張子)に従って11バイトのファイル名を使用することを覚えておいてください。Root Directory Tableの形式を思い出してください。ファイル名はエントリの最初の11バイトに格納されています。各ディレクトリエントリの形式をもう一度見てみましょう。
一致するものが見つかったら、そのエントリのバイト26を参照して、現在のクラスタを取得する必要があります。 これらはすべて、見覚えがあるはずです。 さて...次はコードです。 まず、現在のレジスタの状態を保存します。SIを使用する必要があるので、現在のファイル名をどこかに保存する必要があります...BXでしょうか? 画像名を見つけるために、Root Directoryテーブルを解析する必要があることを思い出してください。そのためには、ディレクトリテーブルの各エントリの最初の11バイトをチェックして、一致するものがあるかどうかを確認する必要があります。簡単そうでしょう? これを行うには、エントリの数を知る必要があります。 さて、これでCXには調べるべきエントリの数が格納されました。あとは、11バイト文字のファイル名をループして比較するだけです。文字列命令を使っているので、まず方向フラグがクリアされていることを確認したいのですが、これはcldが行うことです。 DIには、ディレクトリテーブルの現在のオフセットが設定される。つまり、ES:DIはテーブルの開始位置を指しているので、これを解析してみましょう。 11バイトが一致すれば、ファイルが見つかったことになります。DIにはテーブル内のエントリーの位置が含まれているので、すぐに.Foundにジャンプします。 一致しない場合、テーブルの次のエントリを試す必要がある。DIに32バイトを追加します(各エントリが32バイトであることを思い出してください)。 ファイルが見つからなかった場合、スタックに残っているレジスタのみをリストアし、-1(エラー)を返す。 ファイルが見つかった場合は、すべてのレジスタをリストアする。AXには、ルートディレクトリテーブル内のエントリ位置が格納されているので、読み込むことができる。 やったー!これでファイルを見つけることができたので(Root Directory Table内の位置も取得できた)、それをロードしてみましょう。 ファイルのロードこれでようやくすべての設定が終わったので、いよいよファイルをロードする時が来ました!このほとんどは、他のルーチンを呼び出しているので、非常に簡単です。ここでループし、ファイルのすべてのクラスタがメモリにロードされることを確認します。 ここでは、レジスターを保存するだけです。バッファのコピーをどこかに書き込む必要があるので、それもスタックに保存しています。CXはロードしたセクタの数を記録するために使います。 これは後でスタックに保存します。 ファイルを読み込むには、まずファイルを見つける必要があります(当たり前といえば当たり前ですが ^^)ここでは、FindFileルーチンを簡単に使うことができます。FindFileは、エラー時にはAXに-1をセットし、成功時にはルートディレクトリテーブル内の開始エントリ位置をセットします。このインデックスを使って、ファイルについて知りたいと思ったことを何でも得ることができます。 さて、ここまでくれば、ファイルが見つかったことになります。ES:DIにはFindFile()で設定された最初のルートエントリの位置が含まれているので、ES:DIを参照することで事実上ファイルのエントリを取得することができるのです。 前節の上のエントリ記述表を見返してください。0x1Aバイトをオフセットして26バイト目(開始クラスタ番号)に到達できることに注目し、それを格納します。 上記は面倒なことだと思います。FindFileの呼び出しによってAXがエントリ番号に設定されたのを覚えていますか?それをここに保存する必要がありますが、書き込むためのバッファをまだスタックの一番上に保持する必要があります。これが、ここでスタックを少し弄った理由です :) とにかく、次はFATをロードします。これは信じられないほど簡単です。 FATがロードされ、開始ファイルクラスタができたので、次は実際にファイルのセクタを読み込む番です このコードはそれほど悪いものではありません。FAT12では、各クラスタはちょうど512バイトであることを覚えていますか? つまり、各クラスタは単に「セクタ」を表します。まず、開始クラスタ/セクタ番号を取得します。しかし、クラスタ番号は線形番号なので、クラスタ番号だけでは大したことはできません。つまり、これはCHSNotLBAフォーマットのセクタ番号で、トラックとヘッドの情報があることが前提です。ReadSectors()はLBAのリニア・セクタ番号を必要とするので、このCHSをLBAアドレスに変換します。そして、クラスタごとのセクタを取得し、それを読み込む! ESとBXは最初からスタックにプッシュされているので、ポップすることに注意してください。ES:BX はこのルーチンに渡された ES:BP バッファを指しており、セクタをロードするバッファを含んでいます。 さて、クラスタがロードされたので、ファイルの終端に達したかどうかをFATでチェックする必要があります。しかし、FATの各エントリーは12バイトであることを思い出してください。Bootloaders 4で、FATを読むときにあるパターンがあることがわかりました。 偶数クラスタの場合は下位12ビット、上位クラスタの場合は上位12ビットを読み取ります。 詳しくは、ブートローダーズ4をご覧ください。 偶数か奇数かの判断は2で割ればいいだけです。 これで全部です!少し複雑ですが、それほど難しくはないと思います。) Fat12.inc素晴らしい!FAT12のコードはすべてFat12.incにあります。ステージ2の終了ステージ2に戻る - カーネルのロードと実行さて、面倒なコードは終わったので、あとはステージ2からカーネル・イメージをメモリにロードして、カーネルを実行するだけです。問題は、「どこで?どこで?1MBにロードしたいところですが、まだ直接はできません。なぜなら、まだリアル・モードだからです。このため、まず最初にイメージをより低いアドレスにロードする必要があります。保護モードに切り替えた後、カーネルを新しい場所にコピーすることができます。これは1MBでも、ページングが有効であれば3GBでも構いません。 今、私たちのカーネルはIMAGE_RMODE_BASE:0にロードされています。ImageSizeはロードされたセクタ数(カーネルのサイズ)を含んでいます。 プロテクトモード内で実行するために必要なことは、ジャンプするか、それを呼び出すだけです。カーネルを1MBにしたいので、実行する前にまずコピーする必要があります。 しかし、ここで少し問題があります。これは、カーネルが純粋なバイナリファイルであると仮定しています。Cはこれをサポートしないので、これを持つことはできません。カーネルはCがサポートするバイナリ形式である必要があり、Cを使ってカーネルをロードするためにそれを解析する必要があります。今のところ、この純粋なバイナリを維持しますが、次の数回のチュートリアルでこれを修正する予定です。かっこいいですか? まとめこのチュートリアルは多くの新しいコードをカバーしました。このチュートリアルのコンセプトのほとんどは以前に説明したものなので、このチュートリアルがそれほど難しいものでないことを願っています ;) しかし、このチュートリアルでは、これらの概念を新しい視点でカバーしました。これは、これらのトピックをもう少し理解するのに役立ち、また、それらが別々のルーチンに実装されるのを見ることができます。 ディスクからセクタをロードするコードを開発し、FAT12を解析して好きな場所にKernelをロードすることができました。クールでしょう?このシリーズでは、カーネルを1MBにロードしています。 基本的なフル 32 ビットカーネルがようやくロードされ、実行できるようになったので、オペ レーティングシステムで最も重要な部分であるカーネルにようやく焦点を当てられるようになりました。 次の数回のチュートリアルでは、カーネルの理論、革命、および設計をカバーします。そして、低レベルCプログラミングと、高レベル言語の概念と理論による低レベルプログラミングをカバーし始めます。 カーネルレベルのC言語プログラミングでは、他のプログラミング分野では許されない多くの自由があります。例えば、「アクセス違反」は存在しないので、メモリ上のすべてのバイトを直接制御することができます。しかし、悪い点もあります。さらに悪いことに、C言語という抽象化されたレイヤーを使って、低レベルの環境をプログラミングしていることも忘れてはいけません。 これから数回のチュートリアルですべてをカバーし、カーネルで動作するようにCをセットアップする予定です。待ち遠しいですね。 |