cyan's blog

しょーもない事しか書いていません

x86におけるセグメンテーションについてのメモ

書かないと忘れるので...

概要

  • 16-bitマシンであった8086では20-bitのデータバスを利用するために各種セグメントレジスタの値を16bit left shiftした値+オフセットをリニアアドレスとして使用していた.*1全てのx86アーキテクチャCPUは今でも16bitリアルモードで起動する.*2
  • protected modeではCPUが動作しているとき,全てのメモリアクセスはGDT(Global Descriptor Table) ,LDT(Local Descriptor Table)を経由する.
    • GTD/LDTにはセグメントディスクリプタがエントリとして格納され,セグメントディスクリプタにはセグメントのベースアドレス,サイズ(limit),アクセス権限などの情報が含まれる.
    • GTDレジスタとLDTレジスタにそれぞれのセグメントディスクリプタテーブルのベースアドレスが格納される.LDTはsystem segmentであるため,LDTを含むセグメントはGDTにセグメントディスクリプタを持つ必要がある.
    • 32bit protected modeにおいては各種セグメントレジスタはセグメントセレクタを保持し,メモリアクセス時にはこれを参照し論理アドレスからリニアアドレスへの変換が行われている
    • 64bit protected modeにおいてはセグメンテーションは基本的に無効化される.*3プロセッサが殆どの*4各種セグメントレジスタをbase: 0として取り扱うことにより64-bitのFlat Model(後述)のリニアアドレス空間が作成される.

セグメントディスクリプタの構造

(Intel® 64 and IA-32 Architectures Software Developer Manualsより)

画像に概ね書いてあるので補足だけ:

  • Segment Limit: セグメントのサイズ.20bitしかないが,粒度を1Byteか4KBytesで選べるため,1B-1MiBか4KiB-4GiBの範囲で設定できる.
  • G: Granularity flag.setでsegment limitを4-KiB単位の粒度に,clearなら1B単位に.
  • Type: Sがsetされているときにセグメントディスクリプタがcode or dataのどちらであるか,codeならRX,コンフォーミング*5権限設定,dataならRW権限,エクスパンドダウン*6の設定を示す.

セグメントセレクタ

セグメントセレクタは各種セグメントレジスタに格納され,どのセグメントディスクリプタを参照するべきかを示す.

(Intel® 64 and IA-32 Architectures Software Developer Manualsより)

  • Index: GDT/LDTのどのエントリ?
  • TI: clear→GDT. set→current LDTを使う
  • RPL: 要求権限レベル

実際の使われ方

セグメントディスクリプタは最低でRing0, Ring3にそれぞれコードとデータセグメント分の計4つ用意しなくてはならない(*TSSとかのsystem segmentは別として) この4つのセグメントディスクリプタをbase: 0 limit: 0xffff_ffffと設定する方法はSDMではFlat Modelとして紹介されており,xv6はFlat Modelを採用している.

以下xv6におけるGDT設定(*LDTは使われない)

void
seginit(void)
{
  struct cpu *c;

  // Map "logical" addresses to virtual addresses using identity map.
  // Cannot share a CODE descriptor for both kernel and user
  // because it would have to have DPL_USR, but the CPU forbids
  // an interrupt from CPL=0 to DPL=3.
  c = &cpus[cpuid()];
  c->gdt[SEG_KCODE] = SEG(STA_X|STA_R, 0, 0xffffffff, 0);
  c->gdt[SEG_KDATA] = SEG(STA_W, 0, 0xffffffff, 0);
  c->gdt[SEG_UCODE] = SEG(STA_X|STA_R, 0, 0xffffffff, DPL_USER);
  c->gdt[SEG_UDATA] = SEG(STA_W, 0, 0xffffffff, DPL_USER);
  lgdt(c->gdt, sizeof(c->gdt));
}

32-bit Linuxにおいても主に使われるセグメントディスクリプタはこの4つであるが,同様にFlat Modelで設定されている.64-bit版ではbaseやlimitは意味がない. また,thread local storageなどを実現するためのセグメントディスクリプタなども別途存在し,これらはFS/GSレジスタを用いて利用される.

*1:このx86セグメンテーションは一般に言うセグメント方式とは異なるので注意.

*2:なおX86-Sで16bitリアルモードなどの後方互換が大きく切り捨てられるらしい

*3:しかし,IA32互換モードで動作している場合はセグメンテーションは無効化されない.

*4:FS, GSは使用可能である.Linuxではthread local storageの実現などに使用されている.

*5:権限レベルが低いコードから実行できる意

*6:セグメントがどちらに伸びるか.この場合はダウンなのでtrue,アドレス空間をダウンする方向.スタックなどを設定する際に使われる