RamenOSにページングを実装した
要約
RamenOSにページングを実装しました.はりぼてOSを改造してページングを実装する場合,ページディレクトリ,ページテーブルの領域を確保し,GDTのエントリを追加又は編集し,保護モード移行直後にfar JMPをし,CR3,CR0レジスタに適切な値を代入し,リンカスクリプトの各アドレスやスタックポインタの初期アドレス,カーネルへの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バイト使用するため,カーネルを0x00501000,IDTを0x00581000に配置します.尚,ページング移行後は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を確認してください.
コード
この記事では,以下のツリーを基に解説を行ないます.
手順
カーネルの.headセグメントを削除する(任意)
リンカスクリプトを編集すればこの工程は不要の筈ですが,機械語コードがカーネルの先頭にある方が簡単なため,カーネルの.headセグメントを削除しました.やり方はこの記事で確認してください.
paging.asmを作成し,head.asmにインクルードする
%include "paging.asm"
インクルードする場所は,カーネルへジャンプする直前です.インクルードの方法についてはこの記事で確認してください.
カーネル,IDTの場所を変更する
前述したように,カーネルとIDTの場所を変更する必要があります.実際の手順はこの記事で確認してください.
paging.asmを書く
ここではコードの補足をします.
ページディレクトリ,ページテーブルの配置場所を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を確認してください.IDT用のエントリを登録する
スタック用のエントリを登録する
スタックの大きさは4KB × 32 = 128KBとしています.これは任意で変更してください.但し変更した場合,後に設定するスタックポインタの初期値を変更しなければなりません.1MB未満の領域の物理アドレスと仮想アドレスを一致させる
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も仮想アドレスでアクセスする必要があります.CR3にページディレクトリのベースアドレスを代入し,CR0の最上位ビットを1にする
コードセグメントを追加する
次のfar jmpを保護モード移行直後に行なうで必要です.解説はそちらで行ないます.追加するコードセグメントの中に現在実行中の機械語コードが含まれている必要があります.はりぼてOSのbootpack用のエントリを変更しても構いません.
DW 0xffff,0x0000,0x9a00,0x00cf ; Executable 32bit
エントリの各ビットの持つ意味に関してはこちらを確認してください.
また,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 | ディスアセンブルされた命令 |
大きさ,形式の情報はこちらを基にしました.
x /AAA BBB
xp /AAA BBBのBBBが仮想アドレスになったものです.AAAの形式はxpの場合と変更ありません.