x86_64アーキテクチャ

メモリアクセス

protected modeと64-Bit modeではメモリアクセスの方法が(少し)異なる。protected modeでは、セグメントと(オプションで)ページを使って、物理メモリ(=physical memory)にアクセスする。一方で、64-Bit modeでは、セグメントが実質的に無効化されており、(オプションで)ページを使って、物理メモリにアクセスする(TODO: 参照挿入)。

プロセッサでは、物理メモリアドレスの他に、リニアアドレス(a.k.a. 実アドレス(=effective address)、もしくは仮想メモリアドレス)、論理アドレス(=logical address)を使っている。命令(=instruction)で指定されるのは、(暗黙的な場合もあるが)すべて論理アドレスである。暗黙的に論理アドレスが使われるとき(ie. offsetのみの指定)、デフォルトのセグメントレジスタが使われる。それぞれのアドレスの違いは、後々説明していく。

セグメント(=segment)

protected modeであれ、64-Bit modeであれ、メモリ指定にセグメント1という仕組みが使われている。セグメントとは、メモリをいくつかの領域に区切ったものだ。有名なものとして、コードセグメント(CS)、データセグメント(DS)、スタックセグメント(SS)などがある。このように、メモリを区切っておくことで、例えば、データセグメントのバイナリを実行したりすることを防ぐことができる。

論理アドレス

セグメントを用いてメモリを指定するときには、segment:offsetという記法を使う。このやり方で表されるアドレスが、先ほど出てきた論理アドレスだ。セグメントが省略された場合には、デフォルトのセグメント(コード中だと、CSレジスタで示されるセグメント)が使われる。CSレジスタは、現在のコードセグメントのセグメントセレクタ等)が格納されているレジスタなのだが、後で詳しく説明する。明示しなければならないのは、far jump(後述)など、セグメントをまたぐときなので、セグメントが明示されたsegment:offsetの形式のアドレスはfar pointerと言われることもある。 f:id:babyron64:20171222205938j:plain offsetは、segmentのベースアドレスからのオフセットで、これら二つの情報からリニアアドレスが割り出される。

リニアアドレス=セグメントのベースアドレス+オフセット

セグメントディスクリプタ(=segment descriptor)

f:id:babyron64:20171222200716j:plain セグメントディスクリプタは、対応するセグメントの位置(base address)・大きさ(limit)・アクセス権(DPL)・タイプなどを決める(DPL: descriptor privilege level)。セグメントにアクセスするときには、offsetがlimitを超えていないかや、現在の実行権限がDPLと同じであるかのチェックが行われる(詳しくは、TODOプロテクション参照挿入)。セグメントのタイプには、よく使うものにコードセグメントとデータセグメントがあり、これら二つのセグメントは、権限チェック時の振る舞いが異なる。その他にも、コールゲート(=call gate)や割り込みゲート(=interrupt gate)等があるが、ここでは割愛する。(詳しくは、TODOプロテクションand制御移譲 参照挿入)

ディスクリプタテーブル(=descriptor table)

セグメントディスクリプタのリストを保持する。GDTとLDTの2種類があり、GDTはプロセッサにつき一つ、LDTはタスクやプロセスにつき一つあるのが普通(GDT: global descriptor table / LDT: local descriptor table)。それぞれのディスクリプタテーブルのアドレスは、GDTRおよびLDTRレジスタ格納される。プロセッサはこのレジスタを参照することでセグメントセレクタ(後述)から、対応するセグメントディスクリプタを探し出している。GDTの0番エントリ(ie. index=0)は、NULLディスクリプタとして使用され、これを参照しようとするとプロセッサは例外を投げる(=generate)。ただし、NULLディスクリプタに対応するセグメントセレクタを使う(eg. セグメントレジスタにロードする)だけなら、例外は投げられない。

セグメントセレクタ(=segment selector)

f:id:babyron64:20171222200725j:plain セグメントセレクタは、セグメントディスクリプタの場所(GDT or LDT)とインデックス(=index)、および要求権限(=RPL)で構成される(RPL: requested privilege level)。要求権限は、ソフトウェアによって決定される。(詳しくは、TODOプロテクション参照挿入)

セグメントレジスタ(=segment register)

f:id:babyron64:20171222200721j:plain 現在使っているセグメントは、セグメントレジスタに入っているセグメントセレクタで示されている。セグメントレジスタには、セグメントセレクタ(可視領域(=visible part))と隠れ領域(=hidden part)がある。隠れ領域には、セグメントセレクタに対応するセグメントディスクリプタの内容をキャッシュしている。

セグメントレジスタは、CS・SS・DS・ES・FS・GSの6つある。

名前 説明
CS コードセグメントレジスタ
SS スタックセグメントレジスタ
DS データセグメントレジスタ
ES・FS・GS 補助的に使う

64-Bitモードでは、フラットメモリモデル(=flat memory)を使う。すなわち、セグメントでのメモリ分割はせず、リニアアドレス全体を一つのまとまりとして扱う。そのため、セグメントはほとんど使われない。

SS・DS・ESは使われない。これらのセグメントレジスタに対応するセグメントディスクリプタの内容は無視され、ベースアドレスが0であるものとして扱われる。また、大きさ(limit)のチェックは行われない。FS・GSは使うことができる。CSは、権限チェックに使われるが、ベースアドレスは0であるものとして扱われる。また、大きさ(limit)のチェックは行われない。

プロテクションの説明で、CPLという単語が出てくるが、これは、現在のコードセグメントレジスタのアクセス権限である(CPL: current privilege level)。そして通常、現在のコードセグメントレジスタに対応するセグメントディスクリプタのDPLである2

参考文献

  • Intel developer's manual vol-3 (2016)

    記事中のTableは、このマニュアルからとった。また、記事の内容も、このマニュアルの解釈および要約である。


  1. セグメントと聞くと、オブジェクトファイルのプログラムヘッダエントリを思い出す人もいるかのしれないが、これとは別のものである。しかし、プログラムヘッダで指定されるセグメントは、ローダーがそれぞれのバイナリデータをどのセグメントに配置するのかを決めるのにつかわれる「はず」だから、無関係とはいえないと思う。

  2. というのも、先述したように、CS(に限らず、どんなセグメントレジスタでも)が読み込まれると、対応するセグメントディスクリプタの内容がセグメントレジスタにキャッシュされる。そして、キャッシュされたDPLがセグメントレジスタのアクセス権限として使われる「はず」だからだ。