cpuidでAVX2に対応しているか調べる

intelの以下のページに方法とコードが書いてあります.

software.intel.com

AVX2等は以下のCPUIDで調べることができます.

CPUID.(EAX=01H, ECX=0H):ECX.FMA[bit 12]==1
CPUID.(EAX=07H, ECX=0H):EBX.AVX2[bit 5]==1
CPUID.(EAX=07H, ECX=0H):EBX.BMI1[bit 3]==1
CPUID.(EAX=07H, ECX=0H):EBX.BMI2[bit 8]==1
CPUID.(EAX=80000001H):ECX.LZCNT[bit 5]==1
CPUID.(EAX=01H, ECX=0H):ECX.MOVBE[bit 22]==1

以下,検証用コード (とりあえず自分の環境で動けば良かったのでかなり適当です)

#include <stdio.h>
#include <string.h>


struct abcd{
    unsigned int a,b,c,d;
};

void cpuid(struct abcd* r, unsigned int eax, unsigned int ecx){
    __asm__ volatile ("cpuid"
                      :"=a"(r->a), "=b"(r->b), "=c"(r->c), "=d"(r->d)
                      : "a"(eax), "c"(ecx));
}

int main(){
    struct abcd r;
    char buf[48];

    // GenuinIntel
    cpuid(&r, 0x0, 0x0);
    printf("0x%x\n", r.a);
    memcpy(buf, &r.b, 4);
    memcpy(buf+4, &r.d, 4);
    memcpy(buf+8, &r.c, 4);
    buf[12] = '\0';
    printf("%s\n", buf);

    // Processor Name, Freq
    cpuid(&r, 0x80000002, 0x0);
    memcpy(buf, &r, 16);
    cpuid(&r, 0x80000003, 0x0);
    memcpy(buf+16, &r, 16);
    cpuid(&r, 0x80000004, 0x0);
    memcpy(buf+32, &r, 16);
    printf("%s\n", buf);

    // SIMD
    // https://software.intel.com/en-us/articles/how-to-detect-new-instruction-support-in-the-4th-generation-intel-core-processor-family
    cpuid(&r, 0x1, 0x0);
    printf("MMX:    %s\n", r.d & 1 << 23 ? "OK" : "NG");
    printf("SSE:    %s\n", r.d & 1 << 25 ? "OK" : "NG");
    printf("AVX:    %s\n", r.c & 1 << 28 ? "OK" : "NG");
    printf("FMA:    %s\n", r.c & 1 << 12 ? "OK" : "NG");
    cpuid(&r, 0x7, 0x0);
    printf("AVX2:   %s\n", r.b & 1 <<  5 ? "OK" : "NG");

    return 0;
}

実行結果

% gcc-6 cpuid.c
% ./a.out
0x16
GenuineIntel
Intel(R) Core(TM) i7-6567U CPU @ 3.30GHz
MMX:    OK
SSE:    OK
AVX:    OK
FMA:    OK
AVX2:   OK

LinuxのBPF : (2) seccompでの利用

seccompについて

seccomp (Secure Computingの略らしい)は,Linuxにおいてサンドボックスを実現するために プロセスのシステムコールの発行を制限する機能です. seccompを使っている代表的なアプリケーションにはchromeやOpen SSHなどがあります.最近利用が増えてきているようです.

seccompはLinux 2.6.12 (2005)から導入されました. このときseccompは/proc/pid/seccompを1にするとread()write()exit()sigreturn()のみを許可し, それ以外のシステムコールを呼ぶとSIGKILLするというものでした.

その後,もう少し柔軟にシステムコールを制限できるようにしようという議論があり,2012年のLinux3.5からseccomp mode 2という新しい seccompが導入されました.この新しいseccompではシステムコール単位に制限ができる他,あ るシステムコールに関してある特定の引数の場合だけ許可,といったこともできるようになりました.

さて,そんなseccompですが,BPFとどんな関係があるのかというと,実はシステムコールのフィルタリングにBPFを利用しています(!). seccompでの実際のプログラムを見てみましょう.

seccompのプログラム

seccomp(2)にseccompの使い方が書いてありますが, Linux3.7から追加されたseccompシステムコール

seccomp(SECCOMP_SET_MODE_FILTER, 0, &prog)

とするか,prctrlを使って

prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog))

とすることでseccompの設定ができます.ここでprogがBPFのフィルタプログラムです.

以下にgetpid()システムコールだけ許可しないよう設定する例を示します(特に意味はないです).

#include <errno.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <linux/audit.h>
#include <linux/filter.h>
#include <linux/seccomp.h>
#include <linux/unistd.h>
#include <sys/prctl.h>
#include <sys/types.h>
#include <sys/syscall.h>
#include <sys/ptrace.h>

#ifndef seccomp
int seccomp(unsigned int op, unsigned int flags, void *args)
{
    errno = 0;
    return syscall(__NR_seccomp, op, flags, args);
}
#endif

struct sock_filter filter[] = {
    BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
            (offsetof(struct seccomp_data, arch))),
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 1, 0),
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
    BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
            (offsetof(struct seccomp_data, nr))),
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_getpid, 0, 1),
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
};

struct sock_fprog prog = {
    .len = (unsigned short) (sizeof(filter) / sizeof(filter[0])),
    .filter = filter,
};


int main(){
    int err;

    if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
        perror("prctl");
        exit(EXIT_FAILURE);
    }

    //if(prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog)){
    if(seccomp(SECCOMP_SET_MODE_FILTER, 0, &prog)){
        perror("seccomp");
    }

    pid_t pid = getpid();
    printf("%d\n", pid);


    return 0;
}

BPFフィルタはパケットフィルタリングと同じように,sock_filter構造体の配列として定義されます. ここで,パケットフィルタリングと異なるのは,パケットフィルタの場合はロード命令でパケットのデータにアクセスできたのに対し, seccompの場合は以下のseccomp_data構造体 のデータにアクセスできるという点です.

struct seccomp_data {
    int nr ; /* System call number */
    __u32 arch ; /* AUDIT_ARCH_ * value */
    __u64 instruction_pointer ; /* CPU IP */
    __u64 args [6]; /* System call arguments */
}

seccompのフィルタリングプログラムについて詳しくみていきましょう.

    BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
            (offsetof(struct seccomp_data, arch))),
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 1, 0),
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
    BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
            (offsetof(struct seccomp_data, nr))),
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_getpid, 0, 1),
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),

offsetof()命令を使うことで構造体から配列へのオフセットが計算できます.した がって最初の命令はseccomp_dataarchの値をロードしています.そして,archx86かどうかを確認しています. 最初にアーキテクチャを確認するのは,アーキテクチャごとにシステムコール番号が異なるなどの理由からです.その後, seccomp_datanrをロードし,getpidであればSECCOMP_RET_KILLを,そうでなければSECCOMP_RET_ALLOWを返します. このプログラムを実行してみると,bashなら

Bad system call

と表示され,getpidを実行しようとした時点でプログラムが終了します. なお,seccompを使う場合はseccompを適用する前にprctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)としてsetuidなどを無効化する必要があります.またseccompで設定したフィルタやforkexecv後も引き継がれ,解除できません.

こうしてみると,確かにBPFを使ってシステムコールのフィルタリングができていることが分かります.これだけならあまり BPFを使う理由はないように思うかもしれませんが,ある特定の引数だけ許可したいとなると,確かにBPFのプログラムで効率的 に処理できるような気がしてきたりしないでしょうか. また,seccomp_dataにはinstruction_pointerがあり,これと/proc/pid/mapsを組み合わせれば特定の関数からのみ システムコールを許可するといったことも可能になります.

特定のシステムコールのみ許可する

BPFとは少々関係ない話になりますが,seccompの使い方について折角なのでいくつか書いておこうと思います.

先ほどのプログラムではgetpid以外を許可しましたが,実際にはブラックリスト形式 ではなくホワイトリスト形式で必要なシステムコールだけを許可した方がセキュリティ的には安全でしょう.

そこで,先ほどの例でgetpidのみを許可することを考えてみましょう.BPF_JUMPの分岐先を入れ替えればいいだけ,と思うかも しれませんが,以下のフィルタプログラムは期待どおりには動きません.

    BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
            (offsetof(struct seccomp_data, arch))),
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 1, 0),
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
    BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
            (offsetof(struct seccomp_data, nr))),
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_getpid, 1, 0),
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),

というのもプログラムは実際にはgetpid以外のシステムコールを発行しているからです.実際にプログラムがどんなシステムコール を発行しているかはstraceを使えばわかります.

% strace ./a.out
...
getpid()                                = 15240
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 4), ...}) = 0
brk(NULL)                               = 0x1342000
brk(0x1363000)                          = 0x1363000
write(1, "15240\n", 615240
)                  = 6
exit_group(0)                           = ?

このプログラムではgetpidを読んだ後,fstatbrkwriteexit_groupを使用していることが分かりました. これらのシステムコールを許可するプログラムは例えば以下のようになります.

struct sock_filter filter[] = {
    BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
            (offsetof(struct seccomp_data, arch))),
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 1, 0),
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
    BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
            (offsetof(struct seccomp_data, nr))),
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_getpid, 0, 1),
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_fstat, 0, 1),
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_brk, 0, 1),
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_write, 0, 1),
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_exit_group, 0, 1),
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
};

ここに書いてあるように,いくつかマクロを定義すれば多少ましにフィルタプログラムが定義できます.

#define ARCH_NR AUDIT_ARCH_X86_64
#define syscall_nr (offsetof(struct seccomp_data, nr))
#define arch_nr (offsetof(struct seccomp_data, arch))

#define VALIDATE_ARCHITECTURE \
     BPF_STMT(BPF_LD+BPF_W+BPF_ABS, arch_nr), \
     BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, ARCH_NR, 1, 0), \
     BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_KILL)

 #define EXAMINE_SYSCALL \
     BPF_STMT(BPF_LD+BPF_W+BPF_ABS, syscall_nr)

 #define ALLOW_SYSCALL(name) \
     BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, __NR_##name, 0, 1), \
     BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ALLOW)

 #define KILL_PROCESS \
    BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_KILL)


struct sock_filter filter[] = {
    VALIDATE_ARCHITECTURE,
    EXAMINE_SYSCALL,
    ALLOW_SYSCALL(getpid),
    ALLOW_SYSCALL(brk),
    ALLOW_SYSCALL(write),
    ALLOW_SYSCALL(fstat),
    ALLOW_SYSCALL(exit_group),
    KILL_PROCESS,
}

libseccomp

マクロを使えば多少ましになりますが,それでもseccompのフィルタプログラムを書くのは面倒かつ間違いやすいです. パケットフィルタリングでtcpdumpが使えたように,seccompのフィルタプログラムを簡単に書くためにlibseccomp というライブラリがあります.libseccompを使うと,上記のフィルタリングプログラムは以下のようにかけます. 自分でフィルタを直接書くよりかはましだと思います.

#include <stdio.h>
#include <fcntl.h>
#include <seccomp.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>

#define BUF_SIZE    256

int main(int argc, char *argv[])
{
    int rc = -1;
    scmp_filter_ctx ctx;
    struct scmp_arg_cmp arg_cmp[] = { SCMP_A0(SCMP_CMP_EQ, 2) };
    int fd;
    unsigned char buf[BUF_SIZE];

    ctx = seccomp_init(SCMP_ACT_KILL);
    if (ctx == NULL) goto out;

    rc = seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(getpid), 0);
    if (rc < 0) goto out;
    rc = seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(brk), 0);
    if (rc < 0) goto out;
    rc = seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0);
    if (rc < 0) goto out;
    rc = seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 1,
            SCMP_CMP(0, SCMP_CMP_EQ, 1));
    if (rc < 0) goto out;
    rc = seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(fstat), 0);
    if (rc < 0) goto out;

    rc = seccomp_load(ctx);
    if (rc < 0) goto out;

    pid_t pid = getpid();
    printf("%d\n", pid);

out:
    seccomp_release(ctx);
    return -rc;
}

主な使い方はまずseccomp_init()でコンテキストを初期化したあと,seccomp_rule_addでルールを追加し, 最後にseccomp_laod()で実際に条件にあったフィルタを生成し,設定します.

あるシステムコールを許可したいだけなら

    rc = seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(getpid), 0);

と書けばいいですが,引数までチェックしたい場合は,

    rc = seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 1,
            SCMP_CMP(0, SCMP_CMP_EQ, 1));

のようにします.ここではwriteシステムコールはfdが1(つまりstdout)のときのみ許可しています.詳細は seccomp_rule_addmanページにあります

なお,libseccompのソースを少し追ってみましたが,どうやら生成したBPFプログラムを確認する方法はソースをいじらない限りないようです.

ここまでのまとめ

  • seccompはLinuxにおいてシステムコールの発行を制限するための機能
  • Linux3.5から導入されたseccomp mode 2ではシステムコールの細かい制御が可能となったが,バックエンドではBPFを利用している
  • libseccompというseccompのフィルタプログラムを書くためのライブラリがある

次回はBPFの内部処理について見ていきたいと思います.

参考文献

LinuxのBPF : (1) パケットフィルタ

はじめに

BPFはBerkeley Packet Filterの略で,1993年に効率的なパケットフィルタリング手法として提案されました[1] *1. BPFの構成要素は大きく2つあって,一つがネットワークからパケットをキャプチャする部分,そしてもう一つがキャプチャした パケットをフィルタリングする部分です.BPFといった場合,後者のフィルタリング機構だけを指すことも多いです. FreeBSDなどのBSD系のOSでは/dev/bpf*という特別なデバイスがあって,このBPFを利用することができます.また, Linuxでもパケットフィルタリング(Linux Sokcet Filtering; LSF)にBPFのフィルタリングをサポートしています*2

そんなBPFですが,Linuxではここ数年で進化を遂げ,パケットフィルタリング以外の用途にも使われるようになりました. 例えば,プロセスのサンドボックス化のために発行できるシステムコールを制限するseccomp[2]システムコールをフィルタリングするためにBPFを利用しています.さらに,Linux 3.15からkprobe[3]とBPFを合わせて利用することが可能となり,カーネル内での様々なイベントに対してユーザが定義した処理をおこなうことができます*3.例えば, iovisorが作成しているbccはBPFを活用したダイナミックトレースツール群で, ディスクI/Oのレイテンシの測定,新しいプロセスの実行の監視,TCPコネクション情報 の取得,スタックプロファイリング機能等々,さまざまなことができます.

BPFは元々パケットフィルタリング機構だったはずなのにいったいどういうことやねん...ということでLinuxにおけるBPFについて ここではその機構と何故そのようなことが可能なのかを説明しようと思います.そのためにまずオリジナルのBPFとLinuxでBPF を利用したフィルタリングについて説明したあと,seccompでのBPFの使われ方を説明し,最後に最近のBPFを利用したトレースについて説明します.

まず始めはBPFでのパケットフィルタリングについて見ていきます.

BPFの基礎

まずはじめにオリジナルのBPF論文について説明します.

BPFの構造

BPFの構造は以下のようになっています.

f:id:mm_i:20160801025727p:plain:w400 (論文より引用)

BPFの基本的な使い方は以下の通りです.

  1. ユーザのプロセスが/dev/bpf*を開く.図に示したようにネットワークデバイスとプロセスがBPFを挟んで接続される.
  2. ユーザプロセスはプロセスごとにフィルタリングプログラム(次で説明)を設定する.
  3. バイスがパケットを受信すると,それがBPFの機構に送られ,フィルタリングされた結果がバッファにたまる.
  4. ユーザはデバイスをreadすることでパケットを受信する.

カーネル内でフィルタリングされるため,無駄なコピーが発生せず効率的です. さらに,フィルタリング手法も次に説明するように効率的なものになっています.

BPFでのフィルタリング

BPF以前のパケットフィルタリング手法として,代表的なものにCMU/Stanford PacketFilter(CSPF)[4]がありました.CSPFではフィルタリングは論理演算(木構造)で表現します.一方BPFではフィルタリングをCFG(Control Flow Graph)として表現します.以下にCSPFとBPFでのフィルタの表現例を示します.

f:id:mm_i:20160801025730p:plain:w300 (論文より引用)

CSPFのような木構造の表現は一般にスタックマシンで評価することができます.しかし,一般にスタックはメモリ上に置かれるため評価のために必要なメモリアクセスが増えることや(速度低下の原因),非効率的なパケットのパースが何度も発生するなどといった問題がありました.また,CSPFには固定オフセット位置の要素にしかアクセスできないといった問題もありました.

BPFはこれらの問題を解決し,汎用的かつ効率的なフィルタリングをおこなうために,仮想的なレジスタマシンを提案しました.

レジスタマシン

BPFで利用するレジスタマシンの命令セットは64bit固定で,フォーマットは以下の通りです.

   opcode:16 jt:8 jf:8 k:32

jtが分岐命令において条件が真のときの分岐先,jfが偽の時の分岐先です.kは命令ごとにしようする目的が変わります.

このマシンは32bitのレジスタ2つ(AとX)と作業用のメモリおよび処理対象のパケットのデータにアクセスすることが可能です.Aがアキュムレータ,Xはインデックスレジスタとして利用します. 命令セットは 1) ストア,2) ロード,3) 算術演算,4) 分岐,5) return,6) その他 の6種類22命令からなります.

f:id:mm_i:20160801025725p:plain:w300 (論文より引用)

例えば,リンクレイヤーでEthernetを使っているデバイスでARPパケットのみを受理するフィルタプログラムは以下のようになります.

ldh [12]           // パケット先頭12byte目から2byteロード
jne #0x806, drop   // 読み込んだ値が0x806でなければdropへジャンプ
ret #-1
drop: ret #0

イーサネットフレームはpreambleを除くと先頭から12byteから2byteがtype/lengthフィールドで,その値が0x806のときパケットがARPであることを示します[5].戻り値が0の場合そのパケットは棄却されます.

BPFでフィルタリングする際は,このようにユーザがBPFのプログラムを作成し設定するわけですが,BPFのプログラムはカーネル内で実行 されるため,プログラムは安全である必要があります.そこで, 安全性確保のために例えば負の方向に対するジャンプは禁止されています(無限ループを防止するため).カーネルにはBPFプログラムを実行して安全かどうかを事前に静的に解析して検証する機能が入っています.

以下,BPFといった場合BPFのフィルタリング機構について指すことにします.

LinuxでのBPF

Linuxには/dev/bpf*は存在せず, Linuxでパケットキャプチャをする場合はソケットのプロトコルファミリにPF_PACKETを指定することでおこないますが[6]カーネル内でパケットのフィルタリングするためにLinux Socket Filtering (LSF) という仕組みが用意されています[7]. このLSFのフィルタリングアルゴリズムは実はBPF(を拡張したもの)です.

LinuxのBPFに関してはカーネルのドキュメント Documentation/networking/filter.txt [7]にまとまっていますので, 一読を勧めます. またBPFプログラムの仕様はBSDのものを引き継いでいるので,FreeBSDのbpf(4)のmanページ[8]も参考になります.

BPFを利用したパケットフィルタリング

LinuxでLSF(BPF)を利用したパケットフィルタリングの例を見てみましょう.以下のプログラムはパケットをキャプチャしてそれが IPパケットかARPパケットかそれ以外かを出力するだけのプログラムです*4

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <arpa/inet.h>
#include <linux/if_ether.h>
#include <linux/filter.h>
#include <netpacket/packet.h>
#include <net/if.h>

int main(){
    int soc;
    struct ifreq ifr;
    struct sockaddr_ll sll;
    unsigned char buf[4096];

    memset(&ifr, 0, sizeof(ifr));
    memset(&sll, 0, sizeof(sll));

    soc = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));

    strncpy(ifr.ifr_name, "br0", IFNAMSIZ);
    ioctl(soc, SIOCGIFINDEX, &ifr);

    sll.sll_family = AF_PACKET;
    sll.sll_protocol = htons(ETH_P_ALL);
    sll.sll_ifindex = ifr.ifr_ifindex;
    bind(soc, (struct sockaddr *)&sll, sizeof(sll));

    while(1){
        ssize_t len = recv(soc, buf, sizeof(buf), 0);
        struct ethhdr* ethhdr = (struct ethhdr*)buf;
        int proto = ntohs(ethhdr->h_proto);
        if(len <= 0) break;
        printf("%3ld %0x %s\n", len, proto,
                proto==ETH_P_ARP ? "arp" : proto==ETH_P_IP ? "ip" : "other");
    }
    return 0;
}

ここで,BPFプログラムを使ってARPパケットのみを受理するようにしてみましょう.そのためには,setsockopt(2)システムコール[8]を 以下のように利用して,ソケットのfdに対してBPFプログラムをアタッチします.

setsockopt(sockfd, SOL_SOCKET, SO_ATTACH_FILTER, &bpf, sizeof(bpf));

ここで,bpf<linux/filter.h> の中で定義されているsock_fprog構造体です.

struct sock_fprog { /* Required for SO_ATTACH_FILTER. */
    unsigned short      len;    /* Number of filter blocks */
    struct sock_filter *filter;
};

struct sock_filter {    /* Filter block */
    __u16   code;   /* Actual filter code */
    __u8    jt; /* Jump true */
    __u8    jf; /* Jump false */
    __u32   k;      /* Generic multiuse field */
};

このように,BPFプログラムの実体はsock_filter構造体の配列です. BPF命令のコードは<linux/bpf_common.h>でdefineされています. また,BPF命令列はマクロを使って定義することができます(もちろんハンドアセンブルしたい人はすればいいですが..).

struct sock_filter code[] = {
    BPF_STMT(BPF_LD | BPF_H | BPF_ABS, 12),  // ldh [12]
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 0x806, 0, 1), // jeq #0x806 jt 0 jf 1
    BPF_STMT(BPF_RET | BPF_K, -1), // ret #-1
    BPF_STMT(BPF_RET | BPF_K, 0) // ret #0
};

struct sock_fprog bpf = {
    .len = sizeof(code)/sizeof(code[0]),
    .filter = code,
};

マクロについては前述のドキュメント[7][8]に詳しくは書いてあります.

以上をまとめると,フィルタリングプログラム全体は以下のようになります.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <arpa/inet.h>
#include <linux/if_ether.h>
#include <linux/filter.h>
#include <linux/kernel.h>
#include <netpacket/packet.h>
#include <net/if.h>

struct sock_filter code[] = {
    BPF_STMT(BPF_LD | BPF_H | BPF_ABS, 12),  // ldh [12]
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 0x806, 0, 1), // jeq #0x06 jt 2 jf 3
    BPF_STMT(BPF_RET | BPF_K, -1), // ret #-1
    BPF_STMT(BPF_RET | BPF_K, 0) // ret #0
};

struct sock_fprog bpf = {
    .len = sizeof(code)/sizeof(code[0]),
    .filter = code,
};

int main(){
    int soc;
    struct ifreq ifr;
    struct sockaddr_ll sll;
    unsigned char buf[4096];

    memset(&ifr, 0, sizeof(ifr));
    memset(&sll, 0, sizeof(sll));

    soc = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));

    strncpy(ifr.ifr_name, "br0", IFNAMSIZ);
    ioctl(soc, SIOCGIFINDEX, &ifr);

    sll.sll_family = AF_PACKET;
    sll.sll_protocol = htons(ETH_P_ALL);
    sll.sll_ifindex = ifr.ifr_ifindex;
    bind(soc, (struct sockaddr *)&sll, sizeof(sll));

    setsockopt(soc, SOL_SOCKET, SO_ATTACH_FILTER, &bpf, sizeof(bpf));

    while(1){
        ssize_t len = recv(soc, buf, sizeof(buf), 0);
        struct ethhdr* ethhdr = (struct ethhdr*)buf;
        int proto = ntohs(ethhdr->h_proto);
        if(len <= 0) break;
        printf("%3ld %0x %s\n", len, proto,
                proto==ETH_P_ARP ? "arp" : proto==ETH_P_IP ? "ip" : "other");
    }
    return 0;
}

このプログラムを実行すると,ARPのパケットを受信したことしか出力されないはずです. ここで重要なことは,フィルタリングがカーネル空間で行われていること,またユーザが定義したプログラムがカーネル内で動作していることです.

libpcapとBPF

先ほどはフィルタプログラムはマクロを使って定義しましたが,パケットキャプチャライブラリとして有名なlibpcapはパケットのフィルタリングに BPFを利用しており,実はtcpdumpを使うとフィルタ式をBPFのプログラムにコンパイルすることができます(というかそもそもtcpdumpがBPFを最初に使ったプログラムだったぽい) *5

% sudo tcpdump  -d arp
(000) ldh      [12]
(001) jeq      #0x806           jt 2    jf 3
(002) ret      #262144
(003) ret      #0

% sudo tcpdump  -dd arp
{ 0x28, 0, 0, 0x0000000c },
{ 0x15, 0, 1, 0x00000806 },
{ 0x6, 0, 0, 0x00040000 },
{ 0x6, 0, 0, 0x00000000 },

% sudo tcpdump  -ddd arp
4
40 0 0 12
21 0 1 2054
6 0 0 262144
6 0 0 0

BPFでフィルタリングしたいのなら普通はこのコンパイル結果を埋め込めばいいでしょう.最適化もしてくれます. また,libpcapのpcap_compileという関数でプログラム内でコンパイルすることもできます.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <arpa/inet.h>
#include <linux/if_ether.h>
#include <linux/filter.h>
#include <netpacket/packet.h>
#include <net/if.h>

#include <pcap/pcap.h>
#include <pcap/bpf.h>

int main(){
    int soc;
    struct ifreq ifr;
    struct sockaddr_ll sll;
    unsigned char buf[4096];

    memset(&ifr, 0, sizeof(ifr));
    memset(&sll, 0, sizeof(sll));

    soc = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));

    strncpy(ifr.ifr_name, "br0", IFNAMSIZ);
    ioctl(soc, SIOCGIFINDEX, &ifr);

    sll.sll_family = AF_PACKET;
    sll.sll_protocol = htons(ETH_P_ALL);
    sll.sll_ifindex = ifr.ifr_ifindex;
    bind(soc, (struct sockaddr *)&sll, sizeof(sll));

    struct bpf_program bpf;
    pcap_t *handle;
    handle = pcap_open_live("br0", 4096, 1, 1000, buf);
    pcap_compile(handle,&bpf,"arp",1,PCAP_NETMASK_UNKNOWN);
    setsockopt(soc, SOL_SOCKET, SO_ATTACH_FILTER, (struct sock_fprog*)&bpf, sizeof(bpf));

    while(1){
        ssize_t len = recv(soc, buf, sizeof(buf), 0);
        struct ethhdr* ethhdr = (struct ethhdr*)buf;
        int proto = ntohs(ethhdr->h_proto);
        if(len <= 0) break;
        printf("%3ld %0x %s\n", len, proto,
                proto==ETH_P_ARP ? "arp" : proto==ETH_P_IP ? "ip" : "other");
    }
    return 0;
}

...まぁそもそもlibpcap使うのならキャプチャ用に便利な関数がいろいろあるのでそれを使った方がいいと思いますが. libpcapにはBPFプログラムのインタプリタも付属していて,ユーザ空間でパケットのフィルタリングをしたい場合に使えます.

また,Linuxのソースのtools/net以下にもbpfのアセンブラなどのツールがあります.これらの使い方は ドキュメント[7]に書いてあります.

ここまでのまとめ

  1. BPFというパケットフィルタリング機構がある.
  2. BPFはパケットキャプチャ部分とフィルタリング部分に分けられ,フィルタリングは仮想的なレジスタマシンを使用する.
  3. LinuxLinux Socket FilterはBPFでフィルタリングをおこなう.
  4. Linuxではsetsockopt(2)でソケットに対してフィルタリングをするBPFプログラムを設定できる.
  5. tcpdump(libpcap)を使ってBPFプログラムはコンパイルできる.

次回はseccompがBPFをどのように利用しているかについて書こうと思います.


参考文献

その他参考URL

*1:論文が出たのが93年で実際にはその2年ぐらい前から存在していたようです

*2:ここでBPFはBPFのフィルタリング機構のことを指しています

*3:Linux 4.7からperfのeventもサポートされました

*4:良い子はエラーハンドリングしましょう

*5:BPFプログラムのコードについて,統一的な仕様は私は知りませんが,LinuxもBSDも同じものを利用しているようです

LaTeX-suite じゃない vim-latex を使ってvimによるLaTeX作成環境を整える

2017-1-7 追記 このページに今でも検索から辿り着く方がいるようなので追記. 以下で説明しているvim-latexですが,Latex-Suiteと名前が被っているということで2015年3月にvimtexに名称が変わりました.

github.com

それに伴い変数のprefixがg:latex_... = ... から g:vimtex_... = ... に変更されています. 他にもいろいろと機能が追加・更新されていますので,使用する際はhelpの一読をお勧めします.

どうでもいい話

vimでのLaTeX文章作成を補助するプラグインとして,代表的なものに LaTeX-suite a.k.a. Vim-LaTeXがあります. 自分も遥か前にこんな記事を 書いてしばらく使っていたんですが,いかんせんLaTeX-suiteは巨大すぎて, 何をやっているのかよく分からないとこも多かったため,結局使わなくなってしまいま した.

それからは自分があまりプラグインを使用しない派になったこともあって,特に何も使わ ずにtexを書いていましたが,最近emacsAUCTeX というものがあることを知りました. AUCTeXで羨ましいなと思ったのは, 数式のプレビュー機能です. vimではこんな風に画像は扱えないのでtexを書くときだけemacsを使おうかなと思っちゃ うぐらい魅力的でした.実際Evilを使って Emacsを試してみたんですが,自分には半日と持ちませんでした..

で,数式のプレビューとは言わなくてもtex文章作成は時間がかかるので,やっぱり何か 良いプラグインが無いかなと思って久しぶりに探してみ たところ, https://github.com/lervag/vim-latex を発 見しました.

本題

名前が紛らわしいですが,LaTeX-suiteではないvim-latexというものがあります (以下でvim-latexといったらこのプラグンのことです).

https://github.com/lervag/vim-latex

vimLaTeX文章作成をサポートするためのプラグインです.主な機能としては,

  • latexmkを利用したコンパイル
  • 分割したtexファイルのサポート
  • 自動コンパイル
  • \refや\citeのオムニ補完
  • アウトライン表示
  • 新しい インデント / 折りたたみ

などがあります.インストール方法はgithubのドキュメントに書いてあるとおりです. このプラグインは自分で設定が変更しやすくなっていますし,プラグイン自体も読みやす いと思います.また,ヘルプがしっかりしている点も良いです.

ちなみに,tex自体の補完やスニペットを使いたいという場合は,何か別のプラグイン (例えばneocomplete, ultisnips, neosnippet, emmet-vimなど)を使うことになります.

また,自分は使ったことがありませんが他のLaTeXプラグインとしては LaTeX-Boxなどもあります.

基本的な使い方

vim-latexはhelpが充実していますので,とりあえずヘルプを読みましょう.

なんでもいいから設定例が知りたいという場合はこの文章の末尾に自分の設定例がありま す.

代表的なコマンド / マッピング

いろいろコマンドやマッピングがありますが,とりあえずよく使いそうなもの

  • コマンド(()内はデフォルトマッピングです)

    • VimLatexHelp (<localleader>lh) : vim-latex関連の現在のマッピング設定が表示されます.
    • VimLatexStatus (<localleader>lg) : latxmkのステータス(実行中かどうかなど)を表示
    • VimLatexStop (<localleader>lk) : 現在のバッファのlatexmkを停止します
    • VimLatexClean (<localleader>lc) : latexmk -c を使ってファイルを削除します.
    • VimLatexCompileToggle (<localleader>lc) : latexmk を使ってファイルをコンパイル / 停止
    • VimLatexErrors (<localleader>le) : コンパイル時のエラーをquickfixに表示
    • VimLatexOutput (<localleader>lo) : コンパイルの出力を表示
    • VimLatexTocOpen (<localleader>lt) : アウトラインの表示
    • VimLatexView (<localleader>lv) : ビューワを用いて生成したファイルを開く
  • オペレータ

    • a$ / i$ : $..$ 内を選択
    • ae / ie : environmentを選択
    • ad / id : delimiterを選択
    • [[ / ]] : セクション開始位置 / セクション終了位置まで選択
  • その他主要マッピング

    • [[ / ]] (normal mode) : 前のセクション / 次のセクションに移動
    • % (normal mode) : begin / end 間の移動
    • dse (normal mode) : 現在のenvironmentを削除します
    • cse (normal mode) : 現在のenvironmentを変更します(プロンプトが出る)
    • tse (normal mode) : 現在のenvironmentの*をトグルしますks
    • ]] (insert mode) : environmentを閉じます
    • <C-x><C-o> (insert mode) : \refや\citeを入力中の場合補完します

基本的な使い方としては<localleader>lltexファイルをコンパイル<localleader>le<localleader>loで出力をチェックし, <localleader>lvでビューワで確認といった感じになると思います.

ちなみにlocalleaderのデフォルトはバックスラッシュだと思います. 自分で変更したい場合は.vimrcでlet maplocalleader = "\<Space>"とかすればできま す.

latexmkによるコンパイル

latexmkというのは platex hoge.tex && platex hoge.tex && dvipdfmx hogeみたいな 処理を自動でおこなってくれるコマンドです.TexLive等でインストールすれば最初から 使えます.vim-latexではこのlatexmkを使ってtex文章のコンパイルをおこないます. latexmkを使用する場合には,latexmk用の設定ファイルが必要です. unix系なら~/.latexmkrcに以下のようなファイルを作っておきます

$latex='platex -kanji=utf8 -guess-input-enc -synctex=1 -interaction=nonstopmode %S';
$dvipdf='dvipdfmx  %S';
$bibtex='pbibtex -kanji=utf8 %B';

platexのsynctex=1のオプションは後述するSyncTeXを使用するのに必要です. さて,以上のように設定すると,あるTeX文章があったときにlatexmk -pdfdvi hoge.texとすればplatex hoge && dvipdfmx hogeとしてpdfを生成してくれます. latexmkのオプションは以下のように.vimrcに設定します.

let g:latex_latexmk_options = '-pdfdvi'

texファイルが分割されている場合

texファイルを\input{}命令を使って分割するということがあると思います. vim-latexではそのような場合に最初から対応していて,再帰的に親ディレクトリ を遡って親のtexファイルを探してくれます.特に何か設定する必要はありません.

つまり,例えば以下のような構成に対応できます.

main.tex
chapter1/chapter1.tex
chapter2/chapter2.tex
...

main.tex内で\input{}命令を使ってchapter1/chapter1.texなどを取り込みます. ただし,以下のような場合は駄目です.

a/main.tex
b/chapter1.tex

このようなファイル構成に対応したい場合はtexファイルの先頭に%! TEX root = /path/to/main.texのように記述します.

自動コンパイル

g:latex_latexmk_continuousを1に設定し,:VimLatexCompileToggle (<localleader>ll)をすると,ファイルが上書きされると自動的にlatexmkを実行す るようになります(停止する場合は再び:VimLatexCompileToggle).この場合自動更新に 対応しているビューワを使うとさらに便利です.ただし,大きいファイルを編集している場合には この機能はオフにした方がいいかもしれません.

この機能を使用するためには+clientserver機能のあるvimが必要です.端末のvimで実行 すると自動的にgvimが起動します.

自動コンパイルをおこなうプラグインとしてはvim-latex-live-preview などがありますが,こちらは+pythonが必要です.

\refや\citeの補完

\refや\citeを入力中に<C-x><C-o>を押すと適当に補完してくれます. bibtexにも対応しています.ただ補完時に候補を集めるため\citeはちょっと重いです.

アウトライン表示

:VimLatexTocOpen (<localleader>lt) を実行すると,現在編集中のファイルのア ウトラインを表示してくれます.ファイルが分割されていてもokです.

ビューワの設定

使用するビューワは以下のように設定します.今自分の作業PCはMacなので Skimを 指定しています.

let g:latex_view_method = 'general'
let g:latex_view_general_viewer = '/Applications/Skim.app/Contents/MacOS/Skim'

:VimLatexViewをしたとき,コンパイルしたファイルと同名のpdfファイルがあれば pdfファイルを,dviファイルがあればdviファイルを開きます.

折り畳み

vim-latexにはfoldの設定ファイルがついてきます. vimのfoldのキーマッピングはいろ いろありますが,とりあえずzRで全ての折りたたみを展開,zMで全ての折りたたみ を閉じます. 折りたたみをオフにしたい場合にはlet g:latex_fold_enabled = 0とし ます.

SyncTexの設定

SyncTex という機能を使うと,ソースコードと生成したfファイルの位置の対応付けをお こなうことができるようになります.SyncTexを使う場合にはlatexmkのオプションで指定 します.

pdfファイルでSyncTeXが使えるかどうかはビューワによります. MuPDF, SumatraPDF, okular, SkimなどがSyncTexに対応しています.

試してないですがvim-latexはMuPDF, SumatraPDFにデフォルトで対応しているようです. また,helpにokularの設定例が載っています.

Skimの場合以下のようにすれば<localleader>lsでSyncTeXを使ってカーソル位置 の文章を検索ができます.

function! s:syncTexForward()
  call system('/Applications/Skim.app/Contents/SharedSupport/displayline -g '
    \ . line(".") . " "
    \ . g:latex#data[b:latex.id].out() . " "
    \ . expand('%:p'))
endfunction
autocmd FileType tex
            \ nnoremap <buffer> <localleader>ls :call <SID>syncTexForward()<CR>

TeXの一部分だけプレビューする

.vimrcに以下のように設定して,ビジュアルモードでプレビューしたい部分を選択して <localleader>laとすれば一部分だけプレビューできるようになります (Windowsの場合は最後のsystem部分を適当に変更する必要があります).

function! s:previewTex() range
    let l:tmp = @@
    silent normal gvy
    let l:selected = split(@@, "\n")
    let @@ = l:tmp

    let l:template1 = ["\\documentclass[a4paper]{jsarticle}",
                      \"\\usepackage[dvipdfmx]{graphicx}",
                      \"\\usepackage{amsmath,amssymb,bm}",
                      \"\\pagestyle{empty}",
                      \"\\begin{document}"]
    let l:template2 = ["\\end{document}"]

    let l:output_file = "preview.tex"
    call writefile(extend(extend(l:template1, l:selected), template2), l:output_file)
    silent call system("latexmk -pdfdvi preview &")
endfunction
autocmd FileType tex
            \   nnoremap <buffer> <localleader>la :call latex#motion#next_section(0,1,0)<CR>v:call latex#motion#next_section(0,0,1)<CR>:call <SID>previewTex()<CR>
            \ | vnoremap <buffer> <localleader>la :call <SID>previewTex()<CR>

やっていることは単純で,選択範囲をもとにpreview.texを作成して,それをコンパイル します.また,上の例ではvim-latexの関数を使って,ノーマルモードで <localleader>laを実行した場合現在のセクションをプレビューします.

AUCTeXのように数式を画像で置き換えるということはできませんが,自動更新のある ビューワを使ってpreview.pdfを表示させておくとまぁ便利なんじゃないかと思います.

設定例

自分の設定は以下のようになっています.一部デフォルト設定を明示的に記述していま す.ビューワ等はMac用の設定になっていますので,他環境で使用する場合には適当に 変更が必要です.

なお,vim-latexは2014年現在でも活発に開発が行われていますので,オプション名が変 更あるいは追加される可能性は十分あります.

augroup MyAutoCmd
  autocmd!
augroup END

let g:latex_latexmk_enabled = 1
let g:latex_latexmk_options = '-pdfdvi'

let g:latex_view_method = 'general'
"let g:latex_view_general_viewer = 'open'
let g:latex_view_general_viewer = '/Applications/Skim.app/Contents/MacOS/Skim'

" fold
let g:latex_fold_parts = [
      \ "appendix",
      \ "frontmatter",
      \ "mainmatter",
      \ "backmatter",
    \ ]
let g:latex_fold_sections = [
      \ "part",
      \ "chapter",
      \ "section",
      \ "subsection",
      \ "subsubsection",
    \ ]
let g:latex_fold_enabled = 1
let g:latex_fold_automatic = 1
let g:latex_fold_envs = 0

" 自動コンパイル
let g:latex_latexmk_continuous = 1
let g:latex_latexmk_background = 1
" コンパイル終了後のエラー通知オフ
let g:latex_latexmk_callback = 0

let g:latex_toc_split_pos = "topleft"
let g:latex_toc_width = 10

" SyncTex
function! s:syncTexForward()
  call system('/Applications/Skim.app/Contents/SharedSupport/displayline -g '
    \ . line(".") . " "
    \ . g:latex#data[b:latex.id].out() . " "
    \ . expand('%:p'))
endfunction

" Preview
function! s:previewTex() range
    let l:tmp = @@
    silent normal gvy
    let l:selected = split(@@, "\n")
    let @@ = l:tmp

    let l:template1 = ["\\documentclass[a4paper]{jsarticle}",
                      \"\\usepackage[dvipdfmx]{graphicx}",
                      \"\\usepackage{amsmath,amssymb,bm}",
                      \"\\pagestyle{empty}",
                      \"\\begin{document}"]
    let l:template2 = ["\\end{document}"]

    let l:output_file = "preview.tex"
    call writefile(extend(extend(l:template1, l:selected), template2), l:output_file)
    silent call system("latexmk -pdfdvi preview &")
endfunction

autocmd MyAutoCmd FileType tex
            \   nnoremap <buffer> <Space>la :call latex#motion#next_section(0,1,0)<CR>v:call latex#motion#next_section(0,0,1)<CR>:call <SID>previewTex()<CR>
            \ | vnoremap <buffer> <Space>la :call <SID>previewTex()<CR>
            \ | nnoremap <buffer> <Space>ls :call <SID>syncTexForward()<CR>

" for neocomplete
if !exists('g:neocomplete#sources#omni#input_patterns')
  let g:neocomplete#sources#omni#input_patterns = {}
endif
let g:neocomplete#sources#omni#input_patterns.tex = '\\ref{\s*[0-9A-Za-z_:]*'
"\citeも自動補完するなら
"let g:neocomplete#sources#omni#input_patterns.tex = '\\cite{\s*[0-9A-Za-z_:]*\|\\ref{\s*[0-9A-Za-z_:]*'

自分はなるべく.vimrcに書く派なのでtex filetype用のマッピングをautocmdで色々設定 していますが,それが嫌な人は vim/after/filetype/tex.vimとかに書くといいんじゃないで しょうか.

2017-1-7 追記 今自分は以下のように設定しています.

let g:tex_flavor = "latex"

let g:vimtex_latexmk_enabled = 1
let g:vimtex_latexmk_options = '-pdfdvi'
let g:vimtex_view_method = 'general'
let g:vimtex_view_general_viewer
      \ = '/Applications/Skim.app/Contents/SharedSupport/displayline'
let g:vimtex_view_general_options = '-r @line @pdf @tex'
let g:vimtex_latexmk_callback_hooks = ['UpdateSkim']
function! UpdateSkim(status)
  if !a:status | return | endif

  let l:out = b:vimtex.out()
  let l:tex = expand('%:p')
  let l:cmd = [g:vimtex_view_general_viewer, '-r']
  if !empty(system('pgrep Skim'))
    call extend(l:cmd, ['-g'])
  endif
  if has('nvim')
    call jobstart(l:cmd + [line('.'), l:out, l:tex])
  elseif has('job')
    call job_start(l:cmd + [line('.'), l:out, l:tex])
  else
    call system(join(l:cmd + [line('.'), shellescape(l:out), shellescape(l:tex)], ' '))
  endif
endfunction

let g:vimtex_latexmk_continuous = 1
let g:vimtex_latexmk_background = 1
let g:vimtex_latexmk_callback = 1

let g:vimtex_toc_split_pos = "topleft"
let g:vimtex_toc_width = 10


" for neocomplete
if !exists('g:neocomplete#sources#omni#input_patterns')
  let g:neocomplete#sources#omni#input_patterns = {}
endif
let g:neocomplete#sources#omni#input_patterns.tex = '\\ref{\s*[0-9A-Za-z_:]*'
let g:neocomplete#sources#omni#input_patterns.tex = '\\cite{\s*[0-9A-Za-z_:]*\|\\ref{\s*[0-9A-Za-z_:]*'