備忘録やめた

備忘録として使用していたけどやめた.このブログに載せてあるコードのライセンスは別途記載がない限りWTFPL OR NYSLです.

RamenOSにページングを実装した

要約

RamenOSにページングを実装しました.はりぼてOSを改造してページングを実装する場合,ページディレクトリ,ページテーブルの領域を確保し,GDTのエントリを追加又は編集し,保護モード移行直後にfar JMPをし,CR3CR0レジスタに適切な値を代入し,リンカスクリプトの各アドレスやスタックポインタの初期アドレス,カーネルへのJMP命令のアドレスを物理アドレスから仮想アドレスに変更,そしてカーネル内のGDTの設定コードを削除する必要があります.デバッグの際はJMP $や,QEMUデバッグコンソールが便利です.

はじめに

はりぼてOSを基にしているRamenOSにページングを実装しました.

ページディレクトリ,ページテーブル等の配置

ページディレクトリを物理メモリの1MBの地点*1から開始し,ページディレクトリの直後にページテーブルを配置します.1MB未満にはVRAMや,BIOSが使用する領域があるため,1MB地点以上に配置すると確実です.

32ビットOSの場合,ページディレクトリは高々1個,ページテーブルは高々1024個存在します.ページディレクトリまたはページテーブルの各エントリの大きさは4バイトなので,1つのページディレクトリまたは1つのページテーブルの大きさは4 × 1024 = 4KBです.従って全部のページディレクトリとページテーブルの大きさの合計は4 × (1 + 1024) = 4MB + 4KBです.

ページディレクトリとページテーブルがメモリを4MB + 4KB = 0x00401000バイト使用するため,カーネル0x00501000IDT0x00581000に配置します.尚,ページング移行後はGDTを使用しません.また,スタック領域を0x00582000より128KB確保します.

マッピング

仮想アドレス0x10000 - 0xBFFFFFFFをアプリケーション用に,仮想アドレス0xC0000000 - 0xFFFFFFFFをシステム用に使用します.

カーネルは0xC0000000- 0xC007FFFFにマッピングします.その直後,0xC0080000 - 0xC0080FFFにIDTマッピングします*2.スタックは0xC0081000 - 0xC00A0FFFにマッピングします.VRAMのマッピングは0xC0400000を始点とします.ページディレクトリ上では,カーネル用のページディレクトリのエントリの直後にVRAMのエントリがマッピングされます.

1MB未満の領域は物理アドレスと仮想アドレスを一致させます.ページングを有効にしていても,物理アドレス0xCAFEへは仮想アドレス0xCAFEでアクセスできるようにします.この操作を実施しないと,ページングを有効にした直後,命令ポインタが不正なアドレスを指してしまいOSがクラッシュします.IPLがhead.asmの内容を1MB未満の領域にコピーしているためです.

アライメント

ページディレクトリ,ページテーブル,4KBページの開始アドレスは全て4KBでアライメントされている必要があります.つまり,これらの開始アドレスの下位12ビットは0x000となっていなければなりません.詳しくはIntel® 64 and IA-32 Architectures Software Developer ManualsのVolume 2, 4-3 Figure 4-4を確認してください.

コード

この記事では,以下のツリーを基に解説を行ないます.

github.com

手順

カーネル.headセグメントを削除する(任意)

リンカスクリプトを編集すればこの工程は不要の筈ですが,機械語コードがカーネルの先頭にある方が簡単なため,カーネル.headセグメントを削除しました.やり方はこの記事で確認してください.

tokuchan3515.hatenablog.com

paging.asmを作成し,head.asmにインクルードする

    %include "paging.asm"

インクルードする場所は,カーネルへジャンプする直前です.インクルードの方法についてはこの記事で確認してください.

tokuchan3515.hatenablog.com

カーネルIDTの場所を変更する

前述したように,カーネルIDTの場所を変更する必要があります.実際の手順はこの記事で確認してください.

tokuchan3515.hatenablog.com

paging.asmを書く

ここではコードの補足をします.

  1. ページディレクトリ,ページテーブルの配置場所を0で初期化する
    誤作動を起こさないための処置です.
    STOSD命令は,EAXレジスタの値をES:EDI(レガシーモードの場合),あるいは,RDIまたはEDI(64ビットモードの場合)に代入されているアドレスに格納します.REP命令と一緒に使用することで,ES:EDI等を先頭にECX × 4バイト,EAXで埋めることが出来ます.詳しくはIntel® 64 and IA-32 Architectures Software Developer ManualsのVolume 2, 4-3を確認してください.

  2. カーネル用のマッピングをする

  3. IDT用のエントリを登録する

  4. スタック用のエントリを登録する
    スタックの大きさは4KB × 32 = 128KBとしています.これは任意で変更してください.但し変更した場合,後に設定するスタックポインタの初期値を変更しなければなりません.

  5. 1MB未満の領域の物理アドレスと仮想アドレスを一致させる

  6. VRAM用のマッピングをする
    MUL命令は積を計算します.オペラントの大きさがBYTEの場合,オペラントとALの積を計算し,結果をAXに代入します.オペラントの大きさがWORDの場合,オペラントとAXの積を計算し,結果の低い桁をAX,高い桁をDXに代入します.オペラントの大きさがDWORDの場合,オペラントとEAXの積を計算し,結果の低い桁をEAX,高い桁をEDXに代入します.詳しくはIntel® 64 and IA-32 Architectures Software Developer ManualsのVolume 2, 4-3を確認してください.
    VRAMの先頭へのポインタの仮想アドレスを,忘れずにメモリ上に記憶してください.VRAMの物理アドレスと仮想アドレスを一致させない限り,VRAMも仮想アドレスでアクセスする必要があります.

  7. CR3にページディレクトリのベースアドレスを代入し,CR0の最上位ビットを1にする

コードセグメントを追加する

次のfar jmpを保護モード移行直後に行なうで必要です.解説はそちらで行ないます.追加するコードセグメントの中に現在実行中の機械語コードが含まれている必要があります.はりぼてOSのbootpack用のエントリを変更しても構いません.

    DW       0xffff,0x0000,0x9a00,0x00cf ; Executable 32bit

エントリの各ビットの持つ意味に関してはこちらを確認してください.

tokuchan3515.hatenablog.com

また,GDTに新たにエントリを追加した場合,GDTのlimit値も変更してください.

GDTR0:
    DW       8*3-1
    DD       GDT0

far jmpを保護モード移行直後に行なう

Intel® 64 and IA-32 Architectures Software Developer ManualsのVolume 3, 9.9.1より引用.

Immediately following the MOV CR0 instruction, execute a far JMP or far CALL instruction. (This operation is typically a far jump or call to the next instruction in the instruction stream.)

文中のMOV CR0は,32ビット保護モードに移行する際のMOV CR0, EAXです.このMOV命令を実行した直後,far JMPまたはfar CALLを実行しろというのがこの引用文の趣旨です.far JMPは別のセグメントへのジャンプであり,far CALLは別のセグメントに位置する関数の実行をします.

Intelのマニュアルに従って,保護モード移行直後にfar JMPをすることにします.ジャンプ先はコードセグメントである必要があります.

    MOV      CR0,EAX
    JMP      CODE_SEGMENT:pipelineflush

    [BITS 32]
pipelineflush:

CODE_SEGMENTは定数です.

JMP後,セグメントレジスタにデータセグメントに対応する値を代入する必要がありますが,データセグメントのインデックスが変わった場合,正しい値に直してください.例えばデータセグメントがGDTの2番目に登録されている場合,セグメントレジスタ2*8を代入する必要があります.

ところで,はりぼてOSの場合はどうでしょうか.はりぼてOSでは,MOV CR0, EAXで32ビット保護モードに移行した後,パイプラインを空にするためにJMP命令を実行しています.

    MOV    CR0,EAX
    JMP    pipelineflush
pipelineflush:

但し,このJMP命令はfar JMPではなくshort JMPです*3.これはndisasmでディスアセンブルすると分かります.

00000045  0F20C0            mov eax,cr0
00000048  6625FFFFFF7F      and eax,0x7fffffff
0000004E  6683C801          or eax,byte +0x1
00000052  0F22C0            mov cr0,eax
00000055  EB00              jmp short 0x57

はりぼてOSのfar JMPは,asmhead.nasの一番最後のJMP命令で行われます.このJMP命令では,ジャンプ先のセグメントが明示されています.

    JMP    DWORD 2*8:0x0000001b

MOV CR0での保護モード移行とfar JMPの間に数多くの命令が含まれているのにも関わらず,OSの実行に失敗しないのが不思議です.

リンカスクリプトを修正する

カーネルが仮想アドレス0xC0000000に位置するようになったため,リンカスクリプトにその旨を記述します.

    .text 0xC0000000 : { 
        *(.text.os_main)
        *(.text*)
    }

スタックポインタの初期値変更

スタックポインタの初期値を仮想アドレスに変更します.

    MOV      ESP,0xC00A0FFF                ; スタック初期値

カーネルへのJMPのアドレス変更

カーネルのアドレスを仮想アドレスに変更します.

    JMP      0xC0000000

カーネルソースコードからGDT関係を削除する

ページングを有効にしたので,GDTは使用しません.GDTの初期化に関するコードを削除します.

カーネルソースコードの各アドレスの変更

カーネルIDTのアドレスを仮想アドレスに変更する必要があります.

デバッグ

JMP $

Cのwhile(1);やRustのloop{}と同じで,無限ループを起こします.これを利用して,クラッシュするコードを見つけ出すことが出来ます.

QEMUデバッグコンソール

QEMUを実行する際,-monitor stdioを実行オプションとして追加すると,使用しているターミナルにデバッグコンソールが出現します.QEMU実行後にCtrl + Alt + 2を押してデバッグコンソールを開くことも出来ますが,ターミナルで開くほうがコピペが効いたり表示をスクロールすることが出来るのでオプションを付加することをおすすめします.

デバッグコンソールで有用なコマンドを紹介します.尚,デバッグコンソールではTab補完が有効なため,全部の文字を入力する必要はありません.

info registers

全てのレジスタの値を表示します.

info mem

メモリの使用状況を表示します.仮想アドレスで表記されます.ページングが無効の場合,PG disabledと表示されます.

info tlb

ページングが有効になっている場合,仮想アドレスと物理アドレスの対応を表示します.ページングが無効の場合,PG disabledと表示されます.

xp /AAA BBB

物理アドレスBBBから始まる大きさAAAのメモリの内容を表示します.AAAは数字 + 大きさ + 形式です.

大きさ 意味
b 8ビット
h 16ビット
w 32ビット
g 64ビット
形式 意味
x 16進数
d 10進数
u 非負10進数
o 8進数
c 文字表示
i ディスアセンブルされた命令

大きさ,形式の情報はこちらを基にしました.

en.wikibooks.org

x /AAA BBB

xp /AAA BBBのBBBが仮想アドレスになったものです.AAAの形式はxpの場合と変更ありません.

参考文献

software.intel.com

*1:アドレスだと0x00010000

*2:IDTの大きさは2KBだが,ページテーブルの1つのエントリは物理メモリ4KB分に対応するため

*3:short JMPはIntel® 64 and IA-32 Architectures Software Developer ManualsのVolume 2A, 3.2のJMPの項によれば, (現在のEIPレジスタの値) - 128 〜 (現在のEIPレジスタの値) + 127の範囲に限られた,現在のコードセグメント内のジャンプである.