Intel MPX

http://software.intel.com/en-us/blogs/2013/07/23/intel-memory-protection-extensions-intel-mpx-design-considerations

最近MPXという文字を見るな、という気がしてて、Memory Protection Extension とか書いてあったので、またVMとかで保護機能増えたのかな、と思ってたが、そんなことはなかった。

Intel MPX 拡張は、ユーザー空間でアドレス境界チェックを簡単にするための拡張。サポートは、http://software.intel.com/en-us/articles/intel-software-development-emulator によると Skylake/Goldmontからと書いてある。
使い道は色々ありそうで、みんな知っておくべきだと思ったので書いておこうと思う(追記:そんなに使い道無い気もしてきた)


既に対応済みのGCCと、SDE(x86シミュレータ)があるので、
http://software.intel.com/en-us/articles/using-intel-mpx-with-the-intel-software-development-emulator
これを試せば何がどうなってるかが調べられるかと思う。

$ GCC=http://software.intel.com/en-us/articles/intel-software-development-emulator#gcc からダウンロードしたgcc
$ BINUTILS=http://software.intel.com/en-us/articles/intel-software-development-emulator#gcc からダウンロードしたBINUTILS
$ MPX_RUNTIME=https://secure-software.intel.com/en-us/protected-download/267266/144917 からダウンロードしたlibmpx-runtime
$ SDE=https://secure-software.intel.com/en-us/protected-download/267266/144917 からダウンロードしたSDE

として、

#include <stdio.h>

void loop(int n, char *ptr)
{
    int i;
    for (i=0; i<n; i++) {
        ptr[i] = i;
    }

    printf("%p %d\n", ptr, ptr[0]);
}

int main(int argc, char **argv)
{
    int nloop = 100;
    char array[100];

    if (argc >= 2) {
        nloop = atoi(argv[1]);
    }

    loop(nloop, array);

    return 1;
}

こんな感じのを用意する。


で、コンパイルして実行すると、

 $ $GCC -fmpx -l$MPX_RUNTIME -B$BINUTILS hoge.c
 $ $SDE -mpx-mode -- ./a.out 100
 ok
 $ $SDE -mpx-mode -- ./a.out 101
 Bound violation detected,status 0x1 at 0x400634

こんな感じ。


ポイントは、

  • 既存のバイナリと混ぜても大丈夫 (保護機能は無くなるが一応動く)
  • 未対応CPUではnopと解釈されるので昔のCPUでも一応動く
  • コンパイル時にチェック用の命令を入れてるのでコンパイラの対応が必要

という感じ。

まあ、気分的には、mudflap的なものを高速化できるという理解でいいと思う。


実装は次のようになっている。


まず、128bit レジスタが4本増えてる。 bnd0, bnd1, bnd2, bnd3。上位64bitにupper bound、下位64bitにlower boundが入る。

次に、境界チェック命令が増えている

  • bndcu : bnd レジスタとアドレスをオペランドにとる。アドレスがupper boundを越えていたらBR例外が飛ぶ
  • bndcl : bnd レジスタとアドレスをオペランドにとる。アドレスがlower boundより小さかったらBR例外が飛ぶ

基本的にはこれだけ。(あとビット反転した上限をチェックするbndcnがあるが使いかたよくわからん)

境界レジスタに値をセットするbndmk という命令があって、

    bndmk bnd0, [rax + rdx]  # bnd0 の下限値にrax, 上限値にrax + rdxを設定する

と、いうようにできる。


これまで配列範囲チェックするのを、

    cmp rax, r8
    jg xx
    cmp rax, [r8 + xx]
    jg xx
    mov rcx, [rax]

こう書いてたのが、

    bndmk bnd0, [r8 + x]
    bndcu bnd0, [rax]
    bndcl bnd0, [rax]
    mov rcx, [rax]

こんな風に書けるようになる。いやあんまり節約できてる気がしないが…

  • 分岐リソース使わなくていい
  • レジスタ無駄にしなくていい

というのがメリットだろうか…


で、(多分)ABIに拡張が入っていて、ポインタを引数に渡すときには、その上限下限を一緒に渡すようになっている。

int load(int *p, int *q) {
    return *p + *q;
}

extern void load2(int *p, int *q);

void f() {
    int a[2];

    load2(a, a+1);
}

/*
load:
	bndmov	%bnd0, %bnd2        // なんか無駄なコピーがあるのはコンパイラまだ調整されてないからだと思う
	bndmov	%bnd0, -24(%rsp)
	bndmov	%bnd1, -40(%rsp)
	bndcl	(%rdi), %bnd2       // ポインタと一緒に引数に渡された bnd0, bnd1 を使って境界チェック
	bndcu	3(%rdi), %bnd2
	movl	(%rdi), %eax
	bndcl	(%rsi), %bnd1
	bndcu	3(%rsi), %bnd1
	addl	(%rsi), %eax
	bnd ret

f:
	subq	$24, %rsp
	movl	$7, %eax
	bndmk	(%rsp,%rax), %bnd0  // bnd0 に 境界設定
	leaq	4(%rsp), %rsi
	movq	%rsp, %rdi
	bndmov	%bnd0, %bnd1        // bnd0 を bnd1 にコピー
	bnd call	load2       // bnd0, bnd1 をポインタと一緒にわたす
	addq	$24, %rsp
	bnd ret
*/

という感じである。


あとポインタからポインタの境界を検索する仕組みも付いてる。(これ、超巨大なテーブルが必要な気がするし、実際マニュアルには、「2GB」とか書いてあるんだがいいのかそれで…読み間違いかも)

char *(ptr[10]);

void loop(int n)
{
    int i;
    for (i=0; i<n; i++) {
        ptr[0][i] = i;
    }

    puts("ok");
}

int main(int argc, char **argv)
{
    int nloop = 100;
    char array[100];

    if (argc >= 2) {
        nloop = atoi(argv[1]);
    }

    ptr[0] = array;

    loop(nloop);

    return 1;
}

こんな風に、ポインタの配列を使った場合、ポインタと一緒に境界を持たせるというのが難しい。こういう時は、bndstxとbndldxを使う。

bndstx で ポインタ値と境界を関連付けられる。bndldx でポインタから境界を取得して、境界レジスタに設定できる。