この記事はsfc-rg Advent Calendar 2018 5日目の記事です.
遅くなってしまいました.ごめんなさい.
概要
セグメント機構とそれに付随するCPUの動作モードに関する記事.
あとで自分が見るときのために,CPUのセグメント機構について大雑把に説明し,32bit版と64bit版の違いなどについて触れていきたい.
また,セグメント機構をLinuxが32bitと64bitの場合で,それぞれどのように使われているかをまとめていく.
この記事で扱うLinuxは,x86およびx86-64アーキテクチャにおけるLinuxとする.
セグメント機構とページング機構
前回の記事からの引用,
x86アーキテクチャおよびx86-64アーキテクチャには,その上に乗るOSのアドレス変換機構をサポートする機能がある. それが,セグメント機構と,ページング機構である.
論理アドレス --[セグメント機構]--> リニアアドレス --[ページング機構]--> 物理アドレス
それぞれの役割は上の通り.つまり,CPUには二段階の変換機構がハードウェアレベルで用意されているということ. セグメント機構は,無効にすることはできないが,ページング回路は,有効無効を切り替えることができる.OS次第. 上に出てきている,リニアアドレスというのが,いわゆる仮想アドレスのことであり,Linuxにおいては,これがプロセス空間にて使用される.
セグメント機構には,CPUの動作モードに依存した,何種類かある.まずは,前提となるCPUの動作モードからまとめていく.
CPUの動作モード
CPUはブート時,8086互換環境を提供するリアルモードで動き出す.
その後,32bitのプロテクトモードに移行し,最後に64bitモードに移行する.
それぞれのモードの説明
リアルモード
8086用にかかれたプログラムを実行させるためのもの. 電源投入直後はやリセット後は,一時的に,リアルモードで動作する.
リアルモードが扱えるメモリ空間は,20bitで表現できる,0x00000 ~ 0xFFFFFまでの1MB. 20bitなのは,8086のアドレスバスが20本しかないため.
最初の処理が終わったら,プロテクトモードに移行する. アクセスを保護する機能などはない.
プロテクトモード
プロテクトモードは,80386以降に搭載された,メモリ管理やタスク管理などを使うためのモード.
プロテクトモードが扱えるメモリ空間は,4GBまで広がった.
リアルモードはプロテクトモードとの互換は一切ない.
メモリやI/Oデバイスへのアクセスを保護する機能が常に働いている.許可されない範囲のアドレスを,プロセスが利用としたら,CPUは割り込みを発生させる.
仮想8086モード
プロテクトモードのメモリ管理などの機能を働かせたまま,8086用プログラムが実行できるようになるモード.このモードはLinuxでは使われていない.
64bitモード
プロテクトモードのアドレスを64bitに拡張したモード.理論的には,64bitで表せるアドレスの範囲を扱えるが,現在は,40bitしか扱えないようになっている.
互換モード
64bitモードの中で,32bit用のアプリケーションを動かすためのもの.
リアルモードからプロテクトモードへの移行
CPUが動作モードは,CR0と呼ばれるレジスタの最下位bitの内容に基づいて決まる.
この値が1であれば,プロテクトモード,0であれば,リアルモード.
動作的には簡単だが,これだけは移行はできない.
- GDT(Global Descriptor Table)の作成
- GDTレジスタの設定
- IDT(Interrupt Descriptor Table)の作成
- IDTレジスタの設定
- A20のマスク解除
- CPUへの割り込み禁止
- CR0レジスタの再開ビットを1に
- パイプラインの内容をフラッシュ
- セグメントレジスタの設定
これらを設定する必要がある.
レジスタ一覧
- 汎用レジスタ
- RAX
- RBX
- RCX
- RDX
- RSI
- RDI
- R8 ~ R15
- ベースポインタ
- RBP
- スタックポインタ
- コントロールレジスタ
- CR0
- CR1
- CR2
- CR3
- プログラムステータスレジスタ
- フラグレジスタ(EFLAGS)
- セグメントレジスタ
- セレクタレジスタ
- CS
- DS
- ES
- SS
- FS
- GS
セグメント機構の概要
セグメント方式とは,連続したメモリ空間を区画に分割して管理する方式の名前.その区画のことをセグメントと呼ぶ.
セグメントの中の特定のアドレスは,オフセットで表す.
IA-32(32bit)のCPUにおいては,0番地から,高位に向かって連続するアドレス空間の,ある大きさをセグメントとして認識する. この時に指定すべき値としては,セグメントベースと呼ばれる開始アドレスと,大きさである.
セグメント内のアドレスは,セグメントベースにアフセットを加えて指定し,このアドレスをリニアアドレス(仮想アドレス)と呼ぶ.
セグメントベース + オフセット = リニアアドレス(仮想アドレス)
ここでこのセグメントを指定する方法の解説をしていく,
セグメントレジスタの値から,セグメントベースを算出し,この値をオフセットを足し合わせることで,リニアアドレスを算出している.この,算出方法については,CPUのモード毎に異なる.
通常,システムには複数のセグメントが存在し,その領域同士が重なっている場合があるが,これは適切に設定できていれば問題ない.
上の図にも書いてあるが,この,セグメント機構にかけられる前のアドレスを,論理アドレスと呼ぶ.論理アドレスのい書式は以下の通り.
レジスタ名:オフセット値
レジスタ名:汎用レジスタ名
例えば,DS:0100
は,DSレジスタが指すセグメントの,先頭から数えて,0x0100番目のアドレスを指す.
また,直接レジスタの値を使うこともある.
レジスタ値:オフセット値
(例: 2345:0100
)
セグメントレジスタの用途
6つあるセグメントレジスタのうち,それらのうち,CS, DS, SSレジスタは,用途が決められている.
CSレジスタ
Code Segment
CPUが実行するプログラム,つまりプロセスにおけるコードセグメントに相当する領域を指定するために使われる.
IPレジスタ(インデックスポインタ.32bitではeip,64bitでは,ripと呼ばれる)は,実行中の命令のアドレスのオフセット値を保持し,解釈している.
DSレジスタ
Data Segmentを指定するためのレジスタ.
SSレジスタ
Stack Segmentを指定するためのレジスタ.スタックの先頭アドレス.
リアルモードにおけるセグメント
8086では,セグメントレジスタの値は,16bitしかない.しかし,これを4bit左シフトすることで,20bitのセグメントベースとしている.ここに,オフセットを加算し,仮想アドレスを算出する.
オフセットの大きさは16bitであり,一つのセグメントの大きさは,常に64KBになる.
あるセグメントレジスタの値が0の時,作り出せる仮想アドレスの範囲は,
0000:0000 ~ 0000:FFFF
の16bitの範囲となり,仮想アドレスの範囲は,0x00000 ~ 0x0FFFF
となる.
表記上,異なる論理アドレスに見えても,計算した結果,同じ仮想アドレスが算出された場合,その二つの論理アドレスは,同じものであると考えることができる.
プロテクトモードにおけるセグメント
プロテクトモードのCPUでは,32本あるアドレスバスを全て使えるようになるため,オフセットアドレスに32bitの値が使えるようになった.
そのため,プロテクトモードにおいては,4GBあるメモリ空間の全ての仮想アドレスを直接指定できるようになった,
セグメントレジスタの値の大きさは,プロテクトモードにおいても,16bitである.
リアルモードにおけるセグメントレジスタの値は,4bit左にシフトされていたが,プロテクトモードにおいては違う.
プロテクトモードのCPUはセグメントディスクリプタと呼ばれる特別なデータを必要とする.これは,8バイトの構造体である.
これはメモリ上に作成されるデータであり,この構造体を並べて作成し,その開始アドレスと個数をCPUに通知する.
この.並べて作られたセグメントディスクリプタの塊をセグメントディスクリプタテーブルと呼ぶ. プロテクトモードのセグメントは,一つのセグメントに対して,一つのディスクリプタを用意する.
セグメントレジスタは,セグメントディスクリプタテーブルの中から一つのテーブルを指すための,インデックス値を格納する. セグメントセレクタの値は,セグメントレジスタの15bit - 3bitに入っている,これを8倍すると,テーブルのバイトのオフセットが求められる.(デスクリプタの値が8バイトであるため)
セグメントセレクタの値が全て0の場合,セグメントレジスタを無効にする,という意味がある.
こうすると,セグメントディスクリプタテーブルの0番目のエントリが使えないことになるので,0番目のエントリには空のエントリが作成される.
セグメントディスクリプタの構造
セグメントディスクリプタのサイズは,8バイト,つまり64bitである.
それぞれのフィールドの説明をしていく.
ベース 31bit - 0bit
ベースと呼ばれるフィールド.セグメントのベースアドレスが格納されている.
リミット 19bit - 0bit
セグメントのサイズを表す. サイズの単位は,後述のGフラグの内容に依存する
Gフラグが
0の時,1MBまでの値
1の時,4GBまでの値
Gフラグ 55bit
リミットの単位.
Dフラグ 54bit
リアルモードでは,アドレスのサイズ,データのサイズは16bitであった.一方,プロテクトモードでは32bitを選択することもできる.
Dフラグが1の時,32bitとなり,0の時,16bitとなる.
Linuxについては,常に1が設定される.
Pフラグ 47bit
セグメントがメモリ上に存在するときは,1.しないときは0となる.
Linuxでは,セグメント全体がスワップアウトされることがないため,常に1となる.
DPL 46-45bit
Descriptor Privilege Level
デスクリプタ特権レベルと呼ばれ,そのセグメントへアクセスするのに必要なCPUの特権レベルを表す.
タイプ 43bit - 41bit
セグメントのタイプを表す.
基本的に,chmodとかで使う,あの数字っぽい感じ.
プロテクトモードにおけるセグメント続き
以上の形式で,セグメントディスクリプタテーブルがメモリ上に作成されると,その先頭アドレスとエントリ数がCPUに通知される.
この命令を実行すると,GDTRレジスタに32bitの先頭アドレスと,16bitのエントリ数が書き込まれる.
しかし,ここまでやって言うのもあれだが,Linuxにおいては,セグメントの機能はあまり出てこない.
Linuxが使用しているセグメントは以下の4つ.この4つは,セグメントの特権レベルやタイプが違うだけもの.
- KERNEL_CS
- KERNEL_DS
- USER_CS
- USER_DS
全てのセグメントは,ベースアドレスが0から始まり,その大きさは,4GB.つまり,オフセット = 仮想アドレス
この方式のことを,基本フラットモデルという.ほとんど機能していない,あってもなくても同じに見えるセグメント機構であるが,なぜ使われているのか.
その理由は単純で,セグメント回路を無効にすることができないからである.
OSによっては使われているが,少なくともLinuxにおいては,使われていないというのが現状である.
64bitにおけるセグメント
64bitモードでは,DS,ES,SSの3つのセグメントレジスタは使用されない.
CPUのアーキテクチャ上は,コードセグメントの指定に,CSレジスタのみ使う.
しかし,Linuxにおいては,32bitの時と同様,フラットモデルで使われるため,どうでもいいこと.
その他,微妙に,各フラグの使い方は異なるが,きにする必要がない.
結論
Linux(64bit)では,セグメントがほぼ使われていないため,勉強する必要がない.
参考文献

新装改訂版 Linuxのブートプロセスをみる (アスキー書籍)
- 作者: 白崎博生
- 出版社/メーカー: 角川アスキー総合研究所
- 発売日: 2014/10/02
- メディア: Kindle版
- この商品を含むブログ (2件) を見る