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のタグを付けています。
まとめと明日の予定
今日はカーネル領域のメモリへの読み込みをして,カーネルのコードに入りました。明日はメモリアロケータに入っていこうと思います。