Operating Systems Development Series
Bootloaders
by Mike, 2008, 2009

このシリーズは、オペレーティングシステムの開発を一から実演して教えることを目的としています。

ブートプロセス

電源ボタンを押す

実際に電源ボタンを押すとどうなるのでしょうか?このボタンが押されると、ボタンに接続された配線がマザーボードに電子信号を送ります。 マザーボードはこの信号を電源 (PSU) に再ルートするだけです。

この信号には、1ビットのデータが含まれています。それが0であれば、もちろん電源はありません(つまり、コンピューターの電源が切れているか、マザーボードが死んでいる)。1であれば(アクティブな信号を意味する)、電力が供給されていることを意味します。

このことをよりよく理解するために、コンピュータにおける2進法の基本を思い出してください。8つの「ビット」は、簡単に言えば、電気が通る8本の「線」または「ライン」を表します。0」は電流が流れていないことを表し、「1」はその線に電流が流れていることを表す。これが、コンピュータの原点である「デジタル論理回路」の基礎となり、「ロジックゲート」とともに、コンピュータを構成している。

PSUはこのアクティブな信号を受け取ると、システムの残りの部分に電力を供給し始めます。 すべてのデバイスに適切な量の電力が供給されていれば、PSUは大きな問題なく電力を供給し続けることができるのです。

その後、PSUは「power_good」信号と呼ばれる信号をマザーボードに送信し、BIOS(Basic Input Output System)に送信します。

BIOSのPOST

BIOSがこの「power_good」信号を受信すると、BIOSはPOST(Power On Self Test)と呼ばれるプロセスの初期化を開始します。POSTでは、十分な電力が供給されていること、取り付けられているデバイス(キーボード、マウス、USB、シリアルポートなど)、メモリが正常であることを確認します(メモリの破損をテストすることで)。

その後、POSTはBIOSに制御を委ねる。POSTはBIOSをメモリの終端(0xFFFFF0かもしれない)にロードし、メモリの最初のバイトにジャンプ命令を置く。

プロセッサの命令ポインタ(CS:IP)は0に設定され、プロセッサが制御するようになります。

これは何を意味するのでしょうか?プロセッサはアドレス0x0から命令を実行し始めます。この場合、POSTによって置かれたジャンプ命令です。このジャンプ命令は0xFFFFF0(またはBIOSがロードされた場所)にジャンプし、プロセッサーはBIOSの実行を開始します。

BIOSが制御する...

BIOS

基本入出力システム(BIOS)は、いくつかのことを行います。割り込みベクターテーブル(IVT)を作成し、いくつかの基本的な割り込みサービスを提供します。その後、BIOSはハードウェアに問題がないことを確認するために、さらにいくつかのテストを行います。BIOSはまた、セットアップユーティリティを提供します。

BIOSは次にOSを探す必要があります。BIOSセットアップで設定した起動順序に基づき、BIOSは割り込み(INT)0x19を実行して起動可能なデバイスを探そうとします。

起動可能なデバイスが見つからない場合(INT 0x19が戻る)、BIOSは起動順序に記載されている次のデバイスに進みます。これ以上デバイスがない場合、"No Operating System found "と同様のエラーを出力し、システムを停止させます。

割り込みと割り込みベクターテーブル(IVT)

割り込みは、多くの異なるプログラムから実行できるサブルーチンです。 これらの割り込みは、割り込みベクターテーブルと呼ばれるテーブルにアドレス0x0で格納されます。 例えば、DOSで使用されるINT 0x21は、一般的な割り込みです。

注意:DOSはありません。利用可能な割り込みは、BIOSによって設定された割り込みのみであり、それ以上はありません!他の割り込みを使用すると、システムが存在しないルーチンを実行することになり、プログラムがクラッシュする原因になります。

注意:プロセッサのモードを切り替えた場合、IVTは使用できなくなります。つまり、ソフトもハードも、BIOSも、全く割り込みが効かなくなります。32ビットOSでは、これをやらなければならないでしょう。まだ、そうではないですが。

BIOSインタラプト0x19

INT 0x19 - SYSTEM: BOOTSTRAP LOADER (ブートストラップローダー)

メモリーのクリアや割り込みベクターテーブル(IVT)の復元を行わず、Warm Rebootによりシステムを再起動します。

この割り込みはBIOSによって実行されます。最初のハードディスクの最初のセクタ(Sector 1, Head 0, Track 0)を読み出します。

セクタ

セクタとは、簡単に言うと512バイトのまとまりを表します。つまり、セクタ1はディスクの最初の512バイトを表しています。

ヘッド

ヘッド(またはフェイス)は、ディスクの側面を表します。ほとんどのディスクは1面しかないため、ヘッドも1つしかありません(「ヘッド1」)。

トラック

トラックを理解するために、絵を見てみましょう。

この写真では、このディスクはハードディスクまたはフロッピーディスクを表しています。ここでは、ヘッド1(前面)を見ており、セクタは512バイトを表しています。トラックはセクタの集合体です。

注意:1セクタは512バイトで、フロッピーディスクでは1トラックあたり18セクタあることを覚えておいてください。 これはファイルを読み込むときに重要なことでしょう。

ディスクがブータブルであれば、ブートセクタが0x7C00にロードされ、INT 0x19がそこにジャンプし、ブートローダに制御を委ねます。

注:ブートローダは0x7C00にロードされることを忘れないでください。これは重要です。

注意: システムによっては、0x1234 をアドレス 0x0040:0072 に置き、0xFFFF:0 にジャンプすることで、ウォームブートを実行することもできます。コールドリブートの場合は、代わりに0x0を格納してください。

これで、1337 ブートローダが制御できるようになりました!

ブートローダの理論

これまでブートローダについていろいろと説明してきました。重要な部分をまとめてみましょうか。

これまでのところ、ブートローダは...

  • マスターブートレコード(MBR)と共に保存される。
  • ディスクの最初のセクタにある。
  • ...1セクタ(512)バイトの大きさである。
  • ...BIOSのINT 0x19でアドレス0x7C00にロードされる。

想像できるように、512バイトでは多くのことはできません。ではどうするか?

アセンブリ言語では、512バイトの枠を超えることは非常に簡単です。つまり、見た目には問題ないのですが、そのコードの一部しかメモリ上に存在しないことになります。 例えば、次のように考えてみてください。

mov ax, 4ch inc bx ; 512 byte mov [var], bx ; 514 byte
アセンブリ言語では、ファイルの先頭から下に向かって実行されます。 しかし、ファイルをメモリにロードするときは、セクタをロードしていることを覚えておいてください。 セクタはそれぞれ512バイトなので、ファイルの512バイトだけがメモリにコピーされます。

上記のコードを実行し、最初のセクタだけをメモリにロードした場合、512バイトまでしかコピーされません(inc bx命令)。つまり、最後のmov命令はまだディスク上にありますが、メモリ上にはないのです!

では、inc bxの後、プロセッサは何をするのでしょうか?514バイトに進みます。これはメモリ上にないため、ファイルの終端を越えて実行されます。最終結果は?クラッシュです。

しかし、指定されたアドレスの2番目のセクタ(またはそれ以上)をロードして実行することは可能です。 そうすれば、ファイルの残りはメモリ内にあり、すべてがうまく動作します。

このアプローチはうまくいきますが、ハードハッキングされます。最も一般的な方法は、ブートローダのサイズを512バイトに保ち、セカンドステージのブートローダを検索、ロード、実行する方法です。 これについては、後で詳しく説明します。

ハードウェアの例外

ハードウェア例外はソフトウェア例外と同じですが、プロセッサがソフトウェアではなくハードウェア例外を実行します。

例外の発生をすべて停止しなければならない場合があります。例えば、コンピュータのモードを切り替えるとき、割り込みベクタテーブル全体が利用できないので、ハードウェアでもソフトウェアでも割り込みがあると、システムがクラッシュしてしまいます。これについては後で詳しく説明します。

CLIとSTIの命令

STI命令やCLI命令を使って、すべての割り込みを有効にしたり無効にしたりすることができます。 ほとんどのシステムは、大きな問題を引き起こす可能性があるため、アプリケーションに対してこれらの命令を許可していません(システムはこれらをエミュレートすることができますが)。
cli ; clear interrupts ; do something... sti ; enable interrupts--we're in the clear!

ダブルフォールトハードウェア例外

プロセッサが実行中に問題(無効な命令、0による除算など)を発見した場合、2番目の故障例外ハンドラ(ダブルフォールト)を実行し、これが割り込み0x8となります。

ダブルフォールトについては後述します。ダブルフォールトの後、プロセッサがまだ継続できない場合、トリプルフォールトが実行されます。

トリプルフォルト

この言葉、どこかで見たことありますよね?CPUが「Triple Faults」を起こすと、システムがハードリブートすることを意味します。

ブートローダのような初期段階では、コードにバグがあると、システムがトリプルフォルトを起こします。 これはコードに問題があることを示します。

簡単なブートローダを開発する

Yiippee!*待ちに待った瞬間だ!:)

私たちのリストをもう一度見てみましょう。

  • マスターブートレコード(MBR)と共に保存される。
  • ディスクの最初のセクタにある。
  • 1セクタ(512)バイトの大きさです。
  • アドレス0x7C00でBIOS INT 0x19によってロードされます。
普通のテキストエディタ(私はVisual Studio 2005を使用しています)を開きますが、メモ帳で十分です。

ブートローダ(Boot1.asm)は...

;********************************************* ; Boot1.asm ; - A Simple Bootloader ; ; Operating Systems Development Tutorial ;********************************************* org 0x7c00 ; We are loaded by BIOS at 0x7C00 bits 16 ; We are still in 16 bit Real Mode Start: cli ; Clear all Interrupts hlt ; halt the system times 510 - ($-$$) db 0 ; We have to be 512 bytes. Clear the rest of the bytes with 0 dw 0xAA55 ; Boot Signiture
この中には、あまり驚くようなことはないでしょう。一行ずつ解析してみましょう。
org 0x7c00 ; We are loaded by BIOS at 0x7C00
覚えておいてください。BIOSは0x7C00で私たちをロードします。上のコードはNASMに、すべてのアドレスが0x7C00に戻るように指示しています。つまり、最初の命令は0x7C00になります。 チュートリアル2を覚えていますか?あのチュートリアルでは、x86ファミリーがいかに古いDOSシステムと後方互換性を持っているかを説明しました。古いDOSシステムは16ビットでしたから、x86互換のコンピュータはすべて16ビットモードで起動します。これはつまり
  • メモリは1MB(+64k)に制限されています。
コンピュータを32ビットモードに切り替える必要があります。この作業は後ほど行います。
times 510 - ($-$$) db 0 ; We have to be 512 bytes. Clear the rest of the bytes with 0
私はこれがより文書化されていることを望みます。NASMでは、ドル演算子($)は現在行のアドレスを表します。つまり、$-$は現在行から先頭命令までのバイト数(この場合はプログラムのサイズ)を返します。
dw 0xAA55 ; Boot Signiture
これには少し説明が必要です。

BIOSのINT 0x19はブート可能なディスクを検索することを思い出してください。ディスクが起動可能かどうか、どうやって判断しているのでしょうか?ブートシグニチャです。511バイトが0xAAで512バイトが0x55なら、INT 0x19はブートローダをロードして実行します。

ブートシグニチャはブートセクタの最後の2バイトでなければならないので、512バイト目ではなく、510バイト目まで埋めるために異なるサイズを計算するためにtimesキーワードを使用します。

NASMでのアセンブル

NASMはコマンドラインアセンブラなので、コマンドラインかバッチスクリプトで実行する必要があります。Boot1.asmをアセンブルするには、次のようにします。
nasm -f bin Boot1.asm -o Boot1.bin
fオプションはNASMにどのような出力を生成するかを指示するために使います。この場合、それはバイナリプログラムです。

-oオプションは、生成されたファイルに別の出力名をつけるために使います。この場合、Boot1.binとなります。

アセンブル後、"Boot1.bin "という名前の正確な512バイトのファイルができているはずです。