cpuidでAVX2に対応しているか調べる
intelの以下のページに方法とコードが書いてあります.
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_data
のarch
の値をロードしています.そして,arch
がx86かどうかを確認しています.
最初にアーキテクチャを確認するのは,アーキテクチャごとにシステムコール番号が異なるなどの理由からです.その後,
seccomp_data
のnr
をロードし,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で設定したフィルタやfork
やexecv
後も引き継がれ,解除できません.
こうしてみると,確かに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
を読んだ後,fstat
,brk
,write
,exit_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_add
のmanページにあります.
なお,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の構造は以下のようになっています.
(論文より引用)
BPFの基本的な使い方は以下の通りです.
- ユーザのプロセスが
/dev/bpf*
を開く.図に示したようにネットワークデバイスとプロセスがBPFを挟んで接続される. - ユーザプロセスはプロセスごとにフィルタリングプログラム(次で説明)を設定する.
- デバイスがパケットを受信すると,それがBPFの機構に送られ,フィルタリングされた結果がバッファにたまる.
- ユーザはデバイスをreadすることでパケットを受信する.
カーネル内でフィルタリングされるため,無駄なコピーが発生せず効率的です. さらに,フィルタリング手法も次に説明するように効率的なものになっています.
BPFでのフィルタリング
BPF以前のパケットフィルタリング手法として,代表的なものにCMU/Stanford PacketFilter(CSPF)[4]がありました.CSPFではフィルタリングは論理演算(木構造)で表現します.一方BPFではフィルタリングをCFG(Control Flow Graph)として表現します.以下にCSPFとBPFでのフィルタの表現例を示します.
(論文より引用)
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命令からなります.
(論文より引用)
例えば,リンクレイヤーで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]に書いてあります.
ここまでのまとめ
- BPFというパケットフィルタリング機構がある.
- BPFはパケットキャプチャ部分とフィルタリング部分に分けられ,フィルタリングは仮想的なレジスタマシンを使用する.
- LinuxのLinux Socket FilterはBPFでフィルタリングをおこなう.
- Linuxでは
setsockopt(2)
でソケットに対してフィルタリングをするBPFプログラムを設定できる. - tcpdump(libpcap)を使ってBPFプログラムはコンパイルできる.
次回はseccompがBPFをどのように利用しているかについて書こうと思います.
参考文献
- [1]Steven McCanne and Van Jacobson. 1993. The BSD packet filter: a new architecture for user-level packet capture. In Proceedings of the USENIX Winter 1993 Conference Proceedings on USENIX Winter 1993 Conference Proceedings (USENIX'93). USENIX Association, Berkeley, CA, USA, 2-2
- [2]seccomp(2) - Linux manual page
- [3]Kprobe-based Event Tracing
- [4]MOGUL, J.C., RASHID, R.F., AND ACCETTA, M.J. The packet filter: An efficient mechanism for user-level network code. In Proceedings of 11th Symposium on Operating Systems Principles (Austin, TX, Nov. 1987), ACM, pp. 39–51
- [5]Ethernet - The Wireshark Wiki
- [6]packet(7) - Linux manual page
- [7]Linux Socket Filtering aka Berkeley Packet Filter (BPF)
- [8]bpf(4)
- [9]getsockopt(2) - Linux manual page
その他参考URL
LaTeX-suite じゃない vim-latex を使ってvimによるLaTeX作成環境を整える
2017-1-7 追記 このページに今でも検索から辿り着く方がいるようなので追記. 以下で説明しているvim-latexですが,Latex-Suiteと名前が被っているということで2015年3月にvimtexに名称が変わりました.
それに伴い変数のprefixがg:latex_... = ...
から g:vimtex_... = ...
に変更されています.
他にもいろいろと機能が追加・更新されていますので,使用する際はhelpの一読をお勧めします.
どうでもいい話
vimでのLaTeX文章作成を補助するプラグインとして,代表的なものに LaTeX-suite a.k.a. Vim-LaTeXがあります. 自分も遥か前にこんな記事を 書いてしばらく使っていたんですが,いかんせんLaTeX-suiteは巨大すぎて, 何をやっているのかよく分からないとこも多かったため,結局使わなくなってしまいま した.
それからは自分があまりプラグインを使用しない派になったこともあって,特に何も使わ ずにtexを書いていましたが,最近emacsにAUCTeX というものがあることを知りました. 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
vimでLaTeX文章作成をサポートするためのプラグインです.主な機能としては,
などがあります.インストール方法は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>ll
でtexファイルをコンパイル,
<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_:]*'