Operating Systems Development Series
Prepare for the Kernel part 2
by Mike, 2008, 2009

はじめに

ようこそ!:)

前回のチュートリアルでは、プロテクトモードでの基本的な VGA プログラミングについて説明し、さらに 1337 のデモも作りました!

このチュートリアルは、皆さんが待ち望んでいたものです。このチュートリアルは、以前のすべてのコードの上に直接構築され、1 MB のマークでカーネルをロードし、カーネルを実行します。

カーネルは私たちのOSの最も重要な部分です。カーネル...この謎の敵については、以前にも少しお話しましたね?これから数回のチュートリアルで、デザイン、構造、開発など、カーネルについてもっと詳しく説明します。

今、私たちはすでにすべてのセットアップを終えています...カーネルをロードして、Stage 2 にサヨナラするときが来ました!

注意:このチュートリアルは、ブートローダ 3 と 4 のチュートリアルの基本的な理解が必要です。ここでは、すべてを詳しく説明しますが、すべてのコンセプトは、ブートローダ 3 と 4 のチュートリアルで詳しく説明されています。もし、これらのチュートリアルを読んでいない場合は、まずこれらのチュートリアルを見てください。

ブートローダ 3

ブートローダ 4

これらのチュートリアルを読んでいれば、このチュートリアルはそれほど難しくはないはずです。

準備はいいですか?

基本的なカーネルスタブ

これがこれから読み込むカーネルです。
; We are still pure binary. We will fix this in the next few tutorials :) org 0x10000 ; Kernel starts at 1 MB bits 32 ; 32 bit code jmp Stage3 ; jump to stage 3 %include "stdio.inc" ; Our stdio.inc file we developed from the previous tutorial msg db 0x0A, 0x0A, "Welcome to Kernel Land!!", 0x0A, 0 Stage3: ;-------------------------------; ; Set registers ; ;-------------------------------; mov ax, 0x10 ; set data segments to data selector (0x10) mov ds, ax mov ss, ax mov es, ax mov esp, 90000h ; stack begins from 90000h ;---------------------------------------; ; Clear screen and print success ; ;---------------------------------------; call ClrScr32 mov ebx, msg call Puts32 ;---------------------------------------; ; Stop execution ; ;---------------------------------------; cli hlt
さて、ここには何もありません。次のセクションでこのプログラムを大きく発展させます。

すべて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 : セクタをメモリに読み込む
AH = 0x02
AL = 読み込むセクタ数
CH = シリンダ番号の下位8ビット
CL = セクタ番号(ビット0-5)。6-7ビットはハードディスク用のみ
DH = ヘッド番号
DL = ドライブ番号 (ハードディスクの場合はビット7がセットされます)
ES:BX = セクタを読み込むためのバッファ

を返します。
AH = ステータス・コード
AL = 読み込まれたセクタの数
CF = 失敗したらセット、成功したらクリア

これはそんなに難しいことではありません。しかし、ブートローダーのチュートリアルを思い出してください。つまり、セクタ、トラック、ヘッド番号を追跡し、トラックを超えてセクタをロードしようとしないことを確認する必要があります。つまり、1つのトラックには18のセクタがあることを思い出してください。セクタ番号を18より大きく設定すると、コントローラが故障し、プロセッサがトリプルフォルトになります。

OK...1トラックあたり18セクタです。各セクタは512バイトであることを忘れないでください。また、片面80トラックであることも覚えておいてください。

それじゃ!これらの情報はすべて...トラックあたりのセクタ数、トラック数、ヘッド数、セクタの大きさは、完全にディスクそのものに依存します。セクタは512バイトである必要はないことを思い出してください。

OEMパラメータ・ブロックにすべてを記述しています。

bpbOEM db "My OS " bpbBytesPerSector: DW 512 bpbSectorsPerCluster: DB 1 bpbReservedSectors: DW 1 bpbNumberOfFATs: DB 2 bpbRootEntries: DW 224 bpbTotalSectors: DW 2880 bpbMedia: DB 0xf0 ;; 0xF1 bpbSectorsPerFAT: DW 9 bpbSectorsPerTrack: DW 18 bpbHeadsPerCylinder: DW 2 bpbHiddenSectors: DD 0 bpbTotalSectorsBig: DD 0 bsDriveNumber: DB 0 bsUnused: DB 0 bsExtBootSignature: DB 0x29 bsSerialNumber: DD 0xa0a1a2a3 bsVolumeLabel: DB "MOS FLOPPY " bsFileSystem: DB "FAT12 "
これは見覚えがあるはずです!各メンバーはチュートリアル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のチュートリアルにあった、この方法を覚えていますか?

読者の中には、このコードはかなりトリッキーだと言う人がいました。そこで、ここで詳しく説明します。

まず、フォーラムラをもう一度見てみましょう。

absolute sector = (logical sector / sectors per track) + 1 absolute head = (logical sector / sectors per track) MOD number of heads absolute track = logical sector / (sectors per track * number of heads)
なるほど!これはかなり簡単でしょう?論理セクタ "が実際のセクタ番号です。論理セクタ/トラックあたりのセクタは、上記のすべての式の中にあることに注意してください。

この除算はすべての式の中にあるので、その結果を保存して他の2つの式に使用することができます。

これを例にしてみましょう。20番目のセクタはトラック2、セクタ2だといいましたね。 では、この式を試してみましょう。

absolute sector = (logical sector / sectors per track) + 1 2.1111111111111111111111111111111 = 20 / 18 (sectors per track) + 1
私たちは絶対数(2)--嗚呼、セクタ2!--を保持するだけです。LBAアドレス指定は0から始まるので、ここで1を加える必要があることに注意してください。 基本式「論理セクタ/トラックあたりのセクタ」はこれらの式のすべてにあることを忘れないでください。 この例では単に1.111111111111111111111111(上の式ではさらに1を加えています。 整数を扱うので、これは単に1なのです)です。
absolute head = (logical sector / sectors per track) MOD number of heads (1) MOD Number of heads (2) = Head 1
OEMブロックでは、1気筒あたり2個のヘッドを指定したことを思い出してください。これまでのところ、これはヘッド1のセクター2を示しています。 素晴らしい - しかし、我々はどのようなトラックにしている?
absolute track = logical sector / (sectors per track * number of heads) (1) * Number of heads (2) = Track 2
これは、上記と全く同じ式であることに注意してください。唯一の違いは、その単純な操作です。

とにかく...式に従うと、次のようになります。論理セクタ20は、セクタ2トラック2ヘッド0にあることになります。

さて、それではこれらの式をコードに適用してみましょう。

LBACHSの説明です。詳細

さて、このルーチンは1つのパラメータを取ります。この式(論理セクタ/トラックあたりのセクタ数)は3つの式の一部であることに注意してください。これを何度も再計算するよりも、一度だけ計算し、その結果を他のすべての計算で使用する方が効率的です...このルーチンはこのように動作します。

LBACHS: xor dx, dx ; prepare dx:ax for operation div WORD [bpbSectorsPerTrack] ; calculate
今、AXはトラック操作ごとの論理セクタ/セクタを含んでいます。

セクタ1から開始(論理セクタ/セクタ・パー・トラックの+1を覚えていますか?)

inc dl ; adjust for sector 0 mov BYTE [absoluteSector], dl
DXをクリアします。AXにはまだ論理セクタ/トラックあたりのセクタの結果が入っています。
xor dx, dx ; prepare dx:ax for operation
さて、次は計算式です。

絶対ヘッド = (論理セクタ / トラックあたりのセクタ数) MOD個のヘッド数

絶対トラック = 論理セクタ / (トラックあたりのセクタ数 * ヘッド数)

乗算の結果は、ヘッド数で除算されます。つまり、この2つの違いは演算の仕方だけで、1つは除算、もう1つは除算の余り(モジュラス)なのです。

では、余り(MOD)と除算結果の両方を返せる命令は何でしょう?DIVです!

(論理セクタ数/トラックあたりのセクタ数)はAXのままなので、あとはシリンダあたりのヘッド数で割ればいいだけ...ということを覚えておいてください。

div WORD [bpbHeadsPerCylinder] ; calculate
アブソリュートヘッドとアブソリュートトラックの式は非常に似ています。実際に違うのは動作だけです。この単純なDIV命令は、DXとAXの両方を設定します。AXはHeadsPerCylinderのDIVを格納し、DXは同じ操作のREMAINDER(Modolus)を格納します。
mov BYTE [absoluteHead], dl mov BYTE [absoluteTrack], al ret
これで少しはすっきりしたでしょうか。そうでない場合は、私に教えてください;)

CHSをLBAに変換する

これは、よりシンプルです。
ClusterLBA: ; LBA = (cluster - 2 ) * sectors per cluster sub ax, 0x0002 ; subtract 2 from cluster number xor cx, cx mov cl, BYTE [bpbSectorsPerCluster] ; get sectors per cluster mul cx ; multply

セクタの読み込み

さて、これでセクタを読み込むためのすべてが揃いました。このコードもブートローダと全く同じです。
;************************************************; ; Reads a series of sectors ; CX=>Number of sectors to read ; AX=>Starting sector ; ES:BX=>Buffer to read to ;************************************************; ReadSectors: .MAIN mov di, 0x0005 ; five retries for error
さて、ここでは5回セクタを読み込もうとしています。
.SECTORLOOP push ax push bx push cx call LBACHS ; convert starting sector to CHS
レジスタはスタックに格納されます。開始セクタはリニアなセクタ番号です(AXに格納)。 BIOSのINT 0x13を使用しているので、ディスクから読み込む前にこれをCHSに変換する必要があります。 そこで、LBAからCHSへの変換ルーチンを使用しています。ここで、absoluteTrackにはトラック番号が、absoluteSectorにはトラック内のセクタが、absoluteHeadにはヘッド番号が入ります。 これらはすべて、LBA から CHA への変換ルーチンによって設定されたものです。
mov ah, 0x02 ; BIOS read sector mov al, 0x01 ; read one sector mov ch, BYTE [absoluteTrack] ; track mov cl, BYTE [absoluteSector] ; sector mov dh, BYTE [absoluteHead] ; head mov dl, BYTE [bsDriveNumber] ; drive int 0x13 ; invoke BIOS
さて、セクタを読み込むための設定を行い、BIOSに読み込むように要求します。簡単にするために、実行中のBIOS INT 0x13ルーチンをもう一度見てみましょう。

INT 0x13/AH=0x02 - DISK : セクタをメモリに読み込む
AH = 0x02
AL = 読み込むセクタ数
CH = シリンダ番号の下位8ビット
CL = セクタ番号(ビット0-5)。6-7ビットはハードディスク用のみ
DH = ヘッド番号
DL = ドライブ番号 (ハードディスクの場合はビット7がセットされます)
ES:BX = セクタの読み出し先となるバッファ

上のコードの実行方法と比較してみてください。かなりシンプルでしょう?

書き込むバッファはES:BXにあり、INT 0x13はバッファとして参照されていることを思い出してください。このルーチンにES:BXを渡したので、この場所がセクタをロードする場所となります。

jnc .SUCCESS ; test for read error xor ax, ax ; BIOS reset disk int 0x13 ; invoke BIOS dec di ; decrement error counter pop cx pop bx pop ax jnz .SECTORLOOP ; attempt to read again
BIOS INT 0x13関数2は、エラーがあればキャリーフラグ(CF)をセットします。 エラーがあれば、カウンターをデクリメントし(5回試すようにループをセットしたのを覚えていますか)、再試行します! ...これは再起動します。

5回とも失敗した場合(CX=0、Zeroフラグセット)、INT 0x18命令にフォールダウンします。

int 0x18
...これでコンピュータはリブートされます。

キャリーフラグがセットされていない場合(CF=0)、jnz命令はエラーがなかったことを示すので、ここでジャンプします。セクタは正常に読み込まれました。

.SUCCESS pop cx pop bx pop ax add bx, WORD [bpbBytesPerSector] ; queue next buffer inc ax ; queue next sector loop .MAIN ; read next sector ret
あとは、レジスタを復元して、次のセクタに行くだけです。難しいことではありません :)ES:BXにはセクタをロードするアドレスが含まれているので、次のセクタに行くにはBXをセクタごとにバイト単位でインクリメントする必要があることに注意してください。

AXには読み出し開始のセクタが入っていましたので、これもインクリメントする必要があります。

以上です。このルーチンの完全な説明については、Bootloaders 4を参照してください。

フロッピー16.inc

デモの例では、すべてのフロッピーアクセス・ルーチンはFloppy16.inc に含まれています。

FAT12 インタフェース

やった--セクタをロードできるぞ。Woohoo... :( ご承知のように、これではたいしたことはできません。次に必要なことは、「ファイル」の基本的な定義と、「ファイル」とは何かということを作ることです。これは、ファイル・システムによって行われます。

ファイルシステムは非常に複雑になることがあります。このコードがどのように動作するかを完全に理解するために、このコードを説明する間、ブートローダ4を参照してください。

定数

Fat12のパースでは、ルート・ディレクトリ・テーブルとFATテーブルをロードするための場所が必要になります。簡単にするために、これらの場所を定数の後ろに隠しておきましょう。
%define ROOT_OFFSET 0x2e00 %define FAT_SEG 0x2c0 %define ROOT_SEG 0x2e0
ルート・ディレクトリ・テーブルを0x2e00に、FATを0x2c00にロードすることにします。FAT_SEGとROOT_SEGは、セグメントレジスタにロードするために使用されます。

FAT12をトラバースする

ご存知のように、OSのコードの中には単に醜いものがあります。ファイルシステムのコードもその一つだと私は考えています。これが、このレビューのようなチュートリアルで、このコードについて説明しようと思った理由の1つです。FAT12 のコードは基本的にブートローダと同じですが、メインプログラムとの依存関係を少なくするために、修正することにしました。このため、ここで詳しく説明することにしました。

注意:ここでは、FAT12について詳しく説明しません。詳細はブートローダ4チュートリアルを参照してください。

とにかく、ご存知のように、FAT12をトラバースするために、最初にロードする必要があるのは、ルートディレクトリ・テーブルです。

Root Directory Tableのロード

ディスクの構造です。
Boot Sector Extra Reserved Sectors File Allocation Table 1 File Allocation Table 2 Root Directory (FAT12/FAT16 Only) Data Region containng files and directories.

ルートディレクトリ・テーブルは、FATのリザーブドセクタの直後に位置することを思い出してください。

ルート・ディレクトリ・テーブルを読み込むには、メモリ内で現在必要のない場所を探し、そこにコピーする必要があります。とりあえず、0x7E00 (リアルモード: 0x7E0:0) を選びました。これはブートローダのすぐ上にあり、上書きしたことがないのでまだメモリ内にあります。

ここで、重要なコンセプトがあります。これは、物理的にどこに何があるかを追跡する必要があるため、非常に悪いことです。そこで、低レベルメモリーマネージャの出番です。詳しくは後ほど...

;******************************************* ; LoadRoot () ; - Load Root Directory Table ;******************************************* LoadRoot: pusha ; store registers push es
まず、現在のレジスタの状態を保存します。そうしないと、それを使っている他のプログラムに影響を与えるので、非常にまずい。

これで、ルートディレクトリテーブルのサイズがわかり、ロードするセクタの数がわかります。

ブートローダ4で説明したように、各エントリは32バイトの大きさです。FAT12フォーマットのディスクに新しいファイルを追加すると、Windowsは自動的にルートディレクトリに追加し、OEMパラメータブロックの bpbRootEntriesバイトオフセット変数に追加します。

ほら、Windowsは親切でしょう?)

つまり...各エントリのサイズが32バイトだとすると、32バイトにルートディレクトリの数を掛ければ、ルートディレクトリテーブルに何バイトあるかわかるということです。しかし、セクタ数が必要なので、この結果をセクタ数で割る必要があります。

; compute size of root directory and store in "cx" xor cx, cx ; clear registers xor dx, dx mov ax, 32 ; 32 byte directory entry mul WORD [bpbRootEntries] ; total size of directory div WORD [bpbBytesPerSector] ; sectors used by directory xchg ax, cx ; move into AX
よし、これでAX=ルートディレクトリがとるセクタの数だ。さて、次は開始位置を探さなければなりません。

ブートローダ4から思い出してください:ルートディレクトリのテーブルは、ディスク上のFATと予約セクタの両方の後に右です。 上のディスク構造表を見て、ルートディレクトリのテーブルがどこにあるのか確認してください。

つまり...FATのセクタ数を求め、それを予約セクタに加えれば、ディスク上の正確な位置がわかるということです。

; compute location of root directory and store in "ax" mov al, byte [bpbNumberOfFATs] ; number of FATs mul word [bpbSectorsPerFAT] ; sectors used by FATs add ax, word [bpbReservedSectors] ; adjust for bootsector mov word [datasector], ax ; base of root directory add word [datasector], cx
さて、読み込むべきセクタ数と正確な開始セクタがわかったので、読み込んでみましょう!
; read root directory push word ROOT_SEG pop es mov bx, 0x0 ; copy root dir call ReadSectors ; read in directory table pop es popa ; restore registers and return ret
ROOT_SEG:0 に読み込むために、seg:offset ロケーションを設定したことに注意してください。

次は、FATの読み込みです

FATの読み込み

Bootloaders 4 で、FAT12 フォーマットのディスク構造について説明しました。 時間をさかのぼって(tm)、もう一度見てみましょう。

ディスクの構造です。

Boot Sector Extra Reserved Sectors File Allocation Table 1 File Allocation Table 2 Root Directory (FAT12/FAT16 Only) Data Region containng files and directories.

FATは1つまたは2つあることを思い出してください。また、ディスク上の予約セクタのすぐ後にあることにも注目してください。これは見慣れた光景でしょう。

;******************************************* ; LoadFAT () ; - Loads FAT table ; ; Parm/ ES:DI => Root Directory Table ;******************************************* LoadFAT: pusha ; store registers push es
まず、ロードするセクタの数を知る必要があります。ディスクの構造をもう一度見てください。FAT の数 (および FAT ごとのセクタ数) は OEM パラメーター・ブロックに格納されています。したがって、総セクタ数を得るには、これらを掛け合わせればよいのです。
; compute size of FAT and store in "cx" xor ax, ax mov al, BYTE [bpbNumberOfFATs] ; number of FATs mul word [bpbSectorsPerFAT] ; sectors used by FATs mov cx, ax
さて、FATの前にある予約セクタを考慮する必要があります...
; compute location of FAT and store in "ax" mov ax, word [bpbReservedSectors]
やったー!さて、CXはロードするセクタの数を含んでいるので、セクタをロードするためにルーチンを呼び出します!
; read FAT into memory (Overwrite our bootloader at 0x7c00) push word FAT_SEG pop es xor bx, bx call ReadSectors pop es popa ; restore registers and return ret
これがすべてです。

ファイルの検索

ファイルを検索するには、検索対象のファイル名が必要です。DOSでは8.3命名規則(8バイトのファイル名、3文字の拡張子)に従って11バイトのファイル名を使用することを覚えておいてください。

Root Directory Tableの形式を思い出してください。ファイル名はエントリの最初の11バイトに格納されています。各ディレクトリエントリの形式をもう一度見てみましょう。

  • Bytes 0-7 : DOS File name (Padded with spaces)
  • Bytes 8-10 : DOS File extension (Padded with spaces)
  • Bytes 11 : File attributes. This is a bit pattern:
    • Bit 0 : Read Only
    • Bit 1 : Hidden
    • Bit 2 : System
    • Bit 3 : Volume Label
    • Bit 4 : This is a subdirectory
    • Bit 5 : Archive
    • Bit 6 : Device (Internal use)
    • Bit 6 : Unused
  • Bytes 12 : Unused
  • Bytes 13 : Create time in ms
  • Bytes 14-15 : Created time, using the following format:
    • Bit 0-4 : Seconds (0-29)
    • Bit 5-10 : Minutes (0-59)
    • Bit 11-15 : Hours (0-23)
  • Bytes 16-17 : Created year in the following format:
    • Bit 0-4 : Year (0=1980; 127=2107
    • Bit 5-8 : Month (1=January; 12=December)
    • Bit 9-15 : Hours (0-23)
  • Bytes 18-19 : Last access date (Uses same format as above)
  • Bytes 20-21 : EA Index (Used in OS/2 and NT, dont worry about it)
  • Bytes 22-23 : Last Modified time (See byte 14-15 for format)
  • Bytes 24-25 : Last modified date (See bytes 16-17 for format)
  • Bytes 26-27 : First Cluster
  • Bytes 28-32 : File Size

太字の項目はすべて重要な項目です。各エントリーの最初の11バイトは、ファイル名を含んでいるので、比較しなければなりません。

一致するものが見つかったら、そのエントリのバイト26を参照して、現在のクラスタを取得する必要があります。 これらはすべて、見覚えがあるはずです。

さて...次はコードです。

;******************************************* ; FindFile () ; - Search for filename in root table ; ; parm/ DS:SI => File name ; ret/ AX => File index number in directory table. -1 if error ;******************************************* FindFile: push cx ; store registers push dx push bx mov bx, si ; copy filename for later
まず、現在のレジスタの状態を保存します。SIを使用する必要があるので、現在のファイル名をどこかに保存する必要があります...BXでしょうか?

画像名を見つけるために、Root Directoryテーブルを解析する必要があることを思い出してください。そのためには、ディレクトリテーブルの各エントリの最初の11バイトをチェックして、一致するものがあるかどうかを確認する必要があります。簡単そうでしょう?

これを行うには、エントリの数を知る必要があります。

; browse root directory for binary image mov cx, word [bpbRootEntries] ; load loop counter mov di, ROOT_OFFSET ; locate first root entry cld ; clear direction flag
さて、これでCXには調べるべきエントリの数が格納されました。あとは、11バイト文字のファイル名をループして比較するだけです。文字列命令を使っているので、まず方向フラグがクリアされていることを確認したいのですが、これはcldが行うことです。

DIには、ディレクトリテーブルの現在のオフセットが設定される。つまり、ES:DIはテーブルの開始位置を指しているので、これを解析してみましょう。

.LOOP: push cx mov cx, 11 ; eleven character name. Image name is in SI mov si, bx ; image name is in BX push di rep cmpsb ; test for entry match
11バイトが一致すれば、ファイルが見つかったことになります。DIにはテーブル内のエントリーの位置が含まれているので、すぐに.Foundにジャンプします。

一致しない場合、テーブルの次のエントリを試す必要がある。DIに32バイトを追加します(各エントリが32バイトであることを思い出してください)。

pop di je .Found pop cx add di, 32 ; queue next directory entry loop .LOOP
ファイルが見つからなかった場合、スタックに残っているレジスタのみをリストアし、-1(エラー)を返す。
.NotFound: pop bx ; restore registers and return pop dx pop cx mov ax, -1 ; set error code ret
ファイルが見つかった場合は、すべてのレジスタをリストアする。AXには、ルートディレクトリテーブル内のエントリ位置が格納されているので、読み込むことができる。
.Found: pop ax ; return value into AX contains entry of file pop bx ; restore registers and return pop dx pop cx ret
やったー!これでファイルを見つけることができたので(Root Directory Table内の位置も取得できた)、それをロードしてみましょう。

ファイルのロード

これでようやくすべての設定が終わったので、いよいよファイルをロードする時が来ました!

このほとんどは、他のルーチンを呼び出しているので、非常に簡単です。ここでループし、ファイルのすべてのクラスタがメモリにロードされることを確認します。

;******************************************* ; LoadFile () ; - Load file ; parm/ ES:SI => File to load ; parm/ BX:BP => Buffer to load file to ; ret/ AX => -1 on error, 0 on success ; ret/ CX => Number of sectors loaded ;******************************************* LoadFile: xor ecx, ecx push ecx
ここでは、レジスターを保存するだけです。バッファのコピーをどこかに書き込む必要があるので、それもスタックに保存しています。CXはロードしたセクタの数を記録するために使います。 これは後でスタックに保存します。

ファイルを読み込むには、まずファイルを見つける必要があります(当たり前といえば当たり前ですが ^^)ここでは、FindFileルーチンを簡単に使うことができます。FindFileは、エラー時にはAXに-1をセットし、成功時にはルートディレクトリテーブル内の開始エントリ位置をセットします。このインデックスを使って、ファイルについて知りたいと思ったことを何でも得ることができます。

.FIND_FILE: push bx ; BX=>BP points to buffer to write to; store it for later push bp call FindFile ; find our file. ES:SI contains our filename cmp ax, -1 ; check for error jne .LOAD_IMAGE_PRE ; No error :) Load the FAT pop bp ; Nope :( Restore registers, set error code and return pop bx pop ecx mov ax, -1 ret
さて、ここまでくれば、ファイルが見つかったことになります。ES:DIにはFindFile()で設定された最初のルートエントリの位置が含まれているので、ES:DIを参照することで事実上ファイルのエントリを取得することができるのです。

前節の上のエントリ記述表を見返してください。0x1Aバイトをオフセットして26バイト目(開始クラスタ番号)に到達できることに注目し、それを格納します。

.LOAD_IMAGE_PRE: sub edi, ROOT_OFFSET sub eax, ROOT_OFFSET ; get starting cluster push word ROOT_SEG pop es mov dx, word [es:di + 0x001A]; ES:DI points to file entry in root directory table. mov word [cluster], dx ; Refrence the table for file's first cluster pop bx ; get location to write to so we dont screw up the stack pop es push bx ; store location for later again push es
上記は面倒なことだと思います。FindFileの呼び出しによってAXがエントリ番号に設定されたのを覚えていますか?それをここに保存する必要がありますが、書き込むためのバッファをまだスタックの一番上に保持する必要があります。これが、ここでスタックを少し弄った理由です :)

とにかく、次はFATをロードします。これは信じられないほど簡単です。

call LoadFAT ; Load the FAT to 0x7c00
FATがロードされ、開始ファイルクラスタができたので、次は実際にファイルのセクタを読み込む番です
.LOAD_IMAGE: mov ax, WORD [cluster] ; cluster to read pop es pop bx call ClusterLBA ; convert cluster to LBA xor cx, cx mov cl, BYTE [bpbSectorsPerCluster] ; sectors to read call ReadSectors ; Read in cluster pop ecx ; increment sector count inc ecx push ecx push bx ; save registers for next iteration push es mov ax, FAT_SEG mov es, ax xor bx, bx
このコードはそれほど悪いものではありません。FAT12では、各クラスタはちょうど512バイトであることを覚えていますか? つまり、各クラスタは単に「セクタ」を表します。まず、開始クラスタ/セクタ番号を取得します。しかし、クラスタ番号は線形番号なので、クラスタ番号だけでは大したことはできません。つまり、これはCHSNotLBAフォーマットのセクタ番号で、トラックとヘッドの情報があることが前提です。ReadSectors()はLBAのリニア・セクタ番号を必要とするので、このCHSをLBAアドレスに変換します。そして、クラスタごとのセクタを取得し、それを読み込む!

ESとBXは最初からスタックにプッシュされているので、ポップすることに注意してください。ES:BX はこのルーチンに渡された ES:BP バッファを指しており、セクタをロードするバッファを含んでいます。

さて、クラスタがロードされたので、ファイルの終端に達したかどうかをFATでチェックする必要があります。しかし、FATの各エントリーは12バイトであることを思い出してください。Bootloaders 4で、FATを読むときにあるパターンがあることがわかりました。

偶数クラスタの場合は下位12ビット、上位クラスタの場合は上位12ビットを読み取ります。

詳しくは、ブートローダーズ4をご覧ください。

偶数か奇数かの判断は2で割ればいいだけです。

; compute next cluster mov ax, WORD [cluster] ; identify current cluster mov cx, ax ; copy current cluster mov dx, ax ; copy current cluster shr dx, 0x0001 ; divide by two add cx, dx ; sum for (3/2) mov bx, 0 ; location of FAT in memory add bx, cx ; index into FAT mov dx, WORD [es:bx] ; read two bytes from FAT test ax, 0x0001 jnz .ODD_CLUSTER .EVEN_CLUSTER: and dx, 0000111111111111b ; take low twelve bits jmp .DONE .ODD_CLUSTER: shr dx, 0x0004 ; take high twelve bits .DONE: mov WORD [cluster], dx ; store new cluster cmp dx, 0x0FF0 ; test for end of file marker (0xFF) jb LOAD_IMAGE ; No? Go on to next cluster then DONE: pop es ; restore all registers pop bx pop ecx xor ax, ax ; return success code ret
これで全部です!少し複雑ですが、それほど難しくはないと思います。)

Fat12.inc

素晴らしい!FAT12のコードはすべてFat12.incにあります。

ステージ2の終了

ステージ2に戻る - カーネルのロードと実行

さて、面倒なコードは終わったので、あとはステージ2からカーネル・イメージをメモリにロードして、カーネルを実行するだけです。問題は、「どこで?どこで?

1MBにロードしたいところですが、まだ直接はできません。なぜなら、まだリアル・モードだからです。このため、まず最初にイメージをより低いアドレスにロードする必要があります。保護モードに切り替えた後、カーネルを新しい場所にコピーすることができます。これは1MBでも、ページングが有効であれば3GBでも構いません。

call LoadRoot ; Load root directory table mov ebx, 0 ; BX:BP points to buffer to load to mov ebp, IMAGE_RMODE_BASE mov Esi, ImageName ; our file to load call LoadFile ; load our file MOV dword [ImageSize], ecx ; size of kernel cmp ax, 0 ; Test for success je EnterStage3 ; yep--onto Stage 3! mov si, msgFailure ; Nope--print error call Puts16 mov ah, 0 int 0x16 ; await keypress int 0x19 ; warm boot computer cli ; If we get here, something really went wong hlt
今、私たちのカーネルはIMAGE_RMODE_BASE:0にロードされています。ImageSizeはロードされたセクタ数(カーネルのサイズ)を含んでいます。

プロテクトモード内で実行するために必要なことは、ジャンプするか、それを呼び出すだけです。カーネルを1MBにしたいので、実行する前にまずコピーする必要があります。

bits 32 Stage3: mov ax, DATA_DESC ; set data segments to data selector (0x10) mov ds, ax mov ss, ax mov es, ax mov esp, 90000h ; stack begins from 90000h ; Copy kernel to 1MB (0x10000) CopyImage: mov eax, dword [ImageSize] movzx ebx, word [bpbBytesPerSector] mul ebx mov ebx, 4 div ebx cld mov esi, IMAGE_RMODE_BASE mov edi, IMAGE_PMODE_BASE mov ecx, eax rep movsd ; copy image to its protected mode address call CODE_DESC:IMAGE_PMODE_BASE; execute our kernel!
しかし、ここで少し問題があります。これは、カーネルが純粋なバイナリファイルであると仮定しています。Cはこれをサポートしないので、これを持つことはできません。カーネルはCがサポートするバイナリ形式である必要があり、Cを使ってカーネルをロードするためにそれを解析する必要があります。今のところ、この純粋なバイナリを維持しますが、次の数回のチュートリアルでこれを修正する予定です。かっこいいですか?

まとめ

このチュートリアルは多くの新しいコードをカバーしました。このチュートリアルのコンセプトのほとんどは以前に説明したものなので、このチュートリアルがそれほど難しいものでないことを願っています ;)

しかし、このチュートリアルでは、これらの概念を新しい視点でカバーしました。これは、これらのトピックをもう少し理解するのに役立ち、また、それらが別々のルーチンに実装されるのを見ることができます。

ディスクからセクタをロードするコードを開発し、FAT12を解析して好きな場所にKernelをロードすることができました。クールでしょう?このシリーズでは、カーネルを1MBにロードしています。

基本的なフル 32 ビットカーネルがようやくロードされ、実行できるようになったので、オペ レーティングシステムで最も重要な部分であるカーネルにようやく焦点を当てられるようになりました。

次の数回のチュートリアルでは、カーネルの理論、革命、および設計をカバーします。そして、低レベルCプログラミングと、高レベル言語の概念と理論による低レベルプログラミングをカバーし始めます。

カーネルレベルのC言語プログラミングでは、他のプログラミング分野では許されない多くの自由があります。例えば、「アクセス違反」は存在しないので、メモリ上のすべてのバイトを直接制御することができます。しかし、悪い点もあります。さらに悪いことに、C言語という抽象化されたレイヤーを使って、低レベルの環境をプログラミングしていることも忘れてはいけません。

これから数回のチュートリアルですべてをカバーし、カーネルで動作するようにCをセットアップする予定です。待ち遠しいですね。