起動するまでの長い道のり IPL編(7) カーネル読み込みの巻

 実は、今まで黙っていたが、起動時のIPLはディスク先頭の512バイト=1セクタしか読み込まれない。1セクタだけだからブートセクタなんて言うのだ。
 言うのだ、なんて威張っても仕方ない。OSのカーネルがまさか512バイトに納まるわけはないので、残りの部分をディスクから読み込まなければならない。そのためにはまずカーネルのサイズを知り、そしてディスクからメモリの適切なアドレスへ読み込む必要がある。

カーネルをどこに置くか考える

 リアルモードの制限により、メモリは下位1MBまでしか使えない。しかも、連続してアクセスできるのは64KBまでだ。しかもその1MBの中には、いくつも予約されている領域が存在する。前回に出てきた割り込みハンドラテーブルもその一つだし、BIOSファンクションのコードがある場所やBIOS作業領域、VRAMにマップされている領域などなどがある。それらをかいくぐってカーネルが置ける場所を見つける必要がある。

 起動直後のメモリは以下の様になっている。

開始アドレス(16進) サイズ(バイト) 用途
0x0000_0000 1K 割り込みハンドラテーブル
0x0000_0300 512 BIOS作業領域
0x0000_0500 29K 空き
0x0000_7c00 512 ブートセクタが置かれる位置
0x0000_7e00 607K? 次のACPI作業領域まで空き
0x0009_fc00? 1K? ACPI作業領域? サイズと位置は不定
0x000a_0000 128K VRAM領域
0x000c_0000 128K IO拡張ROM領域
0x000e_0000 128K システムROM領域
0x0010_0000 14M 空き
0x00e0_0000 1M? ISAホール? サイズと位置は不定
0x00fe_0000 128K システムROM領域

 で、リアルモードでアクセスできるのは大雑把に言って0x0010_0000以下の範囲だ。空いていて使えそうなのは0x0000_0500〜0x0009_fc00までの領域だろう。合計すると637KBほど空いている。
 問題になるのは、すでにブートセクタが0x7c00に置かれているという点だ。これより低位にカーネルを読み込もうとすると、実行中のブートセクタを書き潰してしまう可能性がある。別に0x7e00以降に読んでも良いが、何となく勿体無いのでブートセクタ全体をより低位に移動させ、その後にカーネルを読み込ませる。
 扱いやすそうな0x1000にブートセクタを移し、その直後(0x1200)にカーネルを配置することにしよう。

カーネルの長さをどうやって得るか

 カーネルを置く位置は決まったが、果たしてカーネルが何バイトあるか分からない。以前解説したように、ファイルシステムというものにまだアクセスできない。よって、OS上で普通行われるような方法でカーネルのサイズを調べることができない。
 別に適当に空き領域の分までとりあえず読み込んでおいても良いのだが、FDは実機だと遅くてイライラしたりするので、ここはカッコよくカーネルの分だけ読み込んでおきたい。
 カーネルのサイズは、カーネルが出来上がってからでないと分からない。当たり前だ。まだ存在しないもののサイズなんて誰にも分からない。だがサイズが分かる程度まで「出来上がった」と言えるのはいつなのだろう。
 実はコンパイルしてオブジェクトファイルが出来上がった時点でカーネルのサイズは分かる。オブジェクトファイルの情報を見てサイズが分かるからこそ、リンカは複数のオブジェクトコードを正しく配置できるし、別のオブジェクトファイルを参照するシンボルの解決も行える。
 だから、リンク時にカーネルのサイズが得られ、それがプログラムの中から参照できれば良い事になる。そのために、リンカ(GNU ld)のリンカスクリプトというものが使える。
 普通のプログラムを単にリンクするときはあまり意識しないが、プログラムの開始アドレスをどこにしてどんな順にオブジェクトを配置していくか等には色々と決まりごとがある。リンカスクリプトには、そういったコードの配置方法を記述する。また、アドレスを指すシンボルを追加で定義したりデータを埋め込んだりもできる。
 リンカスクリプトを使ってカーネル終端を指すシンボルが定義できれば、プログラムでそのシンボルを参照することでカーネルのサイズが得られるだろう。また、リンカスクリプトでプログラムコードの開始アドレスを設定することもできる。通常はアドレス0x0000が開始アドレスとなるが、実際にブートセクタとカーネルが置かれる0x1000を開始アドレスとすることで、アセンブラ等のソースコード中では開始アドレスを意識しなくても良くなる。
 具体的なリンカスクリプトの内容については次回やる。

BIOSファンクションを使ってディスクからデータを読む

 さて、肝心なディスクからどうやってデータを読むかについて解説する。これにもBIOSファンクションが使える。「int $0x13」(以下int13h)でディスクアクセスのためのBIOSファンクションが実行できる。

 int13hは、実行時のAHレジスタの値によって書き込み・読み込み・状態取得・システム初期化といった細かいファンクションに分かれている。今回はこのうちシステム初期化と読み込みを使う。

システムの初期化(ah=00h)

 AHレジスタに0を設定してint13hを呼び出すと、ディスクシステムが初期化される。レジスタの値や戻り値については下記にまとめた。

レジスタ
AH 0x00を指定する。実行後、エラーがあればエラーコードが設定される
DL ドライブ番号を指定する
EFLAGS 実行後、エラーがあればキャリー・フラグが立つ

 キャリー・フラグとは、加算命令等で桁上がり(桁あふれ)が生じた場合に立つフラグだ。ここではそれがエラーを返すために流用されている。「jc」というジャンプ命令で、キャリー・フラグが立っている場合にだけジャンプが行える。立っていない場合にジャンプを行う「jnc」もある。

セクタの読み取り(ah=02h)

 AHレジスタに0x02を設定した場合はセクタ単位でディスクからのデータ読み取りが行える。レジスタの値は以下の通り。

レジスタ
AH 0x02を指定する。実行後、エラーがあればエラーコードが設定される
DL ドライブ番号を指定する
DH ヘッド番号
CH トラック番号
AL セクタ数。実行後、実際に読み込めたセクタ数となる
ES:BX 読み込み先のアドレス。セグメントを指定して64KB以降の領域にも読み込める
EFLAGS 実行後、エラーがあればキャリー・フラグが立つ

 もう何度か出てきているが、セクタとはディスクから読み込むときのデータの最小単位だ。FDでは1セクタ=512バイトとなる。トラックとは校庭のトラックと同じで、ディスクの1週分のことだ。ヘッドとはディスクを走査する読み書きヘッダで、FDの場合はディスクの裏表の計2つある。
 ディスクからデータを読み込む場合は、ヘッド・トラック・セクタ数をそれぞれ指定しなければならない。複数回に分けて読むのなら、ヘッド・トラックの値を変えながら繰り返し呼び出しを行う。
 表現としては不正確だが、1.44MBのFDでバイト・セクタ・ヘッド・トラックの関係は以下のようになる。

  • 1セクタ=512バイト
  • 1ヘッド=18セクタ
  • 1トラック=2ヘッド
  • 1ディスク=80トラック

 なお、読み込むセクタ数がALレジスタで指定できるが、ディスク上の64KB境界を越えてアクセスを行ったりするとエラーになる。面倒なので私は1セクタずつの読み込みに固定し、上記の関係に従って繰り返し呼び出しを行うようにしてしまった。実機だと凄く遅くなってしまうかもしれない。

色々な壁

 ディスクからの読み込みについて前の節で説明した。注意しなければならないのは、読み込むサイズが大きくなっていくにつれて色々な壁があるということだ。私はほとんど全ての壁にぶつかった(汗)。

512バイトの壁

 最初の1セクタが読み込めて喜んだのもつかの間、次のセクタが読み込めなかったりする。これはint13hの2回目以降の呼び出しが間違っていたりすると起きる。1セクタ分はちゃんと読み込めているので、一見正常に動いているように見える。512バイトを超えた領域を使用するとエラー(暴走)が発生する。

9Kバイトの壁

 これも512バイトの壁と同じで、ヘッドを動かし忘れたり間違ったりすると突き当たる。ヘッダを動かさないでトラックだけを進めてしまったりすると、その通りのディスク位置のデータが読めてしまう。一応データは読み込まれるけれど、順番が無茶苦茶になってしまうのだ。これもかなり後の方でエラーが発生して初めて分かる。(私はプロテクトモードに移行してD言語を使い始めてようやく気付いた)

18Kバイトの壁

 ほぼ同上。トラック番号の動かし方を間違った場合に起きる。

31Kバイトの壁

 これはディスク読み込みの間違いとはちょっと違う。これまで書いてきたIPLのコードではスタックセグメントのベースを0x7c00に設定している。ディスクからの読み込み時にもスタックの位置を変更していなかった場合、読み込みに従ってスタック領域を破壊することになる。スタックが破壊されるということは、関数の戻り先アドレスも破壊される。そして暴走する。IPLの移動にあわせてスタックもカーネル開始位置より低位に移動すること。

64Kバイトの壁

 これもメモリ領域の問題だ。リアルモード特有の64KB制限で、それ以上のサイズのデータを読み込む場合はセグメントレジスタの値を変えなければならない。具体的に言えば、BXレジスタが0xffffを超えて0x0000に戻った場合(オーバーフローした場合)、ESレジスタに0x1000を加算しなければならない。そうすれば、次の呼び出しでは0x10000以降に読み込まれるようになる。これを忘れると0x0000から先をディスクのデータで上書きしたりする。もちろん暴走する

次回予告

 次回は読み込み処理について具体的に解説する。