リアルモードからプロテクトモードへの変更


はじめに

この記事では、リアルモードで起動したOSがプロテクトモードに切り替わる処理を見ていきます。
また、セグメントやセグメントを取り扱うセグメントディスクリプタテーブル、GDTRなどの処理もコードを交えて解説します。

リアルモードとは

16bitのレジスタを使い、仮想メモリをサポートせず物理アドレスのみで動作するモードです。
リアルモードで使えるレジスタは、
  ax, bx, cx, dx, ip, sp, bp, si, di, flags
があります。
また、セグメントレジスタとして
  cs, ds, ss, es, gs, fs
があります。
リアルモードのセグメントとは、セグメントレジスタの値とオフセットから計算された物理アドレスです。
セグメントの意味はリアルモードとプロテクトモードで異なります。これは後ほど解説します。

リアルモードのアドレッシング

リアルモードのアドレッシングは、セグメントレジスタとオフセットを加算して行います。
図を用いて解説します。

レジスタ値:オフセット値 の形式でセグメントを指定します。例えば、2345:0100のように指定します。
セグメントベースには、セグメントレジスタが使われます。
セグメントレジスタ(cs, ds, ss, es, gs, fs)のうち、cs, ds, ssは用途が決まっています。
cs : Code Segment はCPUが実行するプログラムコードのセグメントを
ds : Data Segment は読み書きするデータのセグメントを
ss : Stack Segment はスタックのセグメントを指定します。
 
物理アドレスの計算式は、(セグメントベース * 16 + オフセット)です。
なぜセグメントベースを16倍するのかは、次の画像を参照してください。

8086のピンです。A0~A19までのピンがあるのがわかります。
メモリアクセスには、この20個のアドレスバスを使用できる事ができます。つまり、20bitでアドレス指定ができるということです。
そのため、セグメントベースを16倍して、20bitでのアクセスを行います。

プロテクトモードとは

プロテクトモードでは、32bitのアドレスバスを使用できます。
それだけでなく、プロテクションリングや仮想メモリ、セグメンテーションの改善が行われました。
プロテクトモードでも、セグメントレジスタ(cs, ds, ss, es, gs, fs)を使用します。
セグメントレジスタは32bitに拡張されている・・・と思いきや、16bitのままです。
しかし、セグメンテーションが改善されており、アドレス計算方法が全く別のものになっています。

プロテクトモードのアドレッシング

プロテクトモードのアドレッシングは、
1, セグメントレジスタがセグメントディスクリプタテーブルを参照
2, セグメントディスクリプタテーブルがセグメントを指定する
という二段階に分かれています。

セグメントディスクリプタテーブルには、LDTとGDTがあり、ほぼすべてのOSでGDTが採用されています。
GDTR(グローバルディスクリプタテーブルレジスタ)にGDTのセグメントディスクリプタテーブルのアドレスが格納されます
 
GDTのセグメントディスクリプタの構成は、以下の図です

重要なのは、BaseとType, DPLです
Baseはセグメントのベースアドレスを表し、Typeがセグメントのタイプ(読み書き実行の可or不可)を、DPLがセグメントのアクセスに必要なCPUの特権レベルを表します。

リアルモードからプロテクトモードへの移行

プロセッサがプロテクトモードに入るには、
1, メモリ上にGDTのセグメントディスクリプタを作成する
2, GDTRをセットアップする
3, レジスタcr0を1にする
4, ファージャンプを行う
の処理を行います。
 
実際に、GRUB2のコードを見ていきます。ソースコードはこちらにあります。
https://chromium.googlesource.com/chromiumos/third_party/grub2/+/11508780425a8cd9a8d40370e2d2d4f458917a73/grub-core/kern/i386/realmode.S
CPUをリアルモードからプロテクトモードに移行する処理は、real_to_protで行われています。
 
まずは、1, メモリ上にGDTのセグメントディスクリプタを作成する 処理を見ていきましょう。
81~97行目にGDTが定義されています。

gdt:
    .word   0, 0
    .byte   0, 0, 0, 0

    /* -- code segment --
     * base = 0x00000000, limit = 0xFFFFF (4 KiB Granularity), present
     * type = 32bit code execute/read, DPL = 0
     */
    .word   0xFFFF, 0
    .byte   0, 0x9A, 0xCF, 0

    /* -- data segment --
     * base = 0x00000000, limit 0xFFFFF (4 KiB Granularity), present
     * type = 32 bit data read/write, DPL = 0
     */
    .word   0xFFFF, 0
    .byte   0, 0x92, 0xCF, 0

コードセグメントは、00 CF 9A 00 00 00 FF FF
データセグメントは、00 CF 92 00 00 00 FF FF
であり、どちらもベースアドレスが0, セグメントの大きさは最大4GB, DPLは0です。
異なる点はタイプで、コードセグメントは実行と読みが許可、データセグメントは読み書きが許可されています。
 
133行~の、real_to_protを見ていきます。

real_to_prot:
    .code16
    cli
    /* load the GDT register */
    xorw    %ax, %ax
    movw    %ax, %ds
    DATA32  ADDR32  lgdt    gdtdesc
    /* turn on protected mode */
    movl    %cr0, %eax
    orl $GRUB_MEMORY_CPU_CR0_PE_ON, %eax
    movl    %eax, %cr0
    /* jump to relocation, flush prefetch queue, and reload %cs */
    DATA32  ljmp    $GRUB_MEMORY_MACHINE_PROT_MODE_CSEG, $protcseg
    .code32
DATA32  ADDR32  lgdt    gdtdesc

のlgdt命令で、2, GDTRをセットアップする を行い、
次に、

movl    %cr0, %eax
orl $GRUB_MEMORY_CPU_CR0_PE_ON, %eax
movl    %eax, %cr0

で 3, レジスタcr0を1にする を行います。
最後に

DATA32  ljmp    $GRUB_MEMORY_MACHINE_PROT_MODE_CSEG, $protcseg

で 4, ファージャンプを行う を完了します。
これにより、CPUがリアルモードからプロテクトモードになります。

参考書籍

低レベルプログラミング
新装改訂版 Linuxのブートプロセスをみる