Advent Calendar 2018: advos
Day 9: カーネルのロードとリロケーション
ブートローダでFloppyドライバ,IDE/SATA (AHCI)ドライバ,USB Mass Storageドライバを実装すればカーネルは何処にでも読み込めるのですが,いきなりこれらに対応するのは大変なので,カーネルもリアルモードでBIOSサービスを使って読み込みます。リアルモードなので,1 MiB以下の領域に読み込みます。
3日目で取り上げたブートローダで使うメモリマップのうち使用しなかった0x00010000-0x00078fff
をカーネルに使います。
Start | End | Description |
---|---|---|
00000500 | 00007bff | ブートローダのスタック領域 |
00007c00 | 00007dff | MBR |
00007e00 | 00007fff | 未使用 |
00008000 | 00008fff | ブート情報(ドライブ番号など) |
00009000 | 0000cfff | ブートモニタ (16 KiB) |
0000d000 | 0000ffff | 未使用 (ブートモニタ拡張用に予約) |
00010000 | 0002ffff | カーネル (最大128 KiB) |
00030000 | 0006ffff | 予約 |
00070000 | 00073fff | 予約:trampoline (16 KiB) |
00074000 | 00075fff | 予約:GDT (8 KiB) |
00076000 | 00077fff | 予約:IDT (8 KiB) |
00078000 | 00078fff | 予約 |
00079000 | 0007ffff | ロングモード用ページテーブル (24 KiB = 6 * 4 KiB以上必要) |
上表の通り,0001000-0002ffff
の領域をカーネルに使います。
カーネルのロード
まず,ブートモニタにカーネルを上記のメモリ領域に読み込み処理を追加します。src/boot/bootmon.Sに記述したように,ブートモニタをMBRから読み込んだ処理と同様にして,
#define KERNEL_LBA 33
#define KERNEL_SIZE 0x20
#define KERNEL_SEG 0x1000
#define KERNEL_OFF 0x0000
load_kernel:
pushw %ax
pushw %bx
pushw %cx
pushw %dx
pushw %es
/* Load the kernel (at 33rd sector) to 0x1000:0x0000 */
movb drive,%dl
movw $KERNEL_SIZE,%cx
movw $KERNEL_LBA,%ax
movw $KERNEL_SEG,%bx
movw %bx,%es
movw $KERNEL_OFF,%bx
call read
popw %es
popw %dx
popw %cx
popw %bx
popw %ax
ret
とすることで,ディスクのセクタ33(0x4200-
)から32セクタ(16 KiB)分を0x1000:0x0000
(リニアアドレスで0x00010000
)からの領域に読み込みます。
メモリマップ表示プログラム(カーネル領域)
前回は起動メッセージとシステムメモリマップの表示をsrc/boot/boot.c
に実装しましたが,これをカーネル領域に移動します。カーネルのソースコードは以下の通りです。
また,ブートローダのsrc/boot/boot.c
から上で読み込んだカーネルのエントリポイントkentry
に移動するために,lretq
命令を使ったロングジャンプを以下のようにsrc/boot/entry64.S
に実装します。
.globl _ljmp
/* void ljmp(uint64_t cs, void *ip); */
_ljmp:
pushq %rdi
pushq %rsi
lretq
これをsrc/boot/boot.c
から呼び出します。64ビットモードのコードセレクタは0x08
,kentry
は0x00010000
なので,src/boot/boot.c
のコードは下記のようになります。
void ljmp(uint64_t, uint64_t);
/*
* Entry point for C code
*/
void
centry(void)
{
ljmp(0x08, 0x00010000);
}
ここまでのコードはgithub上ではday09_1
のタグを付けています。
仮想メモリとリロケーション
カーネルは慣例的に仮想メモリの3–4 GiB (0xc0000000-0xffffffff
)に置くことが多いです。歴史的に,32ビットのメモリ領域のハイメモリ部分をカーネルとして使ってきました。カーネルをこの仮想メモリの3–4 GiBの領域に再配置することをリロケーションと呼びます。
カーネルのリロケーションは,カーネル領域をコピーしたり,カーネル領域のページを再マップすることでできますが,ベースアドレスが変わるので,絶対アドレスでアクセスするような命令(例えば.data
セクションへのアクセス)は利用できません。ELFなどのローダブル形式であればリロケーションは容易ですが,ELFのローダを実装する必要があるので,今回はローダブル形式については扱いません。
リロケーションは必須ではないので,例えば下位1 GiBをカーネル領域で使うこともできます。現代の64ビット前提のOSでしたらこちらの方が綺麗かもしれませんが,他のUnix系OSとのユーザランドバイナリ互換(例えばMINIXはNetBSDとバイナリ互換にしています)とコンパイラなどが流用できるので,advosでもこの領域にカーネルを持ってきます。ただし,リロケーションではなく,ブートローダのページテーブルでマッピングします。このアドベントカレンダーで他のUnix系OSと互換にするところまでは絶対に行かないので,必要無いと言えば必要無いのですが……。
今回はリロケーションはせずにブートローダのページテーブルでカーネルの配置を行うので,0x00000000-0x00200000
の先頭2 MiBの物理領域を0xc0000000
の2 MiBページにマッピングするようにします。
まず,この領域でカーネルのバイナリが正しく実行できるように,kentry
が仮想メモリの0xc0010000
に配置されるようなバイナリを生成するために,リンカスクリプトsrc/kernel/kerne.ld
を以下のように0xc0010000
から始まるように書き換えます。
SECTIONS
{
. = 0xc0010000;
.text : { *(.text) }
.data : { *(.data) }
}
この状態で,src/boot/boot.c
からカーネルの領域(0x00010000
以降)にジャンプもできますが,バイナリの絶対アドレス(0xc0010000
以降)と不整合が起こるため,上述したsrc/kernel/kerne.c
の
print_str(base, "Welcome to advos (64-bit)!");
のように.data
セクションに入る文字列へのアクセスが上手く動かなくなります(この状態のコードは無限ループを起こす可能性もあるため危険です)。リロケーションはこのような絶対アドレスへのアクセスを行わないように注意して行う必要がありますので,非常に面倒です。今回はブートローダの段階でページテーブルを操作し,ジャンプ先を0xc0010000
に
して絶対アドレスとの不整合が起こらないようにします。
カーネル領域のページテーブル設定
src/boot/entry32.S
のページテーブル設定に以下を加えます。
/* Modify kernel page table */
movl %ebx,%edi
addl $(0x1000*5),%edi
movl $0x083,%eax
movl %eax,(%edi)
これにより,0xc0000000
からの2 MiBの仮想ページを物理メモリ0x00000000
に割り当てます。
ページテーブルの設定は以下のようになります。
pg_setup:
movl $KERNEL_PGT,%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 */
/* Level 4 page map */
leal 0x1007(%ebx),%eax
movl %eax,(%ebx)
/* 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
/* 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
/* Modify kernel page table */
movl %ebx,%edi
addl $(0x1000*5),%edi
movl $0x083,%eax
movl %eax,(%edi)
/* Set page table register */
movl %ebx,%cr3
本当はカーネル側でリロケーションをしたいのですが,メモリアロケータとのにわたま問題が起こるので,今回は手抜きでこのようにしました。
ここまでのコードはgithub上ではday09_2
のタグを付けています。
まとめと明日の予定
今日はカーネル領域のメモリへの読み込みをして,カーネルのコードに入りました。明日はメモリアロケータに入っていこうと思います。