GNU* ツールチェーンにおけるインテル® Memory Protection Extensions (インテル® MPX) のサポート

同カテゴリーの次の記事

暗号化/復号化 - JNI 呼び出しによる OpenSSL* API の使用

この記事は、インテル® デベロッパー・ゾーンに公開されている「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 拡張は、関数の引数として渡されるポインターの境界やポインターを持つ戻り値の境界を渡すことを可能にします (http://www.isus.jp/others/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 で入手できます。

コンパイラーの最適化に関する詳細は、最適化に関する注意事項を参照してください

関連記事