Copyright (C) 2025 Takym.
(この記事は osdev-jp の Discord で筆者が投稿した内容を編集して公開したものです。)
背景
この記事では、ブートローダーの実装手法に関して考察してみます。OS を作り始める時に先ず最初に手を付けなければならない箇所であるからです。なお、この記事におけるブートローダーは Master Boot Record(MBR)に書き込まれるものを指します。Legacy BIOS の API においてディスクからデータを読み取る場合、基本的には Logical Block Addressing 方式(LBA)ではなく Cylinder Head Sector 方式(CHS)でアクセスしなければならないのですが、計算がやや面倒です。アセンブリ言語で書かなければならず、Initial Program Loader(IPL)に利用できる容量が 512 バイト未満になり、開発難易度は高めだと感じます。512 バイト未満と述べましたが、実際には BIOS Parameter Block(BPB)や Partition Table(PT)や Boot Signature 等もあるので更に小さくなります。ブートローダーをディスクの種類や容量に関わらず、汎用的に使う事のできる仕様で開発する場合、セクタ番号を CHS へ変換しながらディスクの全てのデータを IPL で読み込む実装は現実的ではないかもしれません。これは筆者の技術力が足りないからかもしれません。多くの環境で実行できる様にする為には LBA 方式を用いるべきではないでしょう。全ての BIOS が LBA 方式に対応しているとは限りません。
解決策1
そこで、幾つかの解決策を考えました。最初に思い付いた解決策を説明します。BIOS からディスク情報を取得し、ディスクの先頭シリンダのみを読み取る様にすれば良いと考えました。フロッピーディスクの場合は 17 セクタになります。最初のセクタは BIOS が読み取る為、新たに読み取る必要はありません。シリンダ番号とヘッダ番号を固定し、読み取るセクタ数のみを取得したディスク情報に合わせて設定する形にすれば、実装コストを格段に下げられる筈です。ディスク全体の読み取りは、二次ローダー内において BIOS を介さず直接的に読み取る様にします。この解決策では、起動ディスクに複数のパーティションを入れる事は考慮していません。つまり、ハードディスクを使う場合でもパーティションは常に一つであると仮定しています。筆者が自力で開発するとすれば、この様な方式を使うだろうと思います。
動作確認はきちんと行っておりませんが、下記のコードの様なブートローダーを考案しました。BPB や PT は省きました。これをより複雑にしたコードでは動作確認しており、というより、自分で作ったコードから本質的な部分を抜き出し、細部を単純な形に簡略化したものです。
MBR:
BITS 16
ORG 0x7C00
JMP .IPL
NOP
.DATA_DRIVE DB 0x00
.DATA_SZ_CYLN DW 0x0000
.DATA_SZ_HEAD DB 0x00
.DATA_SZ_SECT DB 0x00
.IPL:
CLI
XOR AX, AX
MOV ES, AX
MOV SS, AX
MOV DS, AX
MOV SP, 0x7C00
STI
CLD
MOV [.DATA_DRIVE], DL
MOV DI, AX
MOV AH, 0x08
INT 0x13
JC .FAIL
MOV AL, CL
AND AL, 0x3F
SHR CL, 6
ROR CX, 8
MOV [.DATA_SZ_CYLN], CX
MOV [.DATA_SZ_HEAD], DH
MOV [.DATA_SZ_SECT], AL
MOV AX, 0x07E0
MOV ES, AX
XOR BX, BX
MOV AH, 0x02
MOV AL, [.DATA_SZ_SECT]
DEC AL
MOV CX, 0x02
MOV DH, 0x00
MOV DL, [.DATA_DRIVE]
INT 0x13
JC .FAIL
JMP PL2
.FAIL:
HLT
JMP .FAIL
.BOOT_SIGN:
TIMES 0x01FE - ($ - $$) DB 0x00
DW 0xAA55
PL2:
; ...
解決策2
「解決策1」を書いていて気が付いたのですが、筆者が特に難しいと感じたのは最初のセクタを除外して計算する事だろうと思いました。先頭シリンダとそれ以外のシリンダとで読み取りプログラムを切り替えれば、複雑な実装にする必要は無い事に気が付きました。それでも、うろ覚えですが、Legacy BIOS の API ではディスク全体を読み取れなかった気がしますが。従って、ディスク情報と一般的な BIOS の読み取り制限を比較して読み取り可能な最大容量を割り出す必要があるかもしれません。
解決策3
独自のプログラミング言語を作って IPL を実装できる機能を入れるのも選択肢の一つかもしれません。特に INT
命令の呼び出しや特定のレジスタへの値の設定を、その言語の文法上で自然に記述できる様に作り込みます。しかし、この解決策では、寧ろ実装コストは膨らんでしまい、開発難易度を引き下げるという当初の目的は果たせません。
解決策4
ハードディスク向けの IPL とフロッピーディスク向けの IPL を別けて開発しても良いかもしれません。ディスクの種類や容量に応じて各引数を固定した方が作り易いかもしれません。この方法では、数多のディスクに対応するのが困難になってしまいますが。
解決策5
自分自身で開発する事を断念するのであれば、「30日でできる! OS自作入門」や「作って理解するOS x86系コンピュータを動かす理論と実装」や MS-DOS のブートローダーを流用しても良いかもしれません。MS-DOS のブートローダーはオープンソース化され、そのソースコードは GitHub のリポジトリで閲覧する事ができます。既存のブートローダーを流用する場合、権利関係を考慮してオープンソースである事が望ましいです。車輪の再発明を避けて開発時間を短縮するという考えの下では、最も合理的な手段となるでしょう。筆者は自力開発に挑戦してみたいとも思っておりますが。
解決策6
そもそも現代では Legacy BIOS ではなく UEFI BIOS を使うべきだとも思います。しかし、UEFI にするとファイルシステムが FAT に固定されてしまうという問題があります。独自のディスク形式(ファイルシステム)を用いる場合は、その為だけに余分なパーティションを作る必要があります。ディスク容量を最大限有効活用するには、Legacy BIOS の方が適していると思いました。若しくは、独自のディスク形式の使用を諦める必要があります。
以上になります。最後までお読みいただきありがとうございました。