前回、GRUBから読み込まれて 仮のGDTの設定が完了しましたので、 今回はページングを有効にしてロングモードに入り さらにCのカーネル本体を呼び出します。
今回の記事で作成したソースコードは こちら です。
開発マシンが Linux(x86_64) ならおそらく問題なく make が通ります。 それ以外の環境では x86_64-elf なクロスコンパイラ環境が必要です。 クロスコンパイル用のツール群をしかるべきディレクトリに配置して Makefileの宣言部だけを改造すれば大丈夫かと思います。
また、 GRUB2とQEMUがインストールされていれば
make run を実行すると
myOS.iso というブータブルCDイメージを生成して起動テストが できます。
と、いっても今回配布するデータはカーネル本体になんの処理も 書いていないので黒い画面にカーソルが点滅するだけですが。。。
ページング
ロングモードに入る前にページングを有効にする必要があります。
ページング方式では、膨大なメモリ空間を効率よく使用するために、 ページという小さな単位にメモリを区切って仮想メモリ、物理メモリを 管理します。
ページングでは、まず仮想的なメモリ空間を考えます。 x86_64では通常のマシンに搭載されているメモリ量を はるかに超える広大な仮想メモリ空間が考えられます。
これを4KBの倍数ごとの固定サイズのブロックに分割して、 分割したブロックを "ページ" と呼びます。
この仮想メモリのアドレスを用いてプログラムはメモリにアクセスしますが、 実際にデータを保存するのは当然 物理メモリ上 です。
各ページは必要になると、物理メモリと対応させて プログラムはページを通して物理メモリにアクセスすることができます。
各ページが対応する物理メモリは物理メモリ上のどこでもよい、 というよりHDDなど補助記憶装置でも構わない(いわゆるスワップ領域)でも 問題ないため、OSでページングを宜しく管理することで、 ユーザープログラムは物理メモリを意識することなくメモリアクセスが できるようになります。
今回作成中のカーネルはHigherHalf的な配置にするため、 起動時に仮想アドレスの
0x0000000000000000 - 0x0000000000200000 (Lower) 0xFFFFFFFF80000000 - 0xFFFFFFFF80200000 (Higher)
を両方物理メモリの先頭からストレートマップを行います。
カーネルが起動したら Lower のほうは開放して 0x0000000000000000 - 0xFFFFFFFF80000000 をユーザープログラムが使えるようにします。
OSdevによるとロングモードでのページングでは、膨大な メモリ空間を管理するため
4KB(4096byte) を1ページとして 512個のページ情報を格納する Page Table(PT) さらに 512個のPTを保持する Page Direcotry(PD) 512個のPDを保持する Page Direcotry Pointer(PDP) 最後に512個のPDPを保持する Page Map Level 4(PML4)
の4段階で仮想メモリを管理します。
また PDのPSビットを1にすれば PML4 > PDP> PDE で 2MB(4KB x 512)分のページを
PDPのPSビットを1にすれば PML4 > PDP で 1GB(2MBx512)分のページを一気に割り当て可能です。
data.S
.p2align 12
pre_pml4:
.zero PAGE_SIZE
.p2align 12
pre_pdpt_low:
.zero PAGE_SIZE
.p2align 12
pre_pdpt_high:
.zero PAGE_SIZE
.p2align 12
pre_pd:
.zero PAGE_SIZE
まずは data.S に必要な領域を確保しておきます。 各テーブルはちょうど1ページ分のサイズです。 pdpt_low / pdpt_high というのが 先ほどのマッピングの Low/High 用のPDにあたります。
各仮想アドレスと、 PML4等のインデックスは この画像のように表現されているので 以下のようにすると簡単に求められます。
#define PT_(vaddr) (((vaddr) >> (12) ) & 0x1FF)
#define PD_(vaddr) (((vaddr) >> (21) ) & 0x1FF)
#define PDPT_(vaddr) (((vaddr) >> (21+9) ) & 0x1FF)
#define PML4_(vaddr) (((vaddr) >> (21+9+9)) & 0x1FF)
以下 boot.S
#define ENTRY_SIZE 0x8
.extern pre_pml4
.extern pre_pdpt_low
.extern pre_pdpt_high
.extern pre_pd
movl $(pre_pdpt_low), %eax
orl $PAGE_PRESENT, %eax
movl %eax, pre_pml4
movl $(pre_pdpt_high), %eax
orl $PAGE_PRESENT, %eax
movl %eax, pre_pml4 + (ENTRY_SIZE * PML4_(K_VMA_BASE))
movl $pre_pd, %eax
orl $PAGE_PRESENT, %eax
movl %eax, pre_pdpt_low + (ENTRY_SIZE * PDPT_(0x0000000000000000))
movl %eax, pre_pdpt_high + (ENTRY_SIZE * PDPT_(K_VMA_BASE))
xorl %eax, %eax # physical_address 0x0 ...
orl $PAGE_2MB, %eax
orl $PAGE_WRITABLE, %eax
orl $PAGE_PRESENT, %eax
movl %eax, pre_pd + (ENTRY_SIZE * PD_(0x0000000000000000))
movl %eax, pre_pd + (ENTRY_SIZE * PD_(K_VMA_BASE))
これで必要なテーブルは揃いました。
あとはPML4(pre_pml4)を CR3レジスタに登録、 また、ロングモードのページングではPAEの有効が必須なので CR4レジスタを読んで PAEとPSEを有効にして書き戻します。
# Setup long mode page table
movl $(pre_pml4), %eax
movl %eax, %cr3
#enable PAE
movl %cr4, %eax # read Control register 4
orl $CR4_PAE, %eax # enable PAE
orl $CR4_PSE, %eax # enable PSE
movl %eax, %cr4 # re-write
これで準備完了です。 EFERよりロングモード、CR0よりページングを有効にして .code64に移行します。
#enable long mode
movl $EFER, %ecx
rdmsr # rdmsr <= ECXで指定されたMSRを EDX,EAXに読み込む
bts $8, %eax # LME bit = 1
wrmsr # wrmsr <= EDX,EAXの値をECXで指定されたMSRに書き込む
#enable Paging
movl %cr0, %eax
orl $CR0_PAGING, %eax
movl %eax, %cr0
ljmp $GDT_KC64, $.entry_long
長かったですがついに32bitの命令とはおさらばです。
.code64
.code64
.extern kmain
.entry_long:
# Setup Stack (Higher)
movq $(K_VMA_BASE), %rax
addq %rax, %rsp
# Setup data segment selectors
mov $GDT_KD64, %eax
mov %ax, %ds
mov %ax, %es
mov %ax, %ss
# Just in Case
pushq %rsi
pushq %rdi
movq $(K_VMA_BASE), %rax
addq %rax, %rsi
call EXT_C(kmain)
loop:
hlt
jmp loop
カーネルをHigherで実行できるように スタックのアドレスを更新します。 (K_VMA_BASE - 0x0) 分だけ 増加させればOKです。
その後データセグメントレジスタを$GDT_KD64で初期化しています。
これでCのカーネルを呼び出す準備が終わりました。
RSI,RDIにGRUBからの情報が退避されていますので RSIに保持されている multiboot_infoへのポインタもアドレスを 仮想アドレスに合わせておきます。
また、前回GCCではRSI,RDIがCの関数の引数として使われると 書いたものの、なんとなく気持ち悪いのでこれらをスタックに積んでから kmain() を呼び出すようにしています。
以上で全ての準備が終わりましたので C言語の関数 kmain() を呼び出します。
void kmain()
void kmain(){
bss_init();
}
とりあえず bss_init() だけ実行して終了します。
void bss_init()
{
uintptr_t* start = &_bss_start;
uintptr_t* end = &_bss_end;
while(start<end) *start++ = 0x0;
}
Cの規約で bss領域は 0 で初期化されていないといけないので 最初にクリアしておきます。
_bss_start _bss_end
は linker.ld にて宣言されているので
linker.h
#ifndef LINKER_H
#define LINKER_H 1
#include <darkhorse.h>
#include <stddef.h>
extern uintptr_t _rodata_start;
extern uintptr_t _rodata_end;
extern uintptr_t _data_start;
extern uintptr_t _data_end;
extern uintptr_t _bss_start;
extern uintptr_t _bss_end;
extern uintptr_t _heap_early_start;
extern uintptr_t _heap_early_end;
extern uintptr_t _kernel_start;
extern uintptr_t _kernel_end;
#endif /* LINKER_H */
こんなかんじのヘッダファイルを用意しておけば簡単に扱えます。
ということで今回の作業はここまでです。 ようやく Cの関数が読み込めました。。。
早速 make してみます。
起動してみるとこんなかんじです。 とくに何もせずに
loop:
hlt
jmp loop
に入っているので画面は、カーソルが点滅しているだけです。 が、停止はしてないのでうまくいっている模様です。
QEMUは Ctrl+Alt+2 に 情報モニタコンソールが開くので
info mem してみると
ちゃんとページングも有効になっているようです。 さらに
info registers も実行します
RDI = 0x3d76289 となっています。
ちゃんとGRUBのマジックナンバーもとれているようです。 他CS,DSなどの値も問題無さそうです。
ついでに kernel.elfを objdump -D して眺めてみます。
冒頭はこんなかんじです。
0x100000 からスタートして #define MULTIBOOT2_HEADER_MAGIC 0xe85250d6 が先頭に書き込まれているのがわかります。
もう少し後ろをみると
.text の bss_init や kmain は 0xFFFFFFFF80000000 ~ に 配置されているのが確認できます。
ということで画面になにも映らないので不安ですが 一応うまく動いているようです。
次回はとりあえずテキストVGA用の簡易ドライバを書いて ディスプレイに文字が表示できるようにします。