起動するまでの長い道のり プロテクト・モード移行編(2) セグメンテーション始動の巻
だいぶ間が空いてしまった(汗)。いよいよ実際にセグメンテーションを動かして、32ビットコードを実行するプロテクト・モードへ移行する。64KBの壁よさらば。
GDTの用意
前回説明したとおり、セグメンテーションを行うためには、各セグメントのサイズや特性を記した構造体(セグメント・ディスクリプタ)が必要になる。その構造体を納める領域を用意して、セグメント・ディスクリプタを書き込まなければならない。
セグメント・ディスクリプタを置く領域をGDT(グローバル・ディスクリプタ・テーブル)という。これはセグメント・ディスクリプタの配列だ。今回は以下のようなGDTを用意する。
GDTインデックス | ベース・アドレス | セグメント・リミット | 特権レベル | タイプ | 説明 |
---|---|---|---|---|---|
0 | 0x0000_0000 | 0x0_0000 | 0 | - | 空のディスクリプタ。0で埋める。必須 |
1 | 0x0000_0000 | 0xf_ffff(*4K) | 0(最高) | 実行可能 | カーネル・コード・セグメント |
2 | 0x0000_0000 | 0xf_ffff(*4K) | 0(最高) | 読み書き可能 | カーネル・データ・セグメント |
ベース・アドレスとセグメント・リミットを見れば分かるとおり、コードもデータも0〜4GBのメモリ領域全体を指定している。いわば重なっている。実はセグメント同士重なっているように指定することもできるのだ! あと物理メモリが無いところまでリミットを指定しても問題ない。アクセスしなければ文句は言われない。
さてこれをコードに埋め込む。GDTの内容は決まりきっているので、わざわざ実行時に値を設定する必要なんてない。ソースにデータを埋め込んでしまえばOKだ。
# setup.s (52行目) .align 8 # GDT。 gdt: # 空ディスクリプタ。 gdt_null: .word 0x00 .word 0x00 .byte 0x00 .byte 0x00 .byte 0x00 .byte 0x00 # カーネル・コード。 gdt_kernel_cs: .word 0xffff .word 0x00 .byte 0x00 .byte 0x98 .byte 0xdf .byte 0x00 # カーネル・データ。 gdt_kernel_ds: .word 0xffff .word 0x00 .byte 0x00 .byte 0x92 .byte 0xdf .byte 0x00 # GDT終端。 gdt_end:
先頭の「.align 8」という擬似命令は、次のラベルや命令のアドレスを8の倍数に合わせると言う意味だ。いわゆるアライメントというやつだ。GDTは始点を8の倍数のアドレスに合わせる必要がある。
セグメントはあまり活用しないつもりなので、とりあえず無茶苦茶簡単にこんな感じで済ませる。
GDTRの用意
セグメンテーションをOnにするとき、GDTだけではまだ足りない。実はGDTの開始位置と終了位置を示す構造体を作り、それをlgdtという命令でレジスタに設定してやる必要がある。その開始位置と終了位置を設定するレジスタのことをGDTRと言う。そのために用意する構造体もGDTRと呼んでしまうことにする。
GDTRの構造は以下の通りだ。
アドレス | サイズ(byte) | 内容 |
---|---|---|
0x0000 | 2 | GDTのリミットを示す。GDTの最後の有効バイトの位置(つまりGDT終端-GDT始点-1) |
0x0002 | 4 | GDTの位置を示す。GDTの始点のアドレス |
これもソースにデータを埋め込めばすぐに作れる。
# setup.s (45行目) .align 8 # GDTR。 gdtr: gdtr_limit: .word gdt_end - gdt - 1 gdtr_base: .long gdt
GDTRも8の倍数のアドレスに合わせる必要があるのでアライメントを行った。
移行する
いよいよプロテクト・モードに移行する。ここで前回書いた移行手順をおさらいする。
- ディスクリプタ・テーブルをメモリ上に用意する。
- ディスクリプタ・テーブルの位置を示す構造体をメモリ上に用意する。
- ディスクリプタ・テーブルの位置をそれ専用の命令で指定する。
- 制御レジスタCR0の0ビット目を1にする。
- ジャンプ命令でパイプラインをリセットする。
- コード・セグメントをジャンプ命令で設定する。
- データ・セグメントを設定する。
で、実は1と2は既に終わった。コードを読み込めば自然とメモリ上にデータ構造が出来ているはずだ。というわけで、3〜7を一気にやってしまおう。
# setup.s (10行目) # 割り込み禁止。 cli # GDTリミットのロード。(3) lgdt gdtr # プロテクトモードへ移行。(4) movl %cr0, %eax orl $0x01, %eax movl %eax, %cr0 # パイプラインのリセット。(5) jmp reset_pipeline reset_pipeline: # コード・セグメント設定。カーネル・コードへ。(6) ljmp $0x08, $set_cs set_cs: # ここからは32ビットコード。 .code32 # 各データ・セグメント設定。カーネル・データへ。(7) movw $0x10, %ax movw %ax, %ds movw %ax, %es movw %ax, %ss # スタック・ポインタ設定。 movl $stack_begin, %esp # 止まる。 hlt
(3)のlgdtが、先述のGDTRレジスタを設定する命令で、これによりGDTが参照されるようになる。
(4)で制御レジスタCR0にビットを立て、プロテクト・モードに移行している。eaxとかmovlとか32ビットのレジスタと命令を使っていることに注意。実は今まで黙っていたけど(ヲイ)、リアル・モード中でも32ビットのレジスタや命令は使えたりしました。まあもうプロテクト・モードがそこまで来ているから良いじゃないか!
(5)でリアル・モードのまま実行されてしまった恐れのあるパイプライン内の命令をリセットする。
.code32という擬似命令は、この先は32ビットコードを生成するようアセンブラに指示している。コード・セグメントを設定した後なので、もう祝32ビットの世界なのだ!
(6)(7)でセグメント・レジスタにセグメント・セレクタを設定している。0x08や0x10とかいう値は、GDTの開始アドレスからセグメント・ディスクリプタまでのオフセットだ。空のディスクリプタを抜かした最初のディスクリプタが0x08バイト目にあり、次のは0x10バイト目にある。ここではそれを指定している。
これでhlt命令があるところ以降では32ビットコードが普通に動く。今はhltで止まるだけだけど、例えば普通のC言語コンパイラでコンパイルした関数にジャンプできてしまったりするのだ! C言語でOSを作る場合はここでもうアセンブラとはさよならできる。(でも細かいところで使うな、きっと)
次回予告
プロテクト・モードへの以降が完了した。次回はついに念願のD言語呼び出しでも行ってみようと思う。ただ標準ライブラリの書き直しとかを考えると頭が痛いなあ……。D言語そのものの解説とかどうするか……。
本文でも少し書いたけど、C言語を使う場合はこれまでのコード+C言語のソースで結構簡単に書き始められると思う。C言語はライブラリへの依存度も低いしD言語よりかなり簡単だろう。はっきり言って誘惑される(笑)。でもD言語のメタプログラミング機能とか簡潔な構文に慣れるともう元に戻れないのよ……。
参考資料
- 作者: Daniel P. Bovet,Marco Cesati,高橋浩和,杉田由美子,清水正明,高杉昌督,平松雅巳,安井隆宏
- 出版社/メーカー: オライリー・ジャパン
- 発売日: 2007/02/26
- メディア: 大型本
- 購入: 9人 クリック: 269回
- この商品を含むブログ (73件) を見る
- 作者: 蒲地輝尚
- 出版社/メーカー: アスキー
- 発売日: 1994/09
- メディア: 単行本
- 購入: 20人 クリック: 165回
- この商品を含むブログ (83件) を見る