x86のPCがブートすると、マザーボードの不揮発性メモリ上に格納されたBIOSというプログラムが実行される。BIOSの仕事は、ハードウェアの準備を行い、制御をオペレーティングシステムに渡すことである。仕様としては、BIOSはブートディスク上の先頭の512バイト、ブートセクタに格納されたコードに制御を移す。
このブートセクタには、ブートローダが格納されている。ブートローダはカーネルをメモリにロードする役割を持っている。BISOはブートセクタをメモリアドレス0x7c00にロードし、ジャンプする(このときに、プロセッサの%ip
を設定する)。ブートローダが実行し始めると、プロセッサはIntel8088をシミュレーションしている。よってローダの仕事は、ディスクからメモリに格納されたカーネルをロードするために、プロセッサをより現代の動作モードに変更することであり、さらに制御をカーネルに移す。xv6のブートローダは2つのソースファイルから構成されており、一つは16ビットと32ビットのアセンブリで記述されており(bootasm.S
; 8900行)、
もう一つはC言語で記述されている (``bootmain.c`; 9000行)。
ブートローダの最初の命令は、cli
命令であり(8912行目)、これはプロセッサの割り込みを停止させる命令である。割り込みは、ハードウェアデバイスにとって、割り込みハンドラと呼ばれるオペレーティングシステムの機能を呼ぶための方法である。BIOSは小さなオペレーティングシステムであり、ハードウェアを初期化する中で独自の割り込みハンドラを設定する。しかし、BIOSはそれ以上は動作せず、ブートローダが動作するようになる。従って、ハードウェアデバイスからの割り込みを安全もしくは適切に処理する方法では無い。xv6の準備が整うと(第3章)、再度割り込みが許可される。
プロセッサがリアルモードである時、Intel8088をシミュレートしている。リアルモードでは、8つの16ビットの汎用レジスタが存在しているが、プロセッサは、20ビットのアドレスをメモリに転送する。セグメントレジスタ%cs
,%ds
,%es
,%ss
により、20ビットのメモリアドレスを、16ビットのレジスタから生成される。
プログラムがメモリアドレスを参照すると、プロセッサはその値にセグメントレジスタの値を自動的に16回加算する;これらのレジスタの値は16ビット幅である。どのセグメントレジスタが利用されるかについては暗黙的に決められており、それはメモリの参照の種類によって決まる:命令フェッチには%cs
が使われ、データの読み書きには%ds
が利用され、スタックの読み書きには%ss
が利用される。
xv6はx86の命令がメモリオペランドとして仮想アドレスを利用しているように偽装しているが、x86の命令は実際には「論理アドレス(logical address)(図B-1を参照のこと)」として利用している。論理アドレスはセグメントセレクタとオフセットから構成されており、しばしばsegment:offset
のように表記される。さらに、セグメントは暗黙的であり、プログラムは直接はオフセットしか操作しない。セグメンテーションのハードウェアは「線形なアドレス(linear address)」を生成するために、上記の変換を実行する。ページングハードウェア(第2章を参照のこと)が有効であれば、その線形のアドレスを物理アドレスに変換する;そうでなければ、プロセッサは線形アドレスを物理アドレスとして利用する。
ブートローダはページングハードウェアを有効にはしない; 論理アドレスはセグメンテーションハードウェアにより線形アドレスに変換され、それがそのまま物理アドレスとして利用される。xv6はセグメンテーションハードウェアを、何の変更もせずに物理アドレスとして取り扱うように設定する;従って、全てが同一のアドレスである。私達が、プログラムによって操作されたアドレスを示すときに「仮想アドレス」という言葉を使う歴史的な理由としては、xv6の仮想アドレスはx86の論理アドレスと同一であり、それはセグメンテーションハードウェアによりマップされる線形アドレスと同一である。 一度ページングが有効になると、システムのアドレスマッピングは、線形から物理に変更される。
BIOSは%ds
,%es
,%ss
の値については何の保証もしない。従って、割り込みを設定した後に最初にする仕事は、%ax
をゼロに設定して、それらを%ds
, %es
, %ss
にコピーすることである(8915-8918行目)。
仮想のsegment:offset
のアドレスでは、21ビットの物理アドレスを取り扱うことができるが、Intel 8088では、20ビットのメモリしか扱うことができない。そのため、先頭の0xffff0+0xffff=0x10ffefは破棄される。初期のソフトウェアでは、ハードウェアが21番目のビットを無視することに依存しており、従って、Intelが20ビット以上の物理アドレスを導入したとき、IDBMはPCの互換性のあるハードウェアとしての動作を維持するために、
互換性のハッキングを提供していた。もしキーボードコントローラの出力ポートの2番目のビットが0であるならば、21番目の物理アドレスビットは常に0となる; もし1であれば、21番目のビットは通常通り処理される。
ブートローダは21番目のアドレスビットを、キーボードコントローラの0x64および0x60ポートを制御することによって有効化しなければならなかった。
リアルモードの16ビット汎用レジスタおよびセグメントレジスタは、プログラムが65536バイト以上のメモリを使おうとするときには不便であり、またメガバイト単位のメモリを利用することはできない。x86プロセッサは80286から「プロテクトモード」を導入し、物理アドレスにより多くのビットを利用できるようにした。さらに、(80386からは)32ビットモードを導入し、レジスタ、仮想アドレス、そして殆どの整数算術演算を16ビットから、32ビットで処理できるようにした。xv6のブートシーケンスでは、32ビットモードを有効にするために、プロテクトモードを有効にしている。
プロテクトモードでは、セグメントレジスタは「セグメントデスクリプタテーブル」のインデックスを格納している(図B-2を参照のこと)。各テーブルのエントリは、物理アドレスのベースと、リミットと呼ばれる仮想アドレスの最大値と、セグメントのパーミッションビットが格納されている。これらのパーミッションはプロテクトモード内で保護を行っている: カーネルはこのビットを利用して、プログラムが自分のメモリのみを利用することを保証させることができる。xv6はセグメントを殆ど利用しない; その代わりに、第2章で説明したページングハードウェアを利用する。ブートローダはセグメントデキスクリプタテーブルgdtを設定し(8982-8985行目)、全てのセグメントがベースアドレスがゼロであり、リミットを最大値(4GB)に設定する。 テーブルはエントリが入っておらず、1つのエントリがコード実行のために設定され、もう一つのエントリがデータのために設定されている。コードセグメントディスクリプタはコードが32ビットモードで動作するようにフラグを設定している(0660)。この設定により、ブートローダはプロテクトモードに入り、論理アドレスは物理アドレスに一対一にマップされるようになっている。
ブートローダはlgdt
命令を実行し(8941行目)、プロセッサのグローバルディスクリプタテーブル(GDT)レジスタをgdtdesc
値に設定し、(8987-8989行目)、gdtテーブルを参照するように設定している。
一度GDTレジスタがロードされると、ブートローダは%cr
に1ビット(CR0_PE)に設定し、プロテクトモードを有効にする。プロテクトモードを有効にしても、プロセッサがすぐに論理アドレスから物理アドレスへの変換を行う訳ではない;ある新しい値がセグメントレジスタにロードされ、プロセッサがGDTを読み込むと、内部のセグメンテーションの設定が変更される。直接%cs
を変更することはできず、従って、コードは代わりにセグメントセレクタの設定を行う命令であるljmp
(遠い場所へのジャンプ)を実行する(8953行目)。ジャンプは次の行の命令を実行(8956行目)するが、gdt内でコードディスクリプタエントリを参照するように%cs
を設定する。
このディスクリプタは32ビットのコードをセグメントを設定し、プロセッサは32ビットモードにスイッチする。以上のようにして、ブートローダはプロセッサが8088から80286、80386へと進化するように導いている。
32ビットモードになったブートローダの最初の仕事は、データセグメントレジスタをSEG_KDATAに設定することである(8958-8961行目)。論理アドレスは、物理アドレスに直接マッピングされるようになっている。
Cコードに遷移する前にこれを設定する唯一の方法は、スタックを利用していないメモリの領域にセットアップすることである。0xa0000から0x100000のメモリ領域は典型的にデバイスメモリ領域として汚れているため、xv6のカーネルは0x100000を設定する。ブートローダ自身は0x7c00から0x7d00に存在している。基本的に、多のメモリのセクションはスタックには適している場所である。ブートローダは0x7c00(ファイルには$start
として設定されている)をスタックのトップとして設定する; ブートローダから離れると、スタックはここから下方向に0x0000に向かって伸びていく。
最後に、ブートローダはCの関数であるbootmain
を呼び出す(8968行目)。bootmain
の役割はカーネルをロードし実行することである。何かプログラムに間違いがあれば、単純に戻ってくるだけである。その場合、コードはいくつかの出力ワードをポート0x8a00に出力するだけである(8970-8976行目)。実際のハードウェアでは、このポートには何のデバイスも接続されておらず、何も起こらない。ブートローダがPCシミュレータ上で動作しているときは、0x8a00はシミュレータ自身に接続されており、シミュレータ自身に制御が返されるようになっている。
シミュレータかどうかに関わらず、コードが無限ループに入るようになっている(8977-8978行目)。実際のブートローダは、まず最初にエラーメッセージを出力するようになっている。
ブートローダのC言語の部分、``bootmain.c(9000行目)は、ディスクの2番目のセクタに入っているカーネルのコピーをロードする。第2章で説明したように、カーネルはELFフォーマットのバイナリである。 ELFヘッダにアクセスするために、
bootmain`はELFファイルの最初の4096バイトをロードし(9014行目)する。
これらは、メモリの0x10000番地に配置される。
次のステップは、ELFバイナリの簡単なチェックであり、フォーマットに従ったバイナリであるかをチェックする。bootmain
はディスクのoffバイトから先に配置されているELFヘッダ移行を内容を読み込み、paddrから始まるデータをメモリに書き込んでいく。bootmain
はreadseg
を呼び、ディスクからデータを読み込み(9038行目)、stosb
を呼んでセグメントの残りにゼロを書き込む(9040行目)。stosb
(0492行目)はx86のrep stosb
命令を利用して、メモリ内のブロックを初期化する。
カーネルはコンパイルされ、リンクされるため、仮想アドレスは0x80100000から始まるように配置される。
従って、関数コールの命令は0x801xxxxxとなるようにアドレスを設定する必要がある; kernel.asm
の例を見ることができる。このアドレスはkernel.ld
に設定されている。0x80100000は32ビットのアドレス空間の中では相対的に高いアドレスである; 第2章では、何故この選択をしたかについて説明をしている。このような高いアドレスには、物理的なメモリは存在しない。一度コーネルが実行し始めると、ページングハードウェアが動作して、0x80100000から始まる仮想アドレスを0x00100000にマッピングする;カーネル物理アドレスがこの低いアドレスに配置されていると想定する。しかしブート処理の時点では、ページングは有効化されていない。その代わりに、kernel.ld
はELFのpaddrが0x00100000から始めるように指定しており、ブートローダがカーネルを低いアドレスにコピーし、最終的にページングのハードウェアがそこを指すようになっている。
ブートローダの最後のステップは、カーネルのエントリポイントを呼び出すことである。これにより、カーネルの実行が開始される。xv6のエントリアドレスは0x10000cである:
# objdump -f kernel
利便性のため、_start
シンボルがELFのエントリポイントを示しており、これはentry.S
により規定されている(1036行目)。xv6は仮想メモリをセットアップしないため、xv6のエントリポイントは、entry
の物理アドレスである (1040行目)。