Advent Calendar 2018: advos
Day 3: ブートローダ(ディスク読み込み)
MBRからブートモニタへ。
今日からブートローダのうち,起動するOSを選択したり,起動オプションを選択するブートモニタと呼ばれるプログラムを作ります。昨日解説したように,MBRは446バイトまでのプログラムしか配置できない(正確にはパーティションを無くせば510バイトまで可能ですが…)ので,今日は,MBRからブートモニタ領域をメモリに読み込んで処理をそちらに移す(ジャンプする)コードを書きます。
メモリマップ
前回,スタックを0x7c00
から下方向に延びるようにしました。しかし,ビデオRAMのようなMemory Mapped I/O (MMIO)など,主記憶として使えない領域もあります。1 MiB以上の領域は,メモリのどの領域が使用可能か,システムで使用されているかをまとめたメモリマップ(システムアドレスマップ)をINT 15h, EAX=E820hで取得できます。1 MiB以下の領域は機器依存で取得する方法はない(と思われます)ですが,一般的なメモリマップはMemory Map (x86)にまとめられています。この表によると,RAM (guaranteed free for use) と無条件で使える領域は
0x00000500-0x00007BFF
0x00007E00-0x0007FFFF
の2箇所(510 KiB程度)となっています。ブートモニタはこの領域を使って行こうと思います。カーネルについては,別途システムアドレスマップを取得して場所を確保する予定です。advosのブートローダでは以下のアドレスマップを使うことにします。
Start | End | Description |
---|---|---|
00000500 | 00007bff | ブートローダのスタック領域 |
00007c00 | 00007dff | MBR |
00007e00 | 00007fff | 未使用 |
00008000 | 00008fff | ブート情報(ドライブ番号など) |
00009000 | 0000cfff | ブートモニタ (16 KiB) |
0000d000 | 0000ffff | 未使用 (ブートモニタ拡張用に予約) |
00010000 | 00079fff | 未使用 |
00079000 | 0007ffff | ロングモード用ページテーブル (24 KiB = 6 * 4 KiB以上必要) |
ブートモニタ(になる予定のプログラム)
上述した通り,ブートモニタは0x9000-0xcfff
の領域に配置します。今日作成したブートモニタ(と呼べないプログラム)は,昨日MBRで使ったメッセージを表示するだけの簡単な機能です。詳細はsrc/boot/bootmon.Sを参照してください。MBRとの違いは,プログラムが0x9000
に読み込まれるため,リンカスクリプト(src/boot/bootmon.ld
)を以下の通り,.text
セクションを0x9000
から始まるバイナリにしている点です。エントリポイントはbootmon
というラベルにしました。
OUTPUT_FORMAT("binary","binary","binary");
OUTPUT_ARCH(i386:x86-64);
ENTRY(bootmon)
SECTIONS
{
. = 0x9000;
.text : { *(.text) }
.data : { *(.data) }
}
ブートモニタの読み込み
ブートモニタをディスクからメモリに読み込み,そちらに処理を移すためにMBRを大幅に変更しました。src/boot/mbr.Sの変更点を説明しようと思います。
BIOSのディスクへのアクセスはCHS (Cylinder, Head, Sector)でアクセスします。CHSはフロッピーディスクやHDDの時代のものですが,SSDやUSBフラッシュメモリからのブートの場合もCHS形式でアクセスします。
CHSは,シリンダ,ヘッド,セクタの番号を指定するもので,シリンダ,ヘッドは0オリジン,セクタは1オリジンのインデクシングをします。セクタは下図の通り,ディスクのプラッタ(円盤)の1つのトラックの一部分です。BIOSのディスクサービスから呼ばれる場合,1セクタは512バイトとして扱います。このセクタの周回部分がトラックと呼ばれています。
HDDは複数のプラッタから構成されており,それぞれのプラッタには下図の通りデータ読み込みのためのヘッドが付いています。
また,全てのプラッタの同一円心上のトラックはその形状からシリンダと呼ばれています。
このセクタ(トラック内の位置),ヘッド,シリンダからセクタの絶対位置が決定されるため,このCHS情報をアドレスとしてディスクの読み込みをします。ただし,CHSは現代ではなじみが少ない上に,SSDなどのデバイスの物理特性と異なるため,通常はシーケンシャルなセクタ番号としてLogical Block Address (LBA)を用いてアクセスします。これをCHSに変換する方法は後述します。
LBAをCHSに変換するために,まず,ブートデバイスのドライブ情報を取得します。具体的には,トラックごとのセクタ数,プラッタごとのトラック数,ヘッド数を取得します。以下にまとまますが,ドライブへのアクセスはBIOSのINT 13hで行います。
ドライブ情報は以下のようにINT 13h, AH=08hで取得します。本ページの下部にまとめる通り,CX,DHにセクタ数(最大セクタ番号),トラック数(最大トラック番号+1),ヘッド数(最大ヘッド番号+1)が取得できます。
/* Get drive parameters */
xorw %ax,%ax
movw %ax,%es
movw %ax,%di
movb $0x08,%ah
int $0x13
jc disk_error
下記のコードで取得したセクタ数,トラック数,ヘッド数を.data
セクションに保存します。
/* Save the drive information (CHS) */
incb %dh /* Get # of heads (%dh: last index of heads) */
movb %dh,heads
movb %cl,%al /* %cl[5:0]: last index of sectors per track */
andb $0x3f,%al /* N.B., sector is one-based numbering */
movb %al,sectors
movb %ch,%al /* %cx[7:6]%cx[15:8]: last index of cylinders */
/* then copy %cx[15:8] to %al */
movb %cl,%ah /* Lower byte to higher byte */
shrb $6,%ah /* Pick most significant two bits */
incw %ax /* N.B., cylinder starting with 0 */
movw %ax,cylinders
ここからはbootmon
のメモリへの読み込みを行います。以下のコードはドライブ番号drive
の1セクタ目から32セクタ分を0x0900:0x0000
(リニアアドレスでは0x9000
)に読み込むread
ルーチンを呼び出しています。
#define BOOTMON_SEG 0x0900 /* Memory where to load boot monitor */
#define BOOTMON_OFF 0x0000 /* segment and offset [0900:0000] */
#define BOOTMON_SIZE 0x0020 /* Boot monitor size in sector */
/* Load boot monitor */
movb drive,%dl
movw $BOOTMON_SIZE,%cx/* Specify boot monitor size */
movw $1,%ax
movw $BOOTMON_SEG,%bx
movw %bx,%es /* Buffer address pointer (Segment) */
movw $BOOTMON_OFF,%bx/* Buffer address pointer (Offset) */
call read /* Read %cx sectors starting at LBA %ax on */
/* drive %dl into %es:[%bx] */
read
ルーチンは,LBA形式のアドレスをCHS形式に変換するlba2chs
ルーチンと1セクタ読み込むread_sector
を読み込むセクタ分実行しています。
/*
* Load sectors from the disk
* Parameters:
* %dl: drive
* %cx: # of sectors to read
* %ax: Position in LBA
* %es:(%bx): Buffer
*/
read:
pushw %bp
movw %sp,%bp
/* Save registers */
movw %ax,-2(%bp)
movw %bx,-4(%bp)
movw %cx,-6(%bp)
movw %dx,-8(%bp)
/* Prepare space for local variables */
/* u16 counter -10(%bp) */
subw $10,%sp
/* Reset counter */
xorw %ax,%ax
movw %ax,-10(%bp)
1:
movw -2(%bp),%ax /* Restore %ax */
addw -10(%bp),%ax /* Current LBA */
call lba2chs /* Convert LBA (%ax) to CHS (%cx,%dh) */
call read_sector /* Read a sector */
/* Next 512-byte buffer */
addw $512,%bx
/* Increment the counter */
movw -10(%bp),%ax
incw %ax
movw %ax,-10(%bp)
/* More sectors to read? */
cmpw -6(%bp),%ax
jb 1b /* Read more sectors */
/* Restore the saved registers */
movw -8(%bp),%dx
movw -6(%bp),%cx
movw -4(%bp),%bx
movw -2(%bp),%ax
movw %bp,%sp
popw %bp
ret
ここで,レジスタだけでこのルーチンを扱うのは大変なので,スタック領域を変数領域として使用しています。この手法はC言語のコンパイラでも使われています。
pushw %bp
movw %sp,%bp
はベースポインタ%bp
をスタックに保存し,%bp
に現在のスタックポインタを設定します。ここから
movw %ax,-2(%bp)
movw %bx,-4(%bp)
movw %cx,-6(%bp)
movw %dx,-8(%bp)
はルーチン内で書き換わるレジスタの現在値をスタックの下に保存しています。この次の命令の
subw $10,%sp
はスタックポインタから10を引く命令ですが,これにより10バイト分のスタックを積んだことと同じになります。ただし,上述したレジスタの保存は4つ分で-10(%bp)
については値が設定されていません。これを局所変数領域として使用しています。上記のコードでは読み込んだセクタ数のカウンタとして使っています。
以下のコードは1セクタ分メモリに読み込むルーチンです。基本的にはINT 13h, AH=02hのBIOSサービス呼び出しにより書かれていますが,一点補足すると,フロッピーディスクはその物理特性から読み込みに失敗することがあるため,エラーの場合は最大3回までリトライします。フロッピーを使わない(または使っても仮想マシンで失敗しない)状況であればリトライは無視してもよいです。
/*
* Load one sector from the disk
* Parameters:
* %dl: drive
* %cx, %dh: CHS (%cx[7:6]%cx[15:8] ,%dh, %cx[5:0])
* %es:(%bx): Buffer
*/
read_sector:
pushw %bp
movw %sp,%bp
/* Save registers */
movw %ax,-2(%bp)
/* Prepare space for local variables */
/* u16 retries -4(%bp); retry counter */
/* u16 error -6(%bp); error code */
subw $6,%sp
/* Reset retry counter */
xorw %ax,%ax
movw %ax,-4(%bp)
1:
/* Read a sector from the drive */
movb $0x02,%ah
movb $1,%al
int $0x13
jnc 2f /* Jump if success */
movw %ax,-6(%bp) /* Save the error code */
movw -4(%bp),%ax
incw %ax
movw %ax,-4(%bp)
cmpw $NUM_RETRIES,%ax
movw -6(%bp),%ax /* Restore the error code */
ja disk_error /* Exceeded the maximum number of retries */
cmpb $ERRCODE_TIMEOUT,%ah
je disk_error /* Immediately give up if timeout */
jmp 1b
2:
/* Restore saved registers */
movw -2(%bp),%ax
movw %bp,%sp
popw %bp
ret
次にLBAをCHSに変換するコードを見ていきます。上述のドライブ情報で取得した最大セクタ・ヘッド数で除算をしてシリンダ番号・ヘッド番号・セクタ番号を計算しています。以下の式はシリンダ・ヘッド・セクタの計算式です。ただし,除算は商(整数値)です。
Sector = LBA % (# of sectors) + 1
Head = (LBA % (# of sectors)) % (# of heads)
Cylinder = (LBA / (# of sectors)) % (# of heads)
これに基づくとLBAからCHSに変換するコードは以下のようになります。INT 13h, AH=02hにあわせて,返り値のCHS は%cx
と%dh
に同一の形式で保存しています。
/*
* LBA to CHS
* Parameters:
* %ax: LBA
* Return values:
* %cx, %dh: CHS (%cx[7:6]%cx[15:8] ,%dh, %cx[5:0])
*/
lba2chs:
/* Save registers */
pushw %ax
pushw %bx
pushw %dx
/* Compute sector number */
xorw %bx,%bx
movw %bx,%dx
movw %bx,%cx
movb sectors,%bl
divw %bx /* %dx:%ax / %bx; %ax:quotient, %dx:remainder */
incw %dx /* Sector number is one-based numbering */
movb %dl,%cl /* Sector: %cx[5:0] */
/* Compute head and track (cylinder) numbers */
xorw %bx,%bx
movw %bx,%dx
movb heads,%bl
divw %bx /* %dx:%ax / %bx; %ax:quotient, %dx:remainder */
movb %al,%ch /* Cylinder[7:0]: %cx[7:6]%cx[15:8] */
shlb $6,%ah
orb %ah,%cl
movw %dx,%bx /* %dl: Head */
popw %dx /* Restore %dx */
movb %bl,%dh /* Head */
/* Restore registers */
popw %bx
popw %ax
ret
Makefile
新しくブートモニタsrc/bootmon
をコンパイルするので,src/boot/Makefile
を書き換えます。また,イメージファイルの2セクタ目にブートモニタを配置するようにsrc/Makefile
も変更しますが,ここに長いシェルスクリプトが含まれているとつらいので,イメージを作成するスクリプトをcreate_image.sh
に分離しました。create_image.sh
にはMBR,ブートモニタのサイズチェックを入れています。
おそらくここの読者には詳しい解説は必要ないと思うので,以下のソースコードを参照してください。
Appendix: BIOSサービスの呼び出し
INT 13h: Disk services
AH | Floppy | IDE | ROM | RAM | RFD | Description |
---|---|---|---|---|---|---|
00h | ✓ | ✓ | ✓ | ✓ | ✓ | Reset |
01h | ✓ | ✓ | ✓ | ✓ | ✓ | Read status |
02h | ✓ | ✓ | ✓ | ✓ | ✓ | Read sectors |
03h | ✓ | ✓ | ✓ | ✓ | Write sectors | |
04h | ✓ | ✓ | ✓ | ✓ | ✓ | Verify sectors |
05h | ✓ | ✓ | Format track | |||
08h | ✓ | ✓ | ✓ | ✓ | ✓ | Read drive parameters |
09h | ✓ | Initialize hard disk controller | ||||
0Ah | ✓ | Read long sectors | ||||
0Bh | ✓ | Write long sectors | ||||
0Ch | ✓ | Seek to cylinder | ||||
0Dh | ✓ | Reset hard disk controller | ||||
10h | ✓ | Test drive ready | ||||
11h | Recalibrate drive | |||||
14h | ✓ | Controller diagnostic | ||||
15h | ✓ | ✓ | ✓ | ✓ | ✓ | Read drive type |
16h | ✓ | ✓ | ✓ | ✓ | Detect media change | |
17h | ✓ | Set diskette type | ||||
18h | ✓ | ✓ | ✓ | ✓ | Set media type for format |
Read/write long sectorsなどのサービスはフロッピードライブなどではサポートされていないことがあるので注意が必要です。
今回は02h, 08hのみを使います。
INT 13h, AH=02h: Read sectors
- 入力パラメータ
- AH: 02h
- AL: 読み込むセクタ数
- CH: トラック番号の下位8ビット
- CL: 上位2ビット=トラック番号(10ビット)の上位2ビット,下位6ビット=セクタ番号
- DH: ヘッド番号
- DL: ドライブ番号
- ES:BX: 読み込み先のバッファアドレス
- 出力パラメータ
- CF: エラーの場合セット
- AH: ディスクステータスコード
- AL: 読み込みセクタ数
トラックとヘッドは0ベース,セクタは1ベースのナンバリングです。
Disk status code
Code | Description |
---|---|
00h | Success |
01h | Invalid Command |
02h | Cannot Find Address Mark |
03h | Attempted Write On Write Protected Disk |
04h | Sector Not Found |
05h | Reset Failed |
06h | Disk change line 'active' |
07h | Drive parameter activity failed |
08h | DMA overrun |
09h | Attempt to DMA over 64kb boundary |
0Ah | Bad sector detected |
0Bh | Bad cylinder (track) detected |
0Ch | Media type not found |
0Dh | Invalid number of sectors |
0Eh | Control data address mark detected |
0Fh | DMA out of range |
10h | CRC/ECC data error |
11h | ECC corrected data error |
20h | Controller failure |
40h | Seek failure |
80h | Drive timed out, assumed not ready |
AAh | Drive not ready |
BBh | Undefined error |
CCh | Write fault |
E0h | Status error |
FFh | Sense operation failed |
INT 13h, AH=08h: Read drive parameters
- 入力パラメータ
- DL: ドライブ番号
- 返り値パラメータ
- CF: エラーの場合セット
- AH: ディスクステータスコード
- BL: ドライブタイプ(フロッピーディスクのみ)
- 01h: 5.25'', 360KB, 40 tracks
- 02h: 5.25'', 1.2MB, 80 tracks
- 03h: 3.5'', 720KB, 80 tracks
- 04h: 3.5'', 1.44MB, 80 tracks
- CH: 最大トラック番号の下位8ビット
- CL: 上位2ビット=最大トラック番号(10ビット)の上位2ビット,下位6ビット=最大セクタ番号
- DH: 最大ヘッド番号
- DL: 搭載されているドライブの数
- ES:DI: フロッピーのディスケットパラメータテーブルエントリーへのポインタ
まとめと明日の予定
今日はMBRからBIOSの機能を使って簡単な文字列の表示するブートモニタ(のなり損ない)を書きました。明日はちゃんとしたブートモニタを実装しようと思います。