1から創る自作OS x86_64 02h

前回、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用の簡易ドライバを書いて
ディスプレイに文字が表示できるようにします。

コメントを追加する