x86_64アーキテクチャ

プロテクション

Intel 64アーキテクチャでは、プロセッサレベルで、様々な保護機構(protection)が用意されている。これを用いる事で、OSは、カーネルをユーザーアプリケーションから保護したり、カーネル自身のバグによってカーネルが壊れるのを防いだりしている。プロテクションでは、様々なチェックを行い、アクセスがチェックで弾かれると例外が送出される。

特権レベル(=privilege level)

特権レベルには、使用される場所によって、以下の二種類(2bitと1bit)がある。

  • 0/1/2/3
  • supervisor/user

セグメントの保護機構には0/1/2/3(以降、PL)、ページの保護機構にはsupervisor/user(以降、S/U)が使われる。PLでは数字が小さいほど(=numerically less)、S/Uではsupervisorの方が特権レベルが高い。

対応関係:

PL S/U
0/1/2 supervisisor
3 user

実際の使用例:

特権レベル
0 OS
1/2 device driver
3 普通のアプリケーション
supervisor OS
user 普通のアプリケーション

特権レベルの設定:

設定 PL S/U
CPL o x
RPL o x
DPL o x
U/S x o

セグメント

セグメント保護を有効にするためには、CR0レジスタのPEフラグをセットする。これは、プロセッサをプロテクティッドモードにしているのだが、それによって、セグメントの保護が有効になる。

セグメントの保護には、

  • 大きさ(=limit)のチェック
  • タイプチェック
  • 特権レベル(privilege level)チェック

等がある。

特権レベルチェック

セグメントの特権レベルは、セグメントディスクリプタのDPLフィールドで設定されている(DPL: descriptor privilege level)。現在実行されているコードセグメントのDPLは、通常、現在のCPLと一致する。どのような特権レベルチェックが行われるかはセグメントのタイプ(ie. コード/スタック/データ等)によって違う。

セグメントの保護は、DPLだけでなく、セグメントセレクタに設定されているRPLによっても行われる(RPL: requested privilege level)。RPLは、アプリケーションが書き換える事ができる。これは、アプリケーションが予期せず重要なデータにアクセスしたりしないようにするためなどに用いられる。セグメントへのアクセスは、基本的に、CPLとRPLのうち、数値として小さい方が現在の特権レベルとして使われる。

以下、現在の特権レベルという時には、CPLとRPLを共に考慮した特権レベルを指す。

  • データセグメント

    データセグメントのDPLは、数値として現在の特権レベル以上の値でないといけない(ie. DPL >= CPL)。すなわち、データセグメントは、現在実行中のアプリケーションの特権レベル以上の特権レベルである必要がある。

  • コードセグメント

    コードセグメントには、nonconfortableセグメントと、confortableセグメントがある。CSの、Cフラグがセットされていればconfortableセグメントで、そうでなければnonconfortableセグメント。

nonconfortableセグメント

nonconfortableセグメントのDPLは、現在の特権レベルと同じでないといけない。現在の特権レベルより高くても、低くてもアクセスできない。

confortableセグメント

confortableセグメントのDPLは、数値としてCPL以下である必要がある。confortableセグメントのDPLが現在のCPLと等しくなくても良いが、confortableセグメント中のコードを実行するときはCPLが変化するわけではなく、CPLはそのまま変化しない。

  • スタックセグメント

    スタックセグメントのDPLはロード時にチェックされる。DPLはセグメントセレクタのRPL、および、現在のCPLと同じでないといけない。~64-Bitでは、スタックセグメントのDPLは使われるときに、自動的に現在のCPLとなる~(出典を確認できず)。

  • コールゲートセグメント(=call gate)

f:id:babyron64:20171225145030j:plain コールゲートによって、プロセッサの厳格な管理下でのコールができる。コールゲートは、コールゲートセグメントを明示的に用いたファーコール等によって呼ばれる。論理アドレスのオフセット部は、コールゲートセグメント内のエントリポイントを示す。コールゲートのアクセス権限は、コールゲートセグメントのDPLによって決まり、コールゲートが指し示すコードセグメント(ターゲット)には因らない。そのため、コールゲートを用いる事で、現在の特権レベルよりもDPLが数値的に小さい(ie. 特権レベルが高い)コードセグメントの内容を実行できる。ただし、現在の特権レベルよりもDSPが大きいコードセグメントの内容は、コールゲートを用いても実行できない(RET等で戻る場合は別)。ターゲットのDPLは、コールがアクセス権をまたぐ(=inter-privilege / outer privilege)かどうかの判定に使われる。アクセス権をまたがない(=intro-privilege / same privilege)コールと、アクセス権をまたぐコールは、処理内容が違う。

コールゲートを使う時は、コールゲートのセグメントセレクタをセグメントレジスタにロードし、そのセグメントレジスタを用いたfar pointerを使ってコールする。この時、オフセットを指定する必要があるが、指定されたオフセットは無視される。

nonconfortableコールゲート

nonconfortableコールゲートは、nonconfortableコードセグメントをターゲットとするコールゲート。ターゲットのDPLが現在の特権レベルよりも数値的に小さい場合、CPLはターゲットのDPLに変化すると共に、スタックスイッチ(後述)が起こる。nonconfortableコールゲートセグメントは、コール命令でも、ジャンプ命令でも使えるが、コール命令の時はターゲットのDPLは現在の特権レベルより高くても良い。一方でジャンプ命令の時は、ターゲットのDPLは現在の特権レベルと同じでなくてはならない。

confortableコールゲート

confortableコールゲートセグメントは、confortableコードセグメントをターゲットとするコールゲート。ターゲットのDPLが現在の特権レベルよりも数値的に小さい場合でも、CPLは変化しない。そして、スタックスイッチも起こらない。nonconfortableコールゲートと違い、コール命令でもジャンプ命令でも、コール命令の時はターゲットのDPLは現在の特権レベルより高くても良い。

大きさチェック

論理アドレスのオフセットがセグメントの大きさを超えてないかチェックする。これは、64-Bitではチェックされない。

ページング

ページングの保護は、ページを有効にした段階で自動的に有効になる。ページング保護には、以下の二種類ある。

  • 特権レベルチェック(=restriction addressable domain)
  • 読み込み書き込みレベルチェック(=page type)

ページ本体を見つけるまでに参照を使っている場合、最終的な特権レベルは、参照した全てのページング機構の特権レベルと読み書きレベルの組み合わせうち、最も厳しいものが適用される。組み合わせの厳しさを考える時、特権レベルの方が読み書きレベルよりも優先される。例えば、

参照の特権レベル 参照の読み書きレベル ページの特権レベル ページの読み書きレベル 最終的な特権レベル 最終的な読み書きレベル
user read only supervisor read / write supervisor read / write

アクセス時にも、特権レベルが読み書きレベルより優先される。例えば、特権レベルがuserで、読み込み専用のページにsupervisorレベルのアプリケーションがアクセスする時、そのアプリケーションはページに書き込む事ができる。ただし、デフォルトの状態ではsupervisorは、特権レベルがsupervisorで読み書きレベルが読み込み専用であるページに書き込める。つまり、supervisorのアクセスに対しては読み込み専用を実質指定することができない。これは、CR0レジスタのWPフラグをセットすることで、変更できる。

現在の特権レベル CR0.WP ページの特権レベル ページの読み書きレベル アプリケーション
supervisor 0 supervisor read only r/w
supervisor 1 supervisor read only r

全ての特権レベルからの書き込みを制限することで、OSはcopy-on-writeという仕組みを実現できる。copy-on-writeは、プロセス(やタスク)を既存のものから派生させて(=fork)新しく作る時に、メモリをコピーすることなく、既存のものをさしあたり使うという仕組み。そうすることで、プロセス生成のコストを下げることができる。しかし、親プロセスや子プロセスがそのメモリを書き換えてしまうと、当然ながら不都合だ。そこで、共有するメモリ領域は読み込み専用にしておき、そこへの(あらゆるレベルからの)書き込みによる(アクセス違反の)例外をキャッチした段階でメモリのコピーを行う。このように、copy-on-writeでは必要になってからコピーを実行する。

特権レベルチェック

特権レベルチェックは、ページング機構のU/Sフラグを参照する事で行われる。U/Sフラグがセットされている時はuesrレベルで、そうでないなら、supervisorレベル。supervisorレベルのページングには、userレベルのアプリケーションはアクセスできない。

読み込み書き込みレベルチェック

読み込み書き込みレベルチェックは、ページング機構のR/Wフラグを参照する事で行われる。R/Wフラグがセットされている時は、読み込み・書き込みがともに許可されている。そうでないなら、読み込み専用(=read only)。

スタックスイッチ(=stack switch)

f:id:babyron64:20171225172552j:plain

特権レベルをまたぐ(=inter-privilege)コールなどが行われると、スタックスイッチが発生する。64-Bitの割り込み・例外ハンドリング(後述)では、特権レベルの変化にかかわらず、スタックスイッチが起こる。スタックスイッチは、現在のSSおよび[R|E]SPを別の値に変化させる。元のSSと[R|E]SPは、切り替わった先のスタックに保存され、リターン時にSSと[R|E]SPに戻される(=restore)。どのような値に変化するかは、プロセッサの動作モードによって違う。

32-Bit

スタックスイッチが起こると、ターゲットのCPLに対応するSSおよびESPが現在のTSSからロードされる。

64-Bit

スタックスイッチが起こると、SSには、RPLがターゲットコードセグメントのDPLに設定されたNULLセレクタ(ie. GDTの0番エントリへのセレクタ)がロードされる1。32-BitのときにはSSにNULLセレクタをロードすると例外が送出されるが、64-Bitではそれは起こらない。RSPには、ターゲットコードセグメントのDPLに対応するRSPが現在のTSSからロードされる。

IDMには、IRETで戻るときに、スタック上に保存されているSSがNULLセレクタで、かつターゲットのCPLが3でない場合、SSを戻さないといった趣旨に内容が書かれている2が、詳細はまだ理解できていないため割愛する。

補足

1: CALLのdescription(IDM vol-2)では、

NewSS <- new code-segment DPL; (* NULL selector with RPL = new CPL *)

と書かれている。NULLセグメントディスクリプタのDPLはチェックされないのだろう。SSにNULLセグメント以外を指定すると、そのDPLはターゲットのCPLでなくてはならない。TSSで各特権レベル用のSSが指定されていない64-Bitでいちいちそのようなセグメントを用意するのは面倒だ。

2: CPL!=3でなければならない理由は、おそらく、ターゲットのCPL=3のときに、スタックに保存されているSSがNULLセレクタであることが起こってはならないからだ。とういうのも、NULLセレクタが保存されるのは、特権レベルをまたぐコールが入れ子上になされた時だけであり、かつ、特権レベルをまたぐ時は、(リターン時を除いて)特権レベルの高い方にしか行けない。 SSを更新しない理由は、パフォーマンスを向上させるためだろう。しかし、SSを更新しない場合、SSのRPLも更新されないので、意味論的に混乱しかねない。まあ、RPLのチェックは、前述したように、ロード時に行われるのでアクセス違反にはならないだろうが。また、IRETのdescription(IDM vol-2)を見ても、SSを更新しているようにしか見えない。