はじめに
この記事においてはプロセス=アプリケーションのことです。プロセスと表現するときはOS 内で動作中のもの、アプリケーションと表現するときはプログラ厶そのもの、というニュアンスです。
また、仮想記憶に関する基礎知識が必要になりますので、以下をご参照下さい。
OS上でプログラムが起動する仕組み
OS 上でプログラムが実行されると、プログラムは最初に OS にメモリ領域を要求し、OS はそのプログラム用に要求通りのメモリ領域を与えます。メモリ領域内には『命令』と、『その命令に使うその他のデータ』が展開されます。
通常、CPU は命令を順番通りに実行していきます。命令を実行する際には、命令に必要なデータを、メインメモリから CPU 内のレジスタに読み込んでから命令を実行します。
ユーザー空間とカーネル空間
パソコンやサーバは起動するとまず HDD にインストールされた OS (Kernel) プログラムをメモリ領域に展開します。このとき使われるメモリ領域を『カーネル空間』と呼びます。
一方、OS 上でプログラムが起動された場合は OS の管理下の元、メモリ領域に展開します。このときのメモリ領域を『ユーザー空間』と呼びます。Linux の場合、最初にユーザー空間に起動されるプロセスは initd だったり systemd だったりします。
システムコールとは
OS の目的はその上で複数のプロセスを動作させることです。しかしプロセスが OS に対して悪さをしてしまう(意図して悪さをするものはウィルス、意図しないものはバグ)と他のプロセスの妨げになったり、最悪システムが破壊されます。
そのため、ユーザー空間上の一般的なプロセスは、カーネル空間には直接アクセスできないようになっています。OS は『システムコール』という関数 (API) を一般的なプログラム向けに提供しており、原則そのシステムコールにより間接的にアクセスすることになっています。
システムコールには例えば HDD への読み込み/書き込みアクセス、ネットワークの利用、等があります。一般的なプログラムから OS に対して変な動作をさせないため、予め決まったことしかできないようになっているわけです。
この『システムコール』でユーザー空間とカーネル空間を繋ぎ、他のアクセス手段を原則禁止にすることで、他のプロセスや周辺ハードウェアへのセキュリティおよび安定性を高めているのです。
また、プロセス#1 のユーザー空間とプロセス#2 のユーザー空間が互いに干渉してしまうと動作が安定しないばかりか、場合によってはプロセス#1 の重要情報をプロセス#2 に搾取されたりしてしまいます。
カーネル空間へのアクセスも制限が必要ですが、ユーザー空間同士のアクセスにも制限が必要です。そこで出てくるのが、メモリ保護です。
プロセス間のメモリ保護について
複数のプロセス間が互いのメモリ領域を浸食しないようにメモリ領域の管理を行うのは、CPU に組み込まれている MMU (メモリ管理ユニット: Memory Management Unit) の仕事です。
プロセス内のメモリ領域保護はアプリケーション自らが (つまりプログラマが) 行わなければなりませんが、プロセス間のメモリ領域保護の場合は、OS と MMU が協調して保護します。
具体的には、各プロセスは MMU 内に各々のページテーブルを持ちます。ページテーブルはプロセスが理解している仮想アドレスを、メモリが実際に持つ物理アドレスに変換してくれるものです。このページテーブルにより、プロセスは互いの物理アドレス領域に入ってこれないように保護されるのです。
しかし例外があります。それはカーネル空間 (カーネルメモリ領域) です。
各プロセスは OS 上で動作し、OS の助けを借りながら動作するので、OS のメモリ領域にアクセスできる必要があります。それが頻繁に必要になるものも多いため、パフォーマンスを考慮し、ページテーブルには、アプリケーション自身に与えられたメモリ領域(ユーザー空間)の他に、カーネル空間が共通して、ページテーブルに用意されています。
ただし、そのままだと危険なので、ページテーブルのカーネルメモリ領域のページには supervisor-bit を付け、CPU がカーネルモード(スーパーバイザーモード: Ring 0 とも呼ばれる) で動作しているときのみにアクセス許可しています (例えば Windows PC だと『管理者権限』と呼ばれるものです)。
ユーザモードでアクセスしようとすると『メモリアクセス違反』として処理されます (通常はプロセスが停止します)。
ユーザモードとカーネルモードの切り替えは、マルチタスク OS の実行プロセスの切り替えでも利用される Context Switch によって実施します。
また、カーネルモードになったからと言って何でもかんでもアクセスできる訳ではなく、システムコール等のあらかじめ OS アクセス用として用意されたライブラリ関数を使う必要があります。
これにより、どこぞのプログラマが作ったか分からぬアプリケーションが、勝手にカーネルメモリ領域に入ってこれないように保護されるわけです。
Meltdown の公表に先駆けて実装が進んだ KAISER (KPTI)
2017年前半までは、このページテーブルは各プロセスで1つのみとするのがごく一般的な実装でした。つまり、アプリケーション用の仮想アドレスとカーネル領域の仮想アドレスが、1つのページテーブルを使うようになっていました。
しかし2018年頭の Meltdown の登場により、状況は変わりました。
投機実行を実装したアウト・オブ・オーダー実行のセキュリティホールを突いた、アプリケーションのサイドチャネル攻撃によりカーネルメモリ領域を観測できることが (公表より前に一部の関係者に) 知られたからです。
この攻撃は、ユーザモードのプロセスが (アクセス権違反である) カーネル領域へアクセスしようとするだけで、違反にはなるものの L1D キャッシュに痕跡が残せる、というものでした。
そこで速やかに打ち出された対策が『KAISER (Kernel Address Isolation to have Side-channel Effectively Removed)』(読み方:かいざー) です。
これは、ページテーブルを 2 つに分割し、1 つをそのアプリケーションのユーザー空間用、もう 1 つをカーネル空間の呼び出し用として管理する機能です。(厳密には、もともとは KASLR という「カーネルのメモリ空間をランダム化し、どこに配置されているかを秘匿化する」セキュリティ技術が突破され、その対策としてこの KAISER の策定を進めていたようですが、Meltdown にも効果的だと分かり、急スピードで実装が進んだ、ということのようです)
これにより、ユーザモードではカーネルメモリ領域に『メモリアクセス違反』すら出来ない状態を作り上げたのです。
この KAISER は、すぐに KPTI (Kernel Page-Table Isolation) という呼び名に変わりました。
コメント