要約
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
の場合と変更ありません.