最近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
こんな感じ。
ポイントは、
という感じ。
まあ、気分的には、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 でポインタから境界を取得して、境界レジスタに設定できる。