Skip to content
kamaboko edited this page Mar 19, 2024 · 19 revisions

GDTとは

GDT(Global Descriptor Table)とは、x86プロセッサにおいて、あるメモリ領域の扱い方やプログラムの実行方法を決めるための情報を記録しているテーブルです。
GDTには8byte単位で、メモリ保護やタスクに関する情報が管理されます。
GDTは通常CPUに1つだけ用意され、lgdt命令により読み込まれます。
また、後述するSegment Descriptorはプロテクトモードへの遷移に必須で、GDTに含まれます。
GDTを設定したあとCR0レジスタのPE bitをセットすることにより、プロテクトモードが有効になります。

Segment Descriptor

プロテクトモードでは「セグメント」と単位でメモリを切り分けて、メモリ空間を管理します。
このセグメントにより「メモリ保護」が実現されます。
もしメモリ保護がない場合、複数のアプリケーションが実行されているとき、あるアプリケーションが別のアプリケーションのデータ領域を参照できてしまいます。
別のアプリケーションやOSのデータを破壊してしまったり、秘匿しておきたいデータに自由にアクセスできてしまうのは問題です。
そこで出てくる考え方が「メモリ保護」で、あるアプリケーションのアクセス可能なメモリの領域を予め定義しておき、それ以外の空間にはアクセスしようとした場合にはエラーを起こすようにします。(一般的にはこのあと、このアプリケーションを強制終了させます)
このメモリ保護はセグメント単位で行われ、各セグメントにはそのメモリの扱い方を設定します。
(その領域に存在するデータをプログラムとして実行可能であるかどうか、データを読み書きできる権限レベルなど) このセグメントを設定するためのデータ構造がSegment Descriptorです。

Segment Descriptorの構造

63                                 48  47                                    32
+----------------------------------------------------------------------------+ 
|                |         |         |                   |                   |
|     base_h     |  Flags  | limit_h |    Access Byte    |     base_m        |
|        8       |   4     |   4     |        8          |         8         | 
+----------------------------------------------------------------------------+ 
31                                 16 15                                     0
+----------------------------------------------------------------------------+ 
|                                    |                                       |
|                base_l              |                limit_l                |
|                 16                 |                  16                   |
+----------------------------------------------------------------------------+ 

Access Byte

8    7     5   4     0
+---+-----+---+------+
| P | DPL | S | TYPE |
+---+-----+---+------+

Flags

4   3     2   1     0
+---+-----+---+-----+
| G | D/B | 0 | AVL |
+---+-----+---+-----+
  • base_h, base_m, base_l
    • セグメントの開始アドレスを指定します。32bitを分割して、[31:24][24:16][15:0]bitで設定します。
  • limit_hi, limit_l
    • セグメントのサイズを指定します。20bitを分割して、[29:16][15:0]bitで指定します。
  • Flags
  • Access Byte:

GDTRの構造

WIP

TSS Descriptor

TSS DescriptorはTSSのアドレスを示すデータ構造です。
TSSはハードウェアによるタスクスイッチや、割り込み時のスタックの切り替えに使用されるデータ構造です。
(Uroborosでは後者の用途でのみ使用しています)
データ構造としては、Segment Descriptorと同じです。
typeに9(1001)を指定すると、32bit TSSを示すTSS Descriptorになります。
また、baseにはTSSのアドレス、limitにはTSSのサイズを指定します。
TSSの詳しい説明は、InterruptのTSSの項目を参照してください。

GDTのセットアップ

uroborosでは、セグメントによるメモリ管理はページングで実装し、セグメントは使用しません。
しかし、プロテクトモードはセグメントが必須になるため、すべてのメモリ領域を指定したセグメントを作成します。

GDTエントリ作成

uroborosではプロテクトモードに遷移するまでの処理はアセンブラで実装されています。
C言語に遷移するのはプロテクトモードに入ってからです。
そのため、プロテクトモードに入るための最初のGDTの設定はアセンブラで記述する必要があります。
この最初に設定するGDTはあくまでも最低限C言語によるプログラムを動かすだけの仮のものです。
プロテクトモードに入った後、OSとして必要な正式なGDTをC言語で改めて設定します。

アセンブラ

Bootの項目で説明しています。

C言語

プロテクトモード有効化後、正式なGDTをC言語で改めて設定していきます。
まずはGDTの構造体を作ります。

typedef struct GDT{
    uint16_t limit_low;
    uint16_t base_low;
    uint8_t base_mid;
    uint16_t type: 4;
    uint16_t s: 1;
    uint16_t dpl: 2;
    uint16_t p: 1;
    uint16_t limit_high: 4;
    uint16_t avl: 1;
    uint16_t zero: 1;
    uint16_t db: 1;
    uint16_t g: 1;
    uint8_t base_high;
} __attribute__((__packed__))GDT;

GDTをセットアップする関数です。
0番目のエントリはnull descriptorと呼ばれ、すべて0を入れます。
1番目以降に、カーネル用・アプリケーション用のデータセグメントとコードセグメントをそれぞれ設定します。
各セグメントの役割ごとに適切にフラグやアクセスバイトを設定します。

#define GDT_SEGNUM_NULL 0            
#define GDT_SEGNUM_KERNEL_DATA 1     
#define GDT_SEGNUM_KERNEL_CODE 2     
#define GDT_SEGNUM_APP_DATA 3        
#define GDT_SEGNUM_APP_CODE 4        
#define GDT_SEG_COUNT 5                  

#define GDT_TYPE_DATA 0 << 3        
#define GDT_TYPE_CODE 1 << 3        
#define GDT_TYPE_CODE_CONFORM 1 << 2
#define GDT_TYPE_DATA_DOWN 1 << 2   
#define GDT_TYPE_CODE_RE 1 << 1     
#define GDT_TYPE_DATA_RW 1 << 1     

void init_gdt(GDT_SEG_DESC *gdt, GDTR *gdtr){
    //null descripter
    gdt[GDT_SEGNUM_NULL].limit_low = 0;
    gdt[GDT_SEGNUM_NULL].limit_high = 0;
    gdt[GDT_SEGNUM_NULL].base_low = 0;
    gdt[GDT_SEGNUM_NULL].base_mid = 0;
    gdt[GDT_SEGNUM_NULL].base_high = 0;
    gdt[GDT_SEGNUM_NULL].type = 0;
    gdt[GDT_SEGNUM_NULL].s = 0;
    gdt[GDT_SEGNUM_NULL].dpl = 0;
    gdt[GDT_SEGNUM_NULL].p = 0;
    gdt[GDT_SEGNUM_NULL].avl = 0;
    gdt[GDT_SEGNUM_NULL].zero = 0;
    gdt[GDT_SEGNUM_NULL].db = 0;
    gdt[GDT_SEGNUM_NULL].g = 0;

    //kernel data segment
    gdt[GDT_SEGNUM_KERNEL_DATA].limit_low = 0xffff;
    gdt[GDT_SEGNUM_KERNEL_DATA].limit_high = 0xf;
    gdt[GDT_SEGNUM_KERNEL_DATA].base_low = 0;
    gdt[GDT_SEGNUM_KERNEL_DATA].base_mid = 0;
    gdt[GDT_SEGNUM_KERNEL_DATA].base_high = 0;
    gdt[GDT_SEGNUM_KERNEL_DATA].type = GDT_TYPE_DATA | GDT_TYPE_DATA_RW; //(0010) ビットフィールド内はMSBを左として書けば良い
    gdt[GDT_SEGNUM_KERNEL_DATA].s = 1;
    gdt[GDT_SEGNUM_KERNEL_DATA].dpl = 0;
    gdt[GDT_SEGNUM_KERNEL_DATA].p = 1;
    gdt[GDT_SEGNUM_KERNEL_DATA].avl = 0;
    gdt[GDT_SEGNUM_KERNEL_DATA].zero = 0;
    gdt[GDT_SEGNUM_KERNEL_DATA].db = 1;
    gdt[GDT_SEGNUM_KERNEL_DATA].g = 1;

    //kernel code segment
    gdt[GDT_SEGNUM_KERNEL_CODE].limit_low = 0xffff;
    gdt[GDT_SEGNUM_KERNEL_CODE].limit_high = 0xf;
    gdt[GDT_SEGNUM_KERNEL_CODE].base_low = 0;
    gdt[GDT_SEGNUM_KERNEL_CODE].base_mid = 0;
    gdt[GDT_SEGNUM_KERNEL_CODE].base_high = 0;
    gdt[GDT_SEGNUM_KERNEL_CODE].type = GDT_TYPE_CODE | GDT_TYPE_CODE_RE;
    gdt[GDT_SEGNUM_KERNEL_CODE].s = 1;
    gdt[GDT_SEGNUM_KERNEL_CODE].dpl = 0;
    gdt[GDT_SEGNUM_KERNEL_CODE].p = 1;
    gdt[GDT_SEGNUM_KERNEL_CODE].avl = 0;
    gdt[GDT_SEGNUM_KERNEL_CODE].zero = 0;
    gdt[GDT_SEGNUM_KERNEL_CODE].db = 1;
    gdt[GDT_SEGNUM_KERNEL_CODE].g = 1;
    
    //user data
    gdt[GDT_SEGNUM_APP_DATA].limit_low = 0xffff;
    gdt[GDT_SEGNUM_APP_DATA].limit_high = 0xf;
    gdt[GDT_SEGNUM_APP_DATA].base_low = 0;
    gdt[GDT_SEGNUM_APP_DATA].base_mid = 0;
    gdt[GDT_SEGNUM_APP_DATA].base_high = 0;
    gdt[GDT_SEGNUM_APP_DATA].type = GDT_TYPE_DATA | GDT_TYPE_DATA_RW;
    gdt[GDT_SEGNUM_APP_DATA].s = 1;
    gdt[GDT_SEGNUM_APP_DATA].dpl = 3;
    gdt[GDT_SEGNUM_APP_DATA].p = 1;
    gdt[GDT_SEGNUM_APP_DATA].avl = 0;
    gdt[GDT_SEGNUM_APP_DATA].zero = 0;
    gdt[GDT_SEGNUM_APP_DATA].db = 1;
    gdt[GDT_SEGNUM_APP_DATA].g = 1;

    //user code segment
    gdt[GDT_SEGNUM_APP_CODE].limit_low = 0xffff;
    gdt[GDT_SEGNUM_APP_CODE].limit_high = 0xf;
    gdt[GDT_SEGNUM_APP_CODE].base_low = 0;
    gdt[GDT_SEGNUM_APP_CODE].base_mid = 0;
    gdt[GDT_SEGNUM_APP_CODE].base_high = 0;
    gdt[GDT_SEGNUM_APP_CODE].type = GDT_TYPE_CODE | GDT_TYPE_CODE_RE | GDT_TYPE_CODE_CONFORM;
    gdt[GDT_SEGNUM_APP_CODE].s = 1;
    gdt[GDT_SEGNUM_APP_CODE].dpl = 3;
    gdt[GDT_SEGNUM_APP_CODE].p = 1;
    gdt[GDT_SEGNUM_APP_CODE].avl = 0;
    gdt[GDT_SEGNUM_APP_CODE].zero = 0;
    gdt[GDT_SEGNUM_APP_CODE].db = 1;
    gdt[GDT_SEGNUM_APP_CODE].g = 1;

    //GDTR
    gdtr->size = GDT_SEG_COUNT * sizeof(GDT_SEG_DESC);
    gdtr->base = (uint32_t)gdt;

    //作ったGDTを読み込む
    lgdt((uint32_t)gdtr);
}

WIP

lgdt命令

WIP

めも

lgdt命令に指定するアドレスはリニアアドレスらしい。  
となると、最初にプロテクトモードに遷移するときに使ったGDTは、ページングが有効化後にも、lgdt命令でGDTRレジスタにセットしたアドレス(=仮想アドレスと解釈される)で参照できないといけない。  
一度読み込まれたGDTの内容は、シャドーレジスタ(CPU内部の見えないレジスタ)にキャッシュされ、GDTが再度読み込まれるのは、セグメントレジスタが変更されるとき。  
これは、割り込み(IDT)についても同じ。
→GDTを変更したら意図的にlong jump(セグメント間ジャンプ)して反映させよう
Clone this wiki locally