起動するまでの長い道のり プロテクト・モード移行編(2) セグメンテーション始動の巻

 だいぶ間が空いてしまった(汗)。いよいよ実際にセグメンテーションを動かして、32ビットコードを実行するプロテクト・モードへ移行する。64KBの壁よさらば。

今回のソース(setup.s)

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の倍数のアドレスに合わせる必要があるのでアライメントを行った。

移行する

 いよいよプロテクト・モードに移行する。ここで前回書いた移行手順をおさらいする。

  1. ディスクリプタ・テーブルをメモリ上に用意する。
  2. ディスクリプタ・テーブルの位置を示す構造体をメモリ上に用意する。
  3. ディスクリプタ・テーブルの位置をそれ専用の命令で指定する。
  4. 制御レジスタCR0の0ビット目を1にする。
  5. ジャンプ命令でパイプラインをリセットする。
  6. コード・セグメントをジャンプ命令で設定する。
  7. データ・セグメントを設定する。

 で、実は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言語メタプログラミング機能とか簡潔な構文に慣れるともう元に戻れないのよ……。

参考資料

詳解 Linuxカーネル 第3版

詳解 Linuxカーネル 第3版

はじめて読む486―32ビットコンピュータをやさしく語る

はじめて読む486―32ビットコンピュータをやさしく語る