Advent Calendar 2018: advos
Day 7: ブートローダ(32/64ビットモード)
今日はいよいよ16ビットリアルモードから32ビットプロテクテッドモード,64ビットロングモードにCPUモードを遷移していきます。
ソースコード
前回まで作ってきたブートモニタの起動ルーチンから32ビットモード,64ビットモードに遷移させます。bootmon.S
にまとめて記述することもできますが,今回は16ビット,32ビット,64ビットでファイルを変えようと思います。
bootmon.S
: ブートモニタ。起動ルーチンから16ビットモードのままentry16.S
のentry16
ラベルにジャンプする。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段階のページテーブルは以下のように構成されます。
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個のテーブルを作成します。
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エントリにそれぞれ,0x0007b007
,0x0007c007
,0x0007d007
,0x0007e007
を書き込みます。これは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を使ってシステムメモリマップを読み込むところを作っていこうと思います。