どうすればIPL(ブートローダー)を作ることができるでしょう?
BIOSは初期化やチェックを行った後、OS(とは限らないが)を起動すべく探しに行きます。
BIOSの設定次第ではありますが、まずはFDのブートセクターの読み込みを試みます。
FDのブートセクターにプログラムが書かれていれば、それを読み込んで実行します。
具体的な流れをすこし見てみましょう。
IPLの読み込み 
BIOSは初期化やチェックを終えると、FDのブートセクタ(セクター0)部分を読み込みます。 読み込まれたデータは、メモリの0x0000:0x7c00に転送され、読み込みが終わるとこの0x0000:0x7c00にジャンプし、実行を開始します。( OS-Wiki参照)
通常、ブートセクタのサイズは512KBしかありません。IPLもその中に収めなくてはなりませんので、普通に「ファイルxxx.xxxを読み込め!」ということはできません。
(普通、「ファイルxxx.xxxを読め」等の動作が出来るのはDOSやLinux等のOSの機能です。まだIPLではOSもなんにもありません。そもそも「ファイル」という概念すら使うことが出来ないのです。この時点で使える方法はBIOSに収められている関数を呼び出したり直接機械語でFDを動かしたりして、セクター単位で読み込むことしかできません。)
(ちゃんと調査していないので間違っているかもしれませんが、MS-DOSなどにこの実例があります。MS-DOSのカーネル(OS本体)は、IO.SYSとMSDOS.SYSというファイルです。当然DOSのIPLもこの二つを読み出します。しかし、実際にはこの二つはファイルであってファイルではありません。この二つのファイルがあるセクタを直接読み出しているのです。そのため、MS-DOSの起動ディスクでIO.SYS等をコピーしたり上書きしたりすると、たとえ内容が同じでも起動ができなくなってしまいます。実際にファイルの内容が書かれているセクターがずれてしまうためです。)
FAT12 
通常、もっとも簡単にできるFDのフォーマットはFAT12です。 (Windowsから普通にフォーマットすれは自動的にそうなる)
FAT12でのIPLはどうなっているでしょう。
K氏のheboosのIPLではこうなっていました。
JMP boot NOP DB 'heboOS ' DW 512 DB 1 DW 1 DB 2 DW 00e0H DW 2880 DB 0f0H DW 9 DW 18 DW 2 DD 0 DD 2880 DB 0,0 DB 29H DD 0ffffffffH DB 'heboOS ' DB 'FAT12 '
これをOS-WikiのIPLのページと照らし合わせるとこうなります。
JMP boot | ;jmp命令用の空き | |
NOP | 以下の「名前」の先頭は3バイト目からと決まっているらしい。JMP命令は2バイトにしかならないため、1バイト稼いでおく? | |
DB | 'heboOS ' | ;名前(たいていはファイルシステムの名前か、もしくはOSの名前が入る。ATでBIOSを使ってこの部分を読みこむと、たいていごみデータに置換される。) |
DW | 512 | ;セクタサイズ(一般的には512か1024) |
DB | 1 | ;クラスタサイズ(1クラスタが何セクタで構成されているか) |
DW | 1 | ;FATがどこから格納されているか(このBPBセクタからの相対セクタ番号で)~(別の言い方をすると、「予約セクタ」がいくつあるかを表します) |
DB | 2 | ;FATがいくつあるか(一般的には2) |
DW | 00e0H | ;ルートディレクトリの長さ(32バイト単位) |
DW | 2880 | ;このドライブ(FDの場合はディスク、HDDの場合はパーティション)の総サイズ(セクタ数で) |
DB | 0f0H | ;メディアのタイプ(一般に1440KB-FDの場合は0xf0を、HDDの場合は0xf8を書いておけば問題はなさそう) |
DW | 9 | ;FATの長さ(1つ分の長さをセクタ数で) |
DW | 18 | ;1トラックにいくつのセクタがあるか(一般に1440KB-FDの場合は18) |
DW | 2 | ;ヘッドの数はいくつか(一般に1440KB-FDの場合は2) |
DD | 0 | ;このBPBセクタはデバイスの先頭から数えてどこにあるか(HDDのパーティション開始位置を表すために用いられる、セクタ単位、一般に1440KB-FDの場合は0) |
DD | 2880 | ;セクターが全部で何個あるか |
DB | 0,0 | ;よくわからないけどとりあえず0にしておくといいらしい |
DB | 29H | ;よくわからないけどとりあえず0x29を書くらしい |
DD | 0ffffffffH | ;0xffffffffを書いておくといいらしい(たぶんボリュームシリアル番号) |
DB | 'heboOS ' | ;ディスクの名前、もしくはパーティションの名前、ボリュームラベルのコピー |
DB | 'FAT12 ' | ;"FAT12"(46 41 54 31 32 20 20 20)か、"FAT16"(46 41 54 31 36 20 20 20)が入る |
これらの後に実際に動作するコードを書き込むようです。(つまりJMP BOOT の飛び先)
ブートローダーとは? 
MS-DOSのような小さなOS、あるいはほとんど変更がされないOSの場合は、このIPL(たった512バイト)のプログラムでも直接読み込むことができます。
しかし、たとえばLinux等のように、頻繁にアップデートが行われるようなものの場合はこの仕組みでは大変です。可能ならファイルとして読み込みを行いたいですね。
しかし、ファイルを扱えるようにするにはOSと同等のファイルシステム管理プログラムが必要になります。これをIPLに収めるのはなかなか大変です。
そこでLinux等の場合は、IPLで一旦ブートローダーという小さなOSモドキをロードして実行させます。この段階では少なくとも512バイトという制限はありませんから、それなりに大きくできます。
このブートローダーという小さなOSにファイルシステムと同じようなプログラムを搭載させ、OS本体はファイルとして読み込むようにしておけば、OS本体そのものは単に特定のディレクトリに決まった名前でコピーさえすれば読み込めるようになるので、アップデートが楽になるという寸法です。
ですから、厳密にはIPLとブートローダーは別のものです。MS-DOSやOSASKなどのように小さい、あるいはイメージディスクでの配布が前提のものなどは、ブートローダーはいらないorIPLと兼用ということになります。
どのセクタを読む? 
先のIPLの先頭にはジャンプ命令があります。ここからジャンプした先にセクタを読む命令を書いておけばそれを読んでくれますが、どのセクタを読めばいいでしょう。
FDのセクタは予約されたものがあるのでこれ以降のデータ領域にOSを書き込み、それを読み込むことになります。FDのFAT構造はこうなります。
IPL | 予約領域 | FAT領域 | ルートディレクトリ領域 | データ領域 |
この各領域はおのおのどのくらいあるのでしょう?
本来は、IPL以外は可変できるような構想だったようです。逆に言えば、IPL以外の領域は、先のBPBの値から計算しなければいけないものということになります。
ただ、こと「WindowsやDOSで作ったFAT12のFD」に関しては、事実上決め打ちと考えていいようです。念のため、計算法をご照会。
- IPL
- これはセクター0と決め打ちされています。大きさも1セクタ(512バイト)です。
- 予約領域
- おそらくなにか特別な用途に使うために用意されたものでしょう。ファイルとしては絶対にアクセスできない領域になるようです。算出は、「FATがどこから格納されているか」の数字で求めます。IPLはセクター0。で、この数字を例えば10にすると、FAT領域はセクター10からとなります。そうすると必然的に、セクター1〜セクター9が予約領域となり、通常は使うことができない特殊な領域となります。
- 上記の例では「1」ですね。つまり、FAT領域はセクター0の次から(セクター1から)ということになり、事実上予約領域は0となります。
- FAT領域
- ファイルの情報(データではない)が格納されます。ここは、「FATの長さ(1つ分の長さをセクタ数で)」の値から解ります。9です。つまり、上記のスタート地点(セクター1)から9個分のセクターを使うということになります。なので、「セクター1〜セクター9」ということになります。
- さらに、「FATがいくつあるか」という情報も参照します。2ですね。もう一個は予備用らしいですが、ちゃんと領域を喰います。二つ目の予備FATも同じく9個なので、「セクター10〜セクター18」となります。
- 結果、FAT領域は、「セクター1〜セクター18」となります。
- ルートディレクトリ領域
- 文字通り、ルートディレクトリにあるファイルの情報を格納するところです。
- FAT12では、ルートディレクトリにはBPBで決められています。「ルートディレクトリの長さ」の値です。00e0h、十進数に直すと224です。つまり、ルートディレクトリには、0〜224番までの225個しかファイルを置くことができません。逆に言えば、この領域はあらかじめ225個分のファイルのデータを格納できなくてはいけません。ファイル一個に使用するデータは32バイトです。つまり 32*225=7200バイト。7200バイトは何セクタで格納できるでしょう?。 7200÷512=14.0625。つまり、14セクターあれば全部格納できます。(端数がなぜ切り捨てになるのかちょっと解りません。調査中・・・)
- 上記の例で、FAT領域は「セクター1〜セクター18」なので、ルートディレクトリ領域は「セクター19〜セクター32」となります。
- データ領域
- 実際にファイルの内容がバラバラに分解されて書き込まれる領域です。スタート位置は上記から必然的に、「セクター33から」となります。
- では、いくつまであるのでしょう。「このドライブ(FDの場合はディスク、HDDの場合はパーティション)の総サイズ(セクタ数で)」という所に総数が書かれています。「2880」ですね。なので、データ領域は、「セクター34〜セクター2879」ということになります。
- ※セクターは0から始まりますので、2880個だと、最後の番号は2879になります。
つまり、こうなります。
IPL | 予約領域 | FAT領域 | ルートディレクトリ領域 | データ領域 |
0 | -- | 1〜18 | 19〜32 | 33〜2879 |
こう考えると、例えばカーネル(OS本体)やブートローダーを起動時に読み込ませるにはこういう手順になります。
- カーネルやブートローダの大きさをとりあえず決める(例えば64KBまでとします。)
- 64KBとはすなわち、1024*64=65536バイト。512で割れば、65536÷512=128。すなわち128個のセクターが必要となります。
- データ領域はセクター33からですから、「セクター33〜セクター161」までにカーネルやブートローダーを書き込みます。
- FAT領域とルートディレクトリ領域に、「「セクター33〜セクター161」はxxx.xxxというファイルで使われている」という情報を書き込みます。
- IPLのBPB以降の位置に、「「セクター33〜セクター161」をメモリのある領域に読み込み、実行しろ」という命令を書き込みます。
こういう流れになるはずです。
論理セクタと物理セクタ 
上記のFAT構造などで、「セクター0」とか「セクター161」とか書いてますね。でも、実はこれは「論理セクター」といい、単なる概念なのです。
IPLの段階では、各セクターをBIOSの命令などで読み出すことになりますが、これらの命令では、「セクター140を読め」という指定はできません。もっと物理的。すなわち、実際にFDの状態に合わせた指定が必要になります。
実際(物理的)のFDは、まず丸く仕切られています。ちょうど、木の年輪・あるいは輪切りにしたたまねぎのような状態です。この輪を「シリンダ」といいます。(本当はシリンダとはトラックの集まりのことですが、FDの場合はとりあえずシリンダ=トラックでいいでしょう。)
シリンダは内側から「シリンダ0、シリンダ1・・・・シリンダ79」という番号が振られており、0〜79まで。すなわち80本あります。
シリンダはさらに孤の状態に仕切られています。輪切りにしたたまねぎを、さらにケーキを切るように切っていったと考えてください。こうして切られた1つひとつを「セクタ」と呼びます。
※シリンダは0から数えますが、なぜかセクタは1から数えます。
セクタは1〜18まで。つまり、1つの輪(シリンダ)が18個のセクタに仕切られます。
さらに、通常のFDは、これが裏表にあります。
なので、通常のFDには、80(シリンダ) × 18(セクタ) × 2(裏表)= 2880。2,880個のセクタがあるというわけです。
・・・カンのいい人はもうおかしいと思いますね。
「セクター161ってのは、じゃどこのことなんだよ?」と。
これまで書いてきた「セクター0」とか「セクター161」という書き方は、「論理セクタ」といい、話を簡単にするための概念なのです。
で、上記の説明が実際のFD上での状態。すなわち、「物理セクタ」です。
「論理セクタ」で言うと、セクターは0〜2879まで。
しかし、「物理セクタ」は、最大で18番までしかありません。また、BIOSなどで実際に指定する場合は、この「物理セクタ」で指定してやらないといけないのです。
論理セクター0を物理セクタに換算する 
論理セクター0。(IPLですね)これを、物理セクタに換算すると、こうなります。
シリンダ | 0 | (一番内側) |
セクタ | 1 | (詳しいことは省略。FDはちゃんとスタート位置があります。) |
ヘッド | 0 | (表面のこと) |
では、論理セクター1はっていうと、
シリンダ | 0 | (一番内側) |
セクタ | 2 | (詳しいことは省略。FDはちゃんとスタート位置があります。) |
ヘッド | 0 | (表面のこと) |
論理セクター2は
シリンダ | 0 | (一番内側) |
セクタ | 3 | (詳しいことは省略。FDはちゃんとスタート位置があります。) |
ヘッド | 0 | (表面のこと) |
と、こう続いていきます。
で、そうなると、論理セクター17は
シリンダ | 0 | (一番内側) |
セクタ | 18 | (詳しいことは省略。FDはちゃんとスタート位置があります。) |
ヘッド | 0 | (表面のこと) |
となります。・・・じゃ、論理セクター18は?っていうと、
シリンダ | 0 | (一番内側) |
セクタ | 1 | (詳しいことは省略。FDはちゃんとスタート位置があります。) |
ヘッド | 1 | (裏面のこと) |
と、こう続いていくわけです。もうお解りですね? (^^)
同じくセクタ34はどこかっていうと、
シリンダ | 0 | (一番内側) |
セクタ | 18 | (詳しいことは省略。FDはちゃんとスタート位置があります。) |
ヘッド | 1 | (裏面のこと) |
じゃ、セクタ35は? というと・・・・
シリンダ | 1 | (一番内側から2番目) |
セクタ | 1 | (詳しいことは省略。FDはちゃんとスタート位置があります。) |
ヘッド | 0 | (表面のこと) |
とこう続いていくわけです。
コードを考えてみる 
OS-WikiのBIOS一覧で調べてみると、ディスクを読むためのBIOSコールは「INT 13H」であることが解る。すこし具体的にコードを考えてみよう。
これまでの考察や調査(パクリとも言う・・・ :P)を元に、以下を仮定してみる。
- OS本体を、とりあえず最大64KBと仮定しておく
- FAT上でも仮ファイルとして成り立つようにしておきたい。
- FAT上でファイルとして成り立たせるためには当然FAT領域やルートディレクトリ領域にも情報を書かなくてはならないが、それはちょっとあとで・・・
- FAT上でファイルとして成り立たせるためには、当然「データ領域」にデータを置かなくてはいけない。データ領域の開始位置は、論理セクタで33番から。OSが64KBなら、161番までとなる。つまり、BIOSで論理セクタ33〜161までを連続で読み込み、メモリに格納すればいい。
- ただし、BIOSコールの詳細を見れば解るが、実際の指定は「物理セクタ」で行わなくてはいけない。
- 実際には読み込んだデータを格納するメモリのアドレスが必要だが、これはちょっと一旦はしょっておく・・・(^^;)
そんなわけで、まず論理セクタ33を読み込んでみる。論理セクタ33は、シリンダ2・セクタ14・ヘッド0となるので、こういうコードになるはず。
MOV AH,02H ;モード指定。(読み込みを指定) MOV AL,01H ;連続処理するセクタ数(とりあえず1) MOV CH,01H ;シリンダ番号(シリンダ1を指定) MOV CL,14 ;セクタ番号(セクター14を指定) MOV DH,0 ;ヘッド番号(0すなわち表を指定) MOV DL,0 ;ドライブ番号(0すなわちAドライブを指定) MOV ES,xxx ;データを読み込むアドレスのセグメント値 MOV BX,xxx ;データを読み込むアドレスのオフセット値 INT 13H
・・・あとはこれを繰り返していけばいいんだけど、すぐに思いつくよね。
めんどくせーーー!!!
これを、ちょうどベーシックのFor〜Nextみたいにできないものだろうか・・・
ここで、論理セクタから自動的に物理セクタに変換して、かつ、とりあえず連番なら自動で読み込んでくれるような式を試案してみる。
;AXレジスタに開始番号、DXレジスタに終了番号を入れてコールすると読んでくれる ;なんてできないものかな・・・ readsec1: PUSH AX ;AXの値を一旦バックアップ SUB AX,DX ;まず引き算して、AXに「個数」を入れる MOV CX,AX ;CXレジスタに個数を入れる。(LOOP用のカウンタ) POP AX ;リストア ;ヘッダの値を計算する PUSH AX ;AXの値を一旦バックアップ DIV AX,2 ;値を半分に割る。 SUB AX,1439 ;片面のセクタ数を引き算する。 ;・・・あれぇ。引き算の場合、マイナスになるような場合って・・・
あかん・・・だめだ。ちょっと整理しよう。
- 読み込みたいセクターの位置指定を求める計算式
- ヘッド
- 読みたい論理セクターの値 ÷ (総セクター数÷2)
- シリンダ
- 読みたい論理セクター値 ÷ セクター数(18)
- セクター値
- 読みたい論理セクター値 mod セクター数(18)
- ヘッド
えーっと・・・これでいいかな? ちょっと検算中・・・
あークソ!だめだ。ちょっと別に式を組み立てないとわからんわい!(あったまわる・・・)
えーっと。逆に、論理セクタを計算する式を考えてみる。
- 論理セクタ : R
- シリンダ :c
- セクタ :s
- ヘッド :h
だとすると・・・こうかな??
R = c × 18 + s +(2880/2*h) -1
おぉぉ! これでいいらしいぞ!!! (^^;)
では、この式を展開して・・・・あ”だめだ。ちょっとこれじゃ解りにくいな。ちと変更する。
- 論理セクタ : R
- シリンダ :c
- セクタ :s
- ヘッド :h
- シリンダ内のセクター数:A
- セクター総数:B
R = cA + s +(B/2*h) -1
こうだ。これを展開。(なつかすいなぁ。こんなの中学以来かも・・・(^^;))
h = (R-cA-s+1)÷B÷2 s = R-cA+1-(B÷2h) c = (R-s-(B÷2h)+1)÷A
よーし!これで求められるぞ! ばんざーい!ばんざーい!
・・・・ダメじゃん・・・・ orz
あー。むずかしく考えすぎたかもしれん。こうかな???
;まず、hを求める h = R ÷ (B ÷ 2) (余りは破棄)
・・・うん。これでよさそうだ。 で、これでhが求められると。
;セクタも求める s = R mod A + 1
・・・おぉ? これでいいのかな???
そうするってーと、シリンダはこうかな?
;シリンダを求める Q = B ÷ A ÷ 2 ;先に一面のシリンダ数を求める c = R ÷ A - (Q * h)
うん!これでよさそうだぞ!
では、これらを実際のアセンブラにするってーと・・・・
まずはh。
;h = R ÷ (B ÷ 2) (余りは破棄)だから、 PUSH AX ;AXには論理セクタ値が入っている。一旦バックアップ。 MOV AX,2880 ;まず、AXに総セクタ数を入れる。 MOV DX,2 ;DXレジスタに2を入れる DIV DX ;AX ÷ DX を実行し、商をAXへ。余りをDXへ。 MOV DX,AX ;AXレジスタの値をDXにコピー POP AX ;AXレジスタを元に戻す(論理セクタ値をセット) PUSH AX ;もう一回バックアップ。 DIV DX ;これで、AXにはhの値が入っているはず。
ふむ・・・一旦DOSのDEBUGで確認してみよう。
・・・あっれえええええ??? 割り算(DIV)がどうしてもうまくいかないぞ??
なになに??? 「÷2」などは shr を使うのが早いと? じゃあこうか。
PUSH AX ;AXには論理セクタ値が入っている。一旦バックアップ。 MOV AX,2880 ;まず、AXに総セクタ数を入れる。 SHR AX,1 ;AX ÷ 2 を実行ししたのと同じ結果になる。 MOV DX,AX ;AXレジスタの値をDXにコピー POP AX ;AXレジスタを元に戻す(論理セクタ値をセット) PUSH AX ;もう一回バックアップ。 DIV DX ;これで、AXにはhの値が入っているはず。
うーーーむ・・・・ 16bit同士の割り算ってどうやったらいいんだ・・・
まいったなぁ。やっぱどっかメモリを使わないといけないのかなぁ。メモリは遅いからできれば使いたくない。・・・うーん。遅いっていえばそもそもDIV命令も評判がよくないなぁ。なんかうまい工夫ないもんかな。
こういう時って、CXやBX使っちゃいけないのかなぁ・・・・。例えば
PUSH CX ;CXレジスタの値をバックアップ PUSH AX ;AXには論理セクタ値が入っている。一旦バックアップ。 MOV AX,2880 ;まず、AXに総セクタ数を入れる。 SHR AX,1 ;AX ÷ 2 を実行ししたのと同じ結果になる。 MOV CX,AX ;AXの内容をCXにコピー POP AX ;AXの値を論理セクタに戻す PUSH AX ;もう一回バックアップ MOV DX,0 ;これでDX:AXの値は32bitながら論理セクタ値となるはず。 DIV CX ;これで、AXにはhの値が入っているはず。 MOV DX,AX ;AXの値(h)をDXレジスタに移す POP AX ;AXを初期値に戻す(論理セクター値) POP CX ;CXを初期値に戻す ;これで全て元通りで、かつ、DXにはhがセットされているはず。
うーむ。とりあえずは成功かな???
そんなわけで、こんなコードを妄想してみる。
debugコマンドで検証してみないと・・・
くそ!だめだ。割り算がメタメタだ。たぶん桁のせいだろう・・・
シリンダやセクタは同時に計算できないだろうか。
そんなわけで、こういうコードコードを書いてみた。
キタ━━━━━━(゚∀゚)━━━━━━!!!!!!!!
たぶん、これで大丈夫なはずだぞ!!!!!
がーーーん! 大勘違いしてたかも!?????。スタックって、メモリなの?????? それじゃ、どこかに適当にメモリを確保して書き込んだって同じじゃんかよ!!!
課題としては、「DIV命令は遅いので最小で」と、「メモリも遅いので出来るだけレジスタ内で完結」だなこりゃ。
コード2を見直そう・・・・・ orz
そんなわけでこんなコードになった。スタックの使用を最小限にして、割り算も減らした。OK。あとは、実際に繰り返しを行うプログラムを書いた時に、一回一回割り算をしなくてもいいように調整すると。
どこに読み込むか?(格納メモリ番地) 
上記のオベンキョでセクタの指定はできるようになったけど、読んだセクタはどこに格納したらいいでしょう???
まずはメモリマップを読んでお勉強
K氏のheboOSのIPLでは、読み込みは 0x800:0x100からスタートしている。このアドレスはなんだろう? (たぶんMS-DOSの実行ファイルとの互換だろうが・・・)
えーーーーっと・・・・・
8086時には、セグメント:オフセットという指定でアドレスを指定できるんだけど、本来は0hからいくらでもアドレスは増やせるはず。(たぶんプロテクトモードなんかではそうなんだろう。
セグメントの指定は5桁まで。で、オフセットは4桁まで。そう考えると、8086時には
0h 〜 FFFFFh までの空間がアクセル可能と。(5桁だから)
0h 〜 FFFFF までのアドレスにアクセルできる。で、この中に勝手に使っていい部分とそうでない部分があるはず。
heboOSで読み込んでいるアドレスは、0x800:0100 ってことは、
00800 + 0100 = 900番地
からは、勝手に使っていい・・・・のかな? いやたぶんそんなことはないはず。0〜900番地の間はどうなっているんだろう???
IPLはかならずセクター0なのでいいのですが、FAT領域は何番から何番なのでしょう。 間違い!。 セクターは、「1」からスタートするみたい。
うーん。ずばり書いてある資料がないなぁ。フォーマットによって多少変化するのか。このFAT12の場合だと・・・・上記のBPBから算出できると?。
DW 1;FATがどこから格納されているか(このBPBセクタからの相対セクタ番号で)
ということは、BPBがあるブートセクタは0。相対的に1ってことは、「セクター1〜」ということかな?
DW 9 ;FATの長さ(1つ分の長さをセクタ数で)
なんだから、9セクターあると。さらに
DB 2 ;FATがいくつあるか(一般的には2)
なんだから、9x2で18。ってことは、FAT領域は「セクター1〜セクター18」で、データ領域は「セクター19〜おしまいまで」ってことでいいのかな?
どうもちがうみたいだ。FATが二個。これ、一個は非常時用の予備?らしいぞ。 だから、FAT領域は「セクター2〜セクター10まで」ってことかな。
データ(OS)の格納 
全てセクター単位で読み書きするってんなら、このFAT12の配置は無視できなくもない。
(セクター1のIPLだけはダメ)
しかし、さしたる理由もなく独自書きしてもあとでこまるので、面倒でもFAT12の形式を踏むのがいいと思う。
たとえば、OSが8KBあったとする。1セクタ=512Bだから、
8000÷512=15.6
つまり、16セクターあれば書き込めるわけだ。
IPL以外の場所(たとえばセクター2〜とか、とりあえずFAT領域を避けてセクター11〜とか)から適当に順番に書き込むことはできるし、わかっていれば読み込むこともできる。
しかし、例えばセクター11〜セクター27までにOSを書き込んだとして、この位置を上書きされてしまったら大変だ。特定のセクターだけは書き込みできないようにしなければいけない。
いろいろな方法があるようだが、一番簡単なのは、「そのセクターはファイルxxxが使用中だよ」ということにしてしまうのが順当。(MS-DOSなんかのIO.SYSやMSDOS.SYSなんかはこの方法のようだ)