連載
» 2018年12月10日 05時00分 公開

Berkeley Packet Filter(BPF)入門(2):BPFのアーキテクチャ、命令セット、cBPFとeBPFの違い (1/2)

Linuxにおける利用が急速に増えている「Berkeley Packet Filter(BPF)」について、基礎から応用まで幅広く紹介する連載。今回は、Linuxで用いられるBPFのアーキテクチャなどを説明する。

[味曽野雅史,OSSセキュリティ技術の会]

 Linuxにおける利用が急速に増えている「Berkeley Packet Filter(BPF)」について、基礎から応用まで幅広く紹介する連載「Berkeley Packet Filter(BPF)入門」。初回は、BPFの歴史や概要について解説しました。今回はBPFの基礎として、Linuxで用いられるBPFのアーキテクチャなどを説明します。

 現在Linuxで主として用いられている「eBPF(extended BPF)」はオリジナルの「cBPF(classic BPF)」とは異なり、互換性がありません。しかし、LinuxにeBPFが導入された後も、LinuxからcBPFが完全になくなったわけではありません。cBPFを利用したパケットフィルタリングも可能ですし(もちろんeBPFでもできます)、システムコールフィルター(seccomp)は依然としてeBPFではなくcBPFを入力として受け取ります。

 ただし、内部的にはcBPFのプログラムはeBPFのプログラムに変換されて実行されています。

 以下では、cBPFとeBPF双方のアーキテクチャについて簡単にまとめます。これは今後の連載のレファレンスを意識したものであり、BPFの利用に当たって覚えておく必要はありません。必要に応じて適宜参照してください。また正式なドキュメンテーションは「Linux Socket Filtering aka Berkeley Packet Filter(BPF)」にあります。

cBPFのアーキテクチャ

 cBPFの命令セットは64bit固定長です。フォーマットをC言語で表現すると、下記のようになります。

struct cbpf_insn {
    u16    code;  /* オペコード */
    u8     jt;    /* 条件が真のときの分岐先 */
    u8     jf;    /* 条件が偽のときの分岐先 */
    u32    k;     /* 汎用フィールド */
};

 ジャンプ先のオフセット(jt、jf)はunsignedなので、cBPFでは負の方向へのジャンプができません。つまり、ループ処理はできません。この仕様とプログラムサイズを制限することで、一定時間内でのプログラム実行終了が保証されます。

 仮想マシンは下記のレジスタとメモリ領域を持ちます。レジスタは32bit幅です。

レジスタ名 説明
A アキュムレータレジスタ
X インデックスレジスタ
M[] メモリ領域、32bit幅、サイズ16

 cBPFには下記のような命令があります。

  • ロード命令
    • ld、ldx
    • AレジスタまたはXレジスタにパケットのデータもしくはメモリ領域のデータをロード
  • ストア命令
    • st、stx
    • メモリ領域にAレジスタまたはXジレスタの値をストア
  • ジャンプ命令
    • jmp、ja、jeq、jneq、jne、jlt、jle、jgt、jge、jset
  • 算術命令
    • add、sub、mul、div、mod、neg、and、or、xor、lsh、rsh
  • リターン命令
    • ret
    • Aレジスタの値もしくは即値をリターン

 パケットデータに対するストア命令はありません。

eBPFのアーキテクチャ

 eBPFの命令セットは64bit固定長です。フォーマットをC言語で表現すると、下記のようになります。

struct ebpf_insn {
    u8    code;       /* オペコード */
    u8    dst_reg:4;  /* ディスティネーションレジスタ */
    u8    src_reg:4;  /* ソースレジスタ */
    s16   off;        /* オフセット */
    s32   imm;        /* 即値 */
};

 オフセットや即値はsigned型になっています。従って、cBPFとは異なり、負方向へのジャンプが可能です。プログラムの一定時間以内の動作終了は検証器が検証します。また、「dst_reg」「src_reg」フィールドで使用するレジスタが指定できるようになっています。

 一方で、cBPFにあった「jt」「jf」フィールドはeBPFには存在しません。eBPFのジャンプ命令は、指定した番地へのジャンプあるいはフォールスルーのどちらかになります。

 仮想マシンは下記のレジスタを持ちます。レジスタは64bit幅です。

レジスタ名 説明
R0 汎用レジスタ(戻り値を格納)
R1〜R5 汎用レジスタ(引数レジスタ)
R6〜R9 汎用レジスタ
R10 フレームポインタ(読み出し専用)

 x86_64やAArch64などのアーキテクチャにおいて、eBPFのレジスタは実際のCPUのレジスタに1対1で対応付けられるようになっています。eBPFの呼び出し規約は64bitカーネルで利用されるものと直接対応します。

cBPFとeBPFの違い

 cBPFとeBPFには、下記のような違いがあります。

cBPF eBPF
レジスタ数 2 10
レジスタ幅(bit) 32 64
スタックサイズ(Byte) 16 512
スタックアクセスサイズ(Byte) 4 1、2、4、8
パケットアクセスサイズ(Byte) 1、2、4 1、2、4、8
外部関数呼び出し ×
負方向への分岐 ×
アトミック加算命令 ×

フレームポインタ

 cBPFに存在したメモリ領域の代わりに、eBPFではR10のフレームポインタを利用してプログラム用のスタック(512B)にアクセスできます。

命令数の増加

 eBPFは一部を除きcBPFと同等の命令をサポートする他、以下のような命令が追加されています。

  • 外部関数呼び出し命令
    • 事前に登録済みのカーネル内関数を呼び出す(後述)
  • アトミック加算命令
    • 主にbpf mapのデータ構造内のデータを更新する際に利用する
  • エンディアン変換命令
  • 64bit幅命令
    • 64bit幅でのメモリの読み書き、および64bit即値のロード

 またcBPFでは多くの命令がAレジスタに対するものであったのに対し、eBPFでは「src_reg」「dst_reg」フィールドにより使用するレジスタを柔軟に選択できるようになっています。

外部関数呼び出し

 eBPFでは事前に登録済みのカーネル内関数をBPFプログラムから呼び出すことが可能です。このとき、呼び出し元はR1〜R5に適切に引数を設定して関数を呼び出します。戻り値はR0に格納されます。

 必要であればR0-R5の値は呼び出し前に退避する必要があります。R6〜R9のレジスタは、関数側が退避する規約になっているため、呼び出し元が退避する必要はありません。引数が6以上の関数呼び出しはサポートされていません。

プログラムタイプ

 eBPFは現在ネットワーク用途のみならず、さまざまな用途で利用されています。利用場面に応じて、BPFプログラムが可能な操作(検証器が検証する内容)や呼び出し可能な外部関数は異なります。

 カーネルではBPFの種類を「BPF_PROG_TYPE」で識別しています。カーネル4.18時点で以下のタイプが存在します

enum bpf_prog_type {
    BPF_PROG_TYPE_UNSPEC,
    BPF_PROG_TYPE_SOCKET_FILTER,
    BPF_PROG_TYPE_KPROBE,
    BPF_PROG_TYPE_SCHED_CLS,
    BPF_PROG_TYPE_SCHED_ACT,
    BPF_PROG_TYPE_TRACEPOINT,
    BPF_PROG_TYPE_XDP,
    BPF_PROG_TYPE_PERF_EVENT,
    BPF_PROG_TYPE_CGROUP_SKB,
    BPF_PROG_TYPE_CGROUP_SOCK,
    BPF_PROG_TYPE_LWT_IN,
    BPF_PROG_TYPE_LWT_OUT,
    BPF_PROG_TYPE_LWT_XMIT,
    BPF_PROG_TYPE_SOCK_OPS,
    BPF_PROG_TYPE_SK_SKB,
    BPF_PROG_TYPE_CGROUP_DEVICE,
    BPF_PROG_TYPE_SK_MSG,
    BPF_PROG_TYPE_RAW_TRACEPOINT,
    BPF_PROG_TYPE_CGROUP_SOCK_ADDR,
    BPF_PROG_TYPE_LWT_SEG6LOCAL,
    BPF_PROG_TYPE_LIRC_MODE2,
}

eBPF map

 eBPFでは「eBPF map」と呼ばれるデータ構造が利用可能です。eBPF mapは、BPFプログラムからは外部関数呼び出し機能を利用して、ユーザーアプリケーションからはシステムコールを利用して、それぞれアクセス可能です。

 eBPF mapを利用することで、eBPFプログラムの状態を管理できます。eBPF mapの具体的な説明は、後の連載で、実際に利用する場面で行います。

コンテキスト

 cBPFでは引数は非明示的な形で与えられ、ロード命令を利用してその中身をレジスタに読み込むことが可能です。

 一方eBPFでは、最初にeBPFプログラムが実行されるときR1レジスタに引数が渡されます。これをeBPFでは「コンテキスト」と呼んでいます。非明示的な引数は存在しません。また複数のコンテキストがBPFプログラムに渡されることはありません。

 コンテキストはBPF_PROG_TYPEごとに異なります。例えば、ネットワーキング関連の場合コンテキストにはカーネル内のパケットのデータ構造である「struct sk_buff」が渡されます。

Tail Call

 「Tail Call」は他のBPFプログラムへの遷移を行う機能です。遷移後に遷移元に戻ることはありません。遷移先のBPFプログラムとはスタックフレームを共有します。

 一定時間での終了を保証するために最大のTail Callの回数は32に制限されています。

他のBPF関数の呼び出し

 Linux 4.16およびLLVM 6.0から、他のBPF関数をBPFプログラムから呼び出せるようになりました。呼び出し方法は外部関数呼び出しに準拠します。それ以前では、BPFプログラムから他のBPF関数を呼び出したい場合は、全てインラインで展開する必要がありました。

 BPF関数を呼び出す場合、最大の呼び出しネスト回数は8に制限されています。引数として呼び出し元のframe pointerを渡すことは可能ですが、逆は不可能です。

 この機能はTail Callと合わせて利用することはできません。

       1|2 次のページへ

Copyright © ITmedia, Inc. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。