この記事は、インテル® デベロッパー・ゾーンに公開されている「Intel® Memory Protection Extensions (Intel® MPX) support in the GNU toolchain」(https://software.intel.com/en-us/blogs/2013/07/22/intel-memory-protection-extensions-intel-mpx-support-in-the-gnu-toolchain) の日本語参考訳です。
不正メモリーアクセスは、C/C++ プログラムよくある問題で、時間のかかるデバッグ作業、不安定なプログラム動作、そして脆弱性につながります。ソフトウェアの不具合に対する攻撃の多くは、バッファー・オーバーフロー (またはバッファー・オーバーラン) による不適切なメモリーアクセスに関連しています。このようなメモリーの不具合を見つけ、プログラムを攻撃から保護する既存の手法と開発ツールは、ソフトウェアによるソリューションしかなく、保護するコードのパフォーマンスを低下させます。
インテルの新しい ISA 拡張であるインテル® Memory Protection Extensions (インテル® MPX – 詳細は、こちらから最新のプログラマーズ・リファレンス・マニュアルを参照してください) は、わずかなパフォーマンス・オーバーヘッドでアプリケーションのメモリーアクセスを保護します。この新しい命令拡張を利用するには、OS カーネル、GNU バイナリー・ユーティリティー、コンパイラー、システム・ライブラリー・サポートの変更が求められます。
この記事では、インテル® MPX をサポートするため GNU* Binutils、GCC、そして Glibc で変更された点を説明します。
インテル® MPX は、”境界レジスター” と呼ばれる新しいレジスターにポインター境界を格納し、命令によってこれらのレジスターを操作します (詳細は、プログラミング・リファレンスを参照してください)。そのため、最初に binutils と GCC で新しいハードウェア機能のサポートを実装する必要があります。
インテル® MPX をサポートする最初のステップは、ポインターチェックとすべてのメモリーアクセスに関する責任を持つ GCC コンパイラーにインテル® MPX のインストルメント・パスを実装することです。ランタイムの境界チェックを行うためコンパイラーには次の変更が含まれます:
- スタティックに割り当てられたオブジェクト、スタックに割り当てられたオブジェクト、およびスタティックに初期化されたポインターの境界を作成します。
- インテル® MPX をサポートする ABI: ABI 拡張は、関数の引数として渡されるポインターの境界やポインターを持つ戻り値の境界を渡すことを可能にします (https://software.intel.com/en-us/forums/intel-isa-extensions にある、インテル® MPX の記事とブログリンクから Linux* ABI に関するドキュメントをご覧ください)。
- 境界テーブルの内容管理: メモリーに格納される各ポインターは、対応する境界テーブルの行に格納されている境界を持つ必要があり、コンパイラーは一貫した境界テーブルを持つように適切なコードを生成します。
- メモリーアクセスのインストルメント: コンパイラーは、各メモリーアクセスに対する境界を計算するためデーターフローを解析し、算出された境界に対して使用されたアドレスをチェックするコードを挿入します。
メモリー割り当てによりヒープ内に動的に作成されるオブジェクトは、割り当て時にオブジェクト (バッファー) の境界を設定する必要があります。そのため、次のステップで glibc の標準メモリーアロケーターにインテル® MPX のサポートを追加します。
アプリケーションを完全に保護するには、インテル® MPX インストルメントをサポートするライブラリーを使用する必要があります。これは glibc はほとんどのアプリケーションで使用されているため、インテル® MPX をサポートする GCC で glibc をコンパイルしなければならないことを意味します。また、アセンブラーで記述された関連するすべての glibc ルーチン (memcpy など) にインテル® MPX のインストルメントを追加しなければいけません。
そして、インテル® MPX 命令を利用するため GCC に新しいオプション -fmpx を追加しました。また、メモリー保護機能を備えたバイナリーを作成するには、インテル® MPX を有効にした binutils を使用しなければいけません。
MPX コンパイルされたプログラム向けに、次の簡単なテストを見てみましょう:
int main(int argc, char* argv[]) { int buf[100]; return buf[argc]; }
元のアセンブラー出力の一部 (-O2 でコンパイル):
movslq %edi, %rdi movl -120(%rsp,%rdi,4), %eax // buf[argc] のメモリーアクセス
サンプルをコンパイル:mpx-gcc/gcc test.c -fmpx –O2
MPX 対応アセンブラー出力の一部:
movl $399, %edx // edx へ配列長を設定 movslq %edi, %rdi // rdi は、argc の値を含む leaq -104(%rsp), %rax // rax へbuf の先頭アドレスを設定 bndmk (%rax,%rdx), %bnd0 // buf の境界を作成 bndcl (%rax,%rdi,4), %bnd0 // メモリーアクセスが、下限境界に違反して // いないかチェック bndcu 3(%rax,%rdi,4), %bnd0 // メモリーアクセスが、上限境界に違反して // いないかチェック movl -104(%rsp,%rdi,4), %eax // 元のメモリーアクセス
生成されたコードは明確です。4バイト (32 ビット整数) のアクセスを行っているため、ここでは上限チェックにディスプレースメント 3 を追加しただけであることに注意してください。
もうひとつの簡単な例で、 bndldx と bndstx 命令の使い方を見てみましょう。
extern int * p; extern int * q; int foo( int c) { q = p; return p; }
-O2 -fmpx オプションでコンパイルしたアセンブラー・コードは以下のようになります:
movq p(%rip), %rax movslq %edi, %rdi bndldx p(,%rax), %bnd0 // ポインター p の境界を bnd0 へロード movq %rax, q(%rip) // q=p bndstx %bnd0, q(,%rax) // q の境界情報を更新 leaq (%rax,%rdi,4), %rax bndcl (%rax), %bnd0 bndcu 3(%rax), %bnd0 movl (%rax), %eax
-fmpx 以外にも、インテル® MPX に関連するコンパイラー・オプションが追加されています。それらの大部分は、-fmpx-check-read や -fmpx-check-write のように、挿入されたランタイム時の境界チェックを制御します。また、開発者は手動でインテル® MPX 命令を追加するため、組込み関数を利用できます。インテル® MPX 関連のすべてのオプションと組込み関数については、GCC Wiki ページをご覧ください。
現在、インテル® MPX をサポートする GCC コンパイラーのソースは、共通 GCC SVN リポジトリ―内の別ブランチで入手できます。詳細については GCC SVN ページをご覧ください。
2013 年 7 月現在、インテル® MPX ISA を利用可能なハードウェアはありませんが、インテル® SDE を使用して評価することができます。インテル® SDE は、http://software.intel.com/en-us/articles/intel-software-development-emulator で入手できます。
コンパイラーの最適化に関する詳細は、最適化に関する注意事項を参照してください