panda's tech note

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バイトとして扱います。このセクタの周回部分がトラックと呼ばれています。

Sector

HDDは複数のプラッタから構成されており,それぞれのプラッタには下図の通りデータ読み込みのためのヘッドが付いています。

Head

また,全てのプラッタの同一円心上のトラックはその形状からシリンダと呼ばれています。

Cylinder

このセクタ(トラック内の位置),ヘッド,シリンダからセクタの絶対位置が決定されるため,この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

Embedded BIOSTM 4.1

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の機能を使って簡単な文字列の表示するブートモニタ(のなり損ない)を書きました。明日はちゃんとしたブートモニタを実装しようと思います。