panda's tech note

Advent Calendar 2018: advos

Day 7: ブートローダ(32/64ビットモード)

今日はいよいよ16ビットリアルモードから32ビットプロテクテッドモード,64ビットロングモードにCPUモードを遷移していきます。

ソースコード

前回まで作ってきたブートモニタの起動ルーチンから32ビットモード,64ビットモードに遷移させます。bootmon.Sにまとめて記述することもできますが,今回は16ビット,32ビット,64ビットでファイルを変えようと思います。

  • bootmon.S: ブートモニタ。起動ルーチンから16ビットモードのままentry16.Sentry16ラベルにジャンプする。
  • entry16.S: 16ビットモードのエントリポイント。32ビットモードの準備を行い,32ビットモードに切り替えると同時にentry32.Sのエントリポイントentry32にロングジャンプする。
  • entry32.S: ブートシーケンスで使用するページテーブル(0–4 GiB領域のリニアマッピング)を準備し,64ビットモードに遷移するとともにentry64.Sのエントリポイントentry64に飛ぶ。32ビットモードから64ビットモードのロングジャンプ命令はないため,代わりにlret命令を使用する。

これに伴い,src/boot/Makefileを変更しました.

A20ゲートの有効化:1 MiB以上のメモリアドレスへのアクセス

32ビットモード,64ビットモードに遷移する前に1 MiB以上のメモリアドレスへのアクセスを有効化します。16ビットモードでは,メモリアドレスをセグメントレジスタ(Segment)とセグメント内のオフセット(Offset)で表します。リニアアドレスは(Segment << 4) + Offsetとなります。例えば,Segment=0xffff, Offset=0xffffのとき,リニアアドレスは0x10ffefとなるべきですが,Intel 8086プロセッサでは20ビットしかアドレス線が用意されていなかったため,0x0ffefとなっていました。Intel 80286 (i286)以降のプロセッサでは,21ビット目のアドレス線が導入されたため,1 MiBを超えるメモリにもアクセスできるようになりましたが,互換性のため,21ビット目のアドレス(A20 Gate/Line)を無効化・有効化できるようになっています。ここでは,A20ゲートを有効にします。

A20の歴史に興味がある方はA20 - a pain from the pastを参照してください。

AT互換機におけるA20ゲートの無効化・有効化は,歴史的経緯で8042 (PS/2)キーボードコントローラの出力ポートを通じて行われます。SMSCキーボードコントローラは以下の通り定義されています。

  • Bit 0: システムリセット
  • Bit 1: A20ゲート
  • Bit 2: 未定義
  • Bit 3: 未定義
  • Bit 4: 出力バッファがフル
  • Bit 5: 入力バッファが空
  • Bit 6: キーボードクロック(出力)
  • Bit 7: キーボードデータ(出力)

A20ゲートはこのbit 1をセット・クリアすることで有効化・無効化できます。このビット操作はキーボードコントローラにコマンドを送信することで実現します。

src/boot/bootmon.Sに追加したenable_a20ルーチンについて説明します。

    xorw	%cx,%cx
1:
    incw	%cx		/* Try until %cx overflows (2^16 times) */
    jz	3f		/*  Failed to enable a20 */
    inb	$0x64,%al	/* Get status from the keyboard controller */
    testb	$0x2,%al	/* Busy? */
    jnz	1b		/* Yes, busy.  Then try again */

まず,キーボードコントローラのステータスポート(64h, read-only)でステータスを確認し,Busyでなくなるまでループします。 ; Data port is at I/O address 60h ; Status port is at I/O address 64h (read only) ; Command port is at I/O address 64h (write only)

    movb	$0xd1,%al	/* Command: Write output port (0x60 to P2) */
    outb	%al,$0x64	/* Write the command to the command register */
2:
    inb	$0x64,%al	/* Get status from the keyboard controller */
    testb	$0x2,%al	/* Busy? */
    jnz	2b		/* Yes, busy.  Then try again */
    movb	$0xdf,%al	/* Command: Enable A20 */
    outb	%al,$0x60	/* Write to P2 via 0x60 output port */

次に,キーボードコントローラのコマンドポート(64h, write-only)にWrite Output Portコマンド(D1h)を書き込みます。ステータスを確認しBusy状態でなくなるまで試行します。Write Output Portコマンドが準備できたら,コマンドDFh(A20を無効化する場合はDDh)をデータポート(60h)に書き込みます。

16ビットモードから32ビットモードへの遷移

16ビットモード(Real Mode)と32ビットモード(Protected Mode)の違いはメモリ幅以外にメモリアクセス方法の違いがあります。16ビットモードではセグメントでメモリ領域を保護します。32ビットモードではGlobal Descriptor Table (GDT)およびLocal Descriptor Table (LDT)で保護する方法とページテーブルで保護する方法があります。ページテーブルはGDTと組み合わせて使用します。

64ビットモードではページテーブルが必須なので,今回はGDTRで厳密にメモリ保護のための領域を定義せずに,ページテーブルでメモリ保護をします。

GDT

GDTはメモリ領域とそのアクセス権を設定するエントリの集合です。GDTのエントリは以下のように定義されます。

31                                                             0
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Base address  |G|D|X|A| Limit |P|DPL|S| Type  | Base address  | +8
| 24:31         | |B| | | 16:19 | |   | |       | 16:23         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Base address                  | Segment limit                 | +0
| 0:15                          | 0:15                          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
63                                                            32

G: Granularity bit. If 0 the limit is in 1 B blocks (byte granularity),
   if 1 the limit is in 4 KiB blocks (page granularity).
DB: Size bit. If 0 the selector defines 16 bit protected mode. If 1 it
    defines 32 bit protected mode. You can have both 16 bit and 32 bit
    selectors at once.
X: L bit in x86-64

P: Present bit. This must be 1 for all valid selectors.
DPL: Privilege, 2 bits. Contains the ring level, 0 = highest (kernel),
     3 = lowest (user applications).
A: 0
S: 1

Type: Ex DC RW Ac
Ex: Executable bit. If 1 code in this segment can be executed, ie. a
    code selector. If 0 it is a data selector.
DC: Direction bit/Conforming bit.
RW: Readable bit/Writable bit.
Ac: Accessed bit. Just set to 0. The CPU sets this to 1 when the segment
    is accessed.

16ビットコード領域(今回は定義しますが使いません),32ビットコード領域,64ビットコード領域,32ビットデータ領域の4つを定義します。また,1つ目のGDTエントリのすべてのビットを0にしたNull descriptorと呼ばれるものが必須であるため,そちらも定義します。これらの5つの定義は以下の通りです。

gdt:
    .word	0x0,0x0,0x0,0x0		/* Null descriptor */
    .word	0xffff,0x0,0x9a00,0xaf	/* Code64 descriptor */
    .word	0xffff,0x0,0x9a00,0xcf	/* Code32 descriptor */
    .word	0xffff,0x0,0x9a00,0x8f	/* Code16 descriptor */
    .word	0xffff,0x0,0x9200,0xcf	/* Data descriptor */

Null descriptor以外は全領域を定義するので,

  • Base address=0
  • Segment limit=0xfffff
  • G=1
  • DB={32ビットモードの場合1,16/64ビットモードの場合0}
  • X={64ビットモードの場合1,それ以外の場合0}
  • P=1
  • DPL=0(今回のブートローダはすべて特権=Ring 0で動作するので0)
  • Type={コードセグメントの場合0xa(Ex,RW),データセグメントの場合0x2(RW)} と設定します。

GDTのサイズとベースアドレスの定義をGDT Register (GDTR)と呼びます。GDTRは連続したメモリ領域上に

  • 16ビット: GDTのサイズ -1
  • Nビット: GDTのベースアドレス(N=16, 32, 64; CPUモードに依る) で定義します。src/boot/entry16.Sではgdtrラベルに定義しています。

以下のLGDT命令でGDTRを読み込んでGDTを有効にします。

    lgdt	(gdtr)

IDT

16ビットモードでは割り込みハンドラをIVTに設定しました。このIVTはBIOSのデフォルトでメモリの先頭から定義されているため,そのまま使いました。

32/64ビットモードではIVT領域を自分で定義する必要があります。このIVTの定義をInterrupt Descriptor Table (IDT)と呼びます。このIDTの定義はIDT Register (IDTR)を通じてCPUに読み込めます。

IDTRは

  • 16ビット: Limit (IDTのサイズ -1)
  • Nビット: IDTのベースアドレス(N=16, 32, 64; CPUモードに依る) で定義されるメモリ領域です。

今日は32/64ビットモードでは,割り込みは扱わないので一時的にLimit=0のIDTを読み込みます。正しい割り込みハンドラとIDTは後日カーネルの中で定義する予定です。

Protected Modeの有効化

GDTとIDTを設定したのちに,

    movl	%cr0,%eax
    orl	$0x1,%eax
    movl	%eax,%cr0

でコントロールレジスタCR0のBit 0をセットすることでProtected Modeを有効化できます。Protected Modeを有効にした直後に,GDTで定義した32ビットコード領域のオフセット値(0x10)をセグメントレジスタCSにセットし,32ビットコードのエントリポイントにロングジャンプすることで,16ビットモードのパイプラインをフラッシュし,32ビットコードに移行できます。具体的には以下のコードでCS=0x10, EIP=$entry32にジャンプします。

    ljmpl	$0x10,$entry32		/* CS=0x10: Code 32 descriptor */

32ビットモードの初期化

32ビットモードの初期化と64ビットモードへの遷移はsrc/boot/entry32.Sに書いています。

32/64ビットモードでは割り込みコントローラとしてPICではなくLocal APICとI/O APICを使います。昨日の電源OFFのときにも説明した通り,i8259 PICを無効化します。

    movb	$0xff,%al
    outb	%al,$0x21
    movb	$0xff,%al
    outb	%al,$0xa1

次にスタックセグメントおよびデータセグメントをGDTで定義した32ビットデータ領域(オフセット=0x20)にします。

    movl	$0x20,%eax
    movl	%eax,%ss
    movl	%eax,%ds
    movl	%eax,%es
    movl	%eax,%fs
    movl	%eax,%gs

また,

    movl	$0x7c00,%eax
    movl	%eax,%esp

のようにスタックを設定します。今回はブートモニタのスタック領域を再利用します。

64ビットモードへの遷移

ここからは32ビットモードから64ビットモードへの遷移の準備を行います。

Page Address Extensionの有効化

32ビットモードでは元来,4 GiBまでの物理メモリ領域しか扱えませんでした。PAEはページテーブルのアドレッシングを32ビットから64ビットに拡張することで,32ビットモードでも4 GiB以上の物理メモリ領域を使用できるようにします。この後,ページテーブルを設定するので,ここでPAEを有効化して32/64ビットモードで共通のページテーブルを使用できるようにします。

PAEを有効にするには以下のようにコントロールレジスタCR4のbit 5をセットします。

    /* Enable Page Address Extension (PAE) */
    movl	$0x20,%eax		/* CR4[bit 5] = PAE */
    movl	%eax,%cr4

ページテーブルの設定

ページテーブルは仮想アドレスから物理アドレスのマッピングを行うテーブルです。1つのテーブルでこのマッピングを行うと,テーブルのサイズが非常に大きくなってしまうため,ページテーブルは通常複数段の木構造となります。

x86-64のページテーブルは4段階のページテーブルで構成されます(注:メモリの大容量化により最新のCPUでは5段のページテーブルが使えるようになっているものもあります)。4段階のページテーブルは以下のように構成されます。

Page Table

1つのテーブルは4 KiBです。PAEを有効にした場合,1エントリは64ビット(8バイト)なので,512エントリ含まれます。最上位のテーブルは1エントリあたり512 GiBなので,最大で256 TiBまでの仮想アドレスを扱うことができます。

今回は,ブートローダで使うためのページテーブルなので,0-4 GiBの仮想アドレスをそのまま物理アドレスにマッピングします。最小のページサイズは4 KiBですが,4 GiB分4 KiBページでマッピングするとページテーブルが巨大になってしまうので,2 MiBページングで2048ページ分のページテーブルを作成します。1 GiBページングはCPUによってサポートされていないことがある(サポートされているはずの古めのx86-64 CPUでも実機で実行したところエラーになったことがある)ので今回は使いません。

0–4 GiBのリニアマッピングをするためには以下のようにPML4 1つ,PDPT 1つ,PD 4つの6個のテーブルを作成します。

Page Table 2

3日目に説明した以下のメモリマップの通り,ブートローダのページテーブルは0x00079000に配置します。

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以上必要)

ここからはこのページテーブル設定のコードを見ていきます。各段のテーブルの定義についてはこのページの後半にまとめます。

まず,

pg_setup:
    movl	$0x00079000,%ebx	/* Low 12 bit must be zero */
    movl	%ebx,%edi
    xorl	%eax,%eax
    movl	$(512*8*6/4),%ecx
    rep	stosl			/* Initialize %ecx*4 bytes from %edi */
                    /*  with %eax */

で,0x00079000から6個のテーブル分(51286=24 KiB),ゼロ初期化します。

次に,

    /* Level 4 page map */
    leal	0x1007(%ebx),%eax
    movl	%eax,(%ebx)

でPML4の1エントリ目に0x0007a007を代入します。これは下位12ビットをマスクした値0x0007a000がアドレスで,下位12ビットの0x007がこのエントリがR/W,一般権限でアクセス可能な下位テーブルへのポインタを表しています。

次はPDPTの設定を行います。

    /* Page directory pointers (PDPE) */
    leal	0x1000(%ebx),%edi
    leal	0x2007(%ebx),%eax
    movl	$4,%ecx
pg_setup.1:
    movl	%eax,(%edi)
    addl	$8,%edi
    addl	$0x1000,%eax
    loop	pg_setup.1

の処理で,0x0007a000から4エントリにそれぞれ,0x0007b0070x0007c0070x0007d0070x0007e007を書き込みます。これはPML4で説明したのと同様にR/W,一般権限でアクセス可能な回テーブルのポインタ(下位12ビットをマスクした値)を表しています。

上記のコードで,0x0007b000から0x0007efffまでが0–4 GiBの連続した2 MiBページを表すページテーブルになりました。

    /* Page directories (PDE) */
    leal	0x2000(%ebx),%edi
    movl	$0x083,%eax
    movl	$(512*4),%ecx
pg_setup.2:
    movl	%eax,(%edi)
    addl	$8,%edi
    addl	$0x00200000,%eax
    loop	pg_setup.2

で,各ページにリニアアドレスの下位12ビットを0x183を設定していきます。この0x083はR/W,特権のみでアクセス可,このページが2 MiBページングのエントリであることを表します。

最後に,

    /* Set page table register */
    movl	%ebx,%cr3

0x00079000をコントロールレジスタCR3にセットすることでページテーブルの設定は完了です。

各段のテーブルについて,エントリのビット定義は以下にまとめます。

CR0
    63                                                             32
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    | Ignored & Address of PML4 table                               |
    |                                                               |
    |                                                               |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    | Address of PML4 table (contd)         | Ignored     |P|P|Ign. |
    |                                       |             |C|W|     |
    |                                       |             |D|T|     |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    31                                                             0
PML4E
    63                                                             32
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    | Ignored               | Rsvd. & Address of                    |
    |                       |  page-directory-pointer table         |
    |                       |                                       |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    | Address of page-directory-pointer     | Ign.  |R|I|A|P|P|U|R|1|
    |  table (contd)                        |       |s|g| |C|W|/|/| |
    |                                       |       |v|n| |D|T|S|W| |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    31                                                             0
PDPTE (1GB page)
    63                                                             32
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    | Ignored               | Rsvd. & Address of 1GB page frame     |
    |                       |                                       |
    |                       |                                       |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |   |  Reserved                       |P| Ign.|G|1|D|A|P|P|U|R|1|
    |   |                                 |A|     | | | | |C|W|/|/| |
    |   |                                 |T|     | | | | |D|T|S|W| |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    31                                                             0
PDPTE (page directory)
    63                                                             32
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    | Ignored               | Rsvd. & Address of page directory     |
    |                       |                                       |
    |                       |                                       |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    | Address of page directory (contd)     | Ign.  |0|I|A|P|P|U|R|1|
    |                                       |       | |g| |C|W|/|/| |
    |                                       |       | |n| |D|T|S|W| |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    31                                                             0
PDE (2MB page)
    63                                                             32
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    | Ignored               | Rsvd. & Address of 2MB page frame     |
    |                       |                                       |
    |                       |                                       |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    | Address of 2MB page | Reserved      |P| Ign.|G|1|D|A|P|P|U|R|1|
    |  frame (contd)      |               |A|     | | | | |C|W|/|/| |
    |                     |               |T|     | | | | |D|T|S|W| |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    31                                                             0
PDE (page table)
    63                                                             32
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |X| Ignored             | Rsvd. & Address of page table         |
    |D|                     |                                       |
    | |                     |                                       |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    | Address of page table (contd)         | Ign.  |0|I|A|P|P|U|R|1|
    |                                       |       | |g| |C|W|/|/| |
    |                                       |       | |n| |D|T|S|W| |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    31                                                             0
PTE (4KB page)
    63                                                             32
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |X| Ignored             | Rsvd. & Address of 4KB page frame     |
    |D|                     |                                       |
    | |                     |                                       |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    | Address of 4KB page frame (contd)     | Ign.|G|P|D|A|P|P|U|R|1|
    |                                       |     | |A| | |C|W|/|/| |
    |                                       |     | |T| | |D|T|S|W| |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    31                                                             0

Long Modeへの遷移

ページテーブルの設定が終わったので,64ビットモード(Long Mode)に遷移します。

まず,64ビットモードに遷移するには,EFER MSR (Address: 0xc0000080)と呼ばれるMachine Specific Register (MSR)のbit 8 (LME: Long Mode Enable)をセットします。

    /* Enable long mode */
    movl	$0xc0000080,%ecx	/* EFER MSR number */
    rdmsr				/* Read from 64 bit-specific register */
    btsl	$8,%eax			/* LME bit = 1 */
    wrmsr

次にコントロールレジスタCR0のbit 31 (Paging)をセットします。

    /* Activate page translation and long mode */
    movl	$0x80000001,%eax
    movl	%eax,%cr0

最後に32ビットモードに遷移したときのようにセグメントレジスタをGDTの64ビットコード(Offset=0x08)にセットして,64ビットコードのエントリポイントにロングジャンプするのですが,32ビットコードではセグメントレジスタを変更するロングジャンプは使えないため,代わりに以下のようにLRET命令を使います。

    /* Load code64 descriptor */
    pushl	$0x08
    pushl	$entry64
    lret

LRETはスタックから8バイトのリターン先とセグメントレジスタを取り出して,リターン先の処理に戻る命令なので,上記のpushlを使ったコードは実質的にロングジャンプと同等の処理です。

C言語の世界へ

src/boot/entry64.Sが64ビットモードのコードです。64ビットモードになったので,ここからはアセンブリを減らして極力C言語で書いていこうと思います。インラインアセンブリはあまり好きではないので,アセンブリが必要になるところは適宜アセンブリで関数を定義していこうと思います。

前半は画面をクリアする処理です。

50行目の

    /* Get into the C code */
    call	_centry

で,centryという関数を呼んでいます。ラベルの_Makefile

CFLAGS=-fleading-underscore

このCFLAGSにより,Cの関数はすべて_がラベルの先頭に付けてアセンブリされます。

文字を表示するCコード

5日目に解説したとおり,VIDEO RAMは0xb8000からなので,この領域を直接Cのポインタ越しで触ることで,画面に文字を表示できます。以下のコードはその一例です。

typedef unsigned short u16;

void hlt(void);

/*
 * Entry point for C code
 */
void
centry(void)
{
    /* Print message */
    u16 *base;
    char *msg = "Congraturations!  Welcome to the 64-bit world!";
    int offset;

    base = (u16 *)0xb8000;
    offset = 0;
    while ( msg[offset] ) {
        *(base + offset) = 0x0700 | msg[offset];
        offset++;
    }

    /* Sleep forever */
    for ( ;; ) {
        hlt();
    }
}

これにより,

Congraturations!  Welcome to the 64-bit world!

と画面に表示されるようになったかと思います。

まとめと明日の予定

今日はついに64ビットロングモードでモダンなOSと同じようにx86-64のCPUを64ビットモードで使えるようになりました。さらにC言語が使えるようになったので,これからのプログラミングが少しは簡単になるはずです。

明日はカーネルに移る前に,BIOSを使ってシステムメモリマップを読み込むところを作っていこうと思います。