LinuxのBPF : (4) ClangによるeBPFプログラムの作成と,BPF Compiler Collection (BCC)
Clangを利用したeBPFプログラムの作成
前回はeBPFプログラムをstruct bpf_insn
の配列として直接書きましたが,あまりにも面倒です.
cBPFの場合はtcpdump (libpcap)を使うことができました.
llvm3.7よりeBPFのバックエンドが追加されました.したがって,clangを利用してC言語のプログラムをeBPFプログラムにコンパイルすることが可能です.
eBPFへのコンパイル
例えば,以下のようなCプログラムを考えます.
int f(int x){ return x+1; }
-target bpf
を指定することでeBPFのプログラムにコンパイルすることが可能です.
llvm IRの出力
clang -c -S -emit-llvm a.c
; ModuleID = 'a.c' source_filename = "a.c" target datalayout = "e-m:e-p:64:64-i64:64-n32:64-S128" target triple = "bpf" ; Function Attrs: noinline nounwind define i32 @f(i32) #0 { %2 = alloca i32, align 4 store i32 %0, i32* %2, align 4 %3 = load i32, i32* %2, align 4 %4 = add nsw i32 %3, 1 ret i32 %4 } attributes #0 = { noinline nounwind "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "unsafe-fp-math"="false" "use-soft-float"="false" } !llvm.ident = !{!0} !0 = !{!"clang version 4.0.1 (tags/RELEASE_401/final)"}
eBPFアセンブリの出力
clang -c -S -target bpf a.c
.text .globl f .p2align 3 f: # @f # BB#0: *(u32 *)(r10 - 4) = r1 r2 = r1 r1 += 1 r0 = r1 *(u64 *)(r10 - 16) = r2 exit
eBPFプログラムの出力
clang -c -target bpf a.c
この場合,ELFバイナリが出力されます.
% objdump -x a.o a.o: file format elf64-little a.o architecture: UNKNOWN!, flags 0x00000010: HAS_SYMS start address 0x0000000000000000 Sections: Idx Name Size VMA LMA File off Algn 0 .text 00000030 0000000000000000 0000000000000000 00000040 2**3 CONTENTS, ALLOC, LOAD, READONLY, CODE SYMBOL TABLE: 0000000000000000 g .text 0000000000000000 f % readelf -x .text a.o Hex dump of section '.text': 0x00000000 631afcff 00000000 bf120000 00000000 c............... 0x00000010 07010000 01000000 bf100000 00000000 ................ 0x00000020 7b2af0ff 00000000 95000000 00000000 {*..............
ELFバイナリからeBPFプログラムだけを抽出したい場合は,以下のようにします.
objcopy -I elf64-little -O binary a.o a.bin
% ./ubpf-disassembler a.bin a.s % cat a.s stxw [r10-4], r1 mov r2, r1 add r1, 0x1 mov r0, r1 stxdw [r10-16], r2 exit
このようにしてeBPFのプログラムが作成できますが,実際にカーネル内で動作できるプログラムを作成するにはいくつか注意が必要になります.
カーネルで動作するeBPFプログラムの例
カーネルのsamples/bpf以下にC言語で書かれたeBPFの例がいくつかあります.IPの上位プロトコロル別に外向きのパケットバイト数をカウントするCのプログラムが,sockex1_kern.c
及びsockex1_user.c
です.
https://github.com/torvalds/linux/blob/v4.12/samples/bpf/sockex1_kern.c (eBPFプログラム)
#include <uapi/linux/bpf.h> #include <uapi/linux/if_ether.h> #include <uapi/linux/if_packet.h> #include <uapi/linux/ip.h> #include "bpf_helpers.h" struct bpf_map_def SEC("maps") my_map = { .type = BPF_MAP_TYPE_ARRAY, .key_size = sizeof(u32), .value_size = sizeof(long), .max_entries = 256, }; SEC("socket1") int bpf_prog1(struct __sk_buff *skb) { int index = load_byte(skb, ETH_HLEN + offsetof(struct iphdr, protocol)); long *value; if (skb->pkt_type != PACKET_OUTGOING) return 0; value = bpf_map_lookup_elem(&my_map, &index); if (value) __sync_fetch_and_add(value, skb->len); return 0; }
https://github.com/torvalds/linux/blob/v4.12/samples/bpf/sockex1_user.c
#include <stdio.h> #include <assert.h> #include <linux/bpf.h> #include "libbpf.h" #include "bpf_load.h" #include "sock_example.h" #include <unistd.h> #include <arpa/inet.h> int main(int ac, char **argv) { char filename[256]; FILE *f; int i, sock; snprintf(filename, sizeof(filename), "%s_kern.o", argv[0]); if (load_bpf_file(filename)) { printf("%s", bpf_log_buf); return 1; } sock = open_raw_sock("lo"); assert(setsockopt(sock, SOL_SOCKET, SO_ATTACH_BPF, prog_fd, sizeof(prog_fd[0])) == 0); f = popen("ping -c5 localhost", "r"); (void) f; for (i = 0; i < 5; i++) { long long tcp_cnt, udp_cnt, icmp_cnt; int key; key = IPPROTO_TCP; assert(bpf_map_lookup_elem(map_fd[0], &key, &tcp_cnt) == 0); key = IPPROTO_UDP; assert(bpf_map_lookup_elem(map_fd[0], &key, &udp_cnt) == 0); key = IPPROTO_ICMP; assert(bpf_map_lookup_elem(map_fd[0], &key, &icmp_cnt) == 0); printf("TCP %lld UDP %lld ICMP %lld bytes\n", tcp_cnt, udp_cnt, icmp_cnt); sleep(1); } return 0; }
このプログラムはカーネルソースのルートでmake headers_install
としたのち,samples/bpf
でmake
すればコンパイルできると思います(要clang/llc).
load_bpf_file()
はelfからeBPFプログラムを呼びだす関数です.
コードを見ればやっていることはなんとなく分かると思いますが,何故このプログラムがちゃんと動作するかは少々複雑です.
前回示したeBPFのプログラムは以下のようになっていました.
BPF_MOV64_REG(BPF_REG_6, BPF_REG_1), BPF_LD_ABS(BPF_B, ETH_HLEN + offsetof(struct iphdr, protocol) /* R0 = ip->proto */), BPF_STX_MEM(BPF_W, BPF_REG_10, BPF_REG_0, -4), /* *(u32 *)(fp - 4) = r0 */ BPF_MOV64_REG(BPF_REG_2, BPF_REG_10), BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4), /* r2 = fp - 4 */ BPF_LD_MAP_FD(BPF_REG_1, map_fd), BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem), BPF_JMP_IMM(BPF_JEQ, BPF_REG_0, 0, 2), BPF_MOV64_IMM(BPF_REG_1, 1), /* r1 = 1 */ BPF_RAW_INSN(BPF_STX | BPF_XADD | BPF_DW, BPF_REG_0, BPF_REG_1, 0, 0), /* xadd r0 += r1 */ BPF_MOV64_IMM(BPF_REG_0, 0), /* r0 = 0 */ BPF_EXIT_INSN(),
特に重要なポイントとしては,
BPF_LD_ABS
はskbuff
からデータを読み出す特別な命令で,R6
レジスタにskbuff
のポインタを格納する必要があるBPF_FUNC_map_lookup_elem
を呼び出す前に,R1
レジスタにeBPF mapのdescriptorを格納する
の2点です.
skbuff
からのロード
まず,skbuff
からのデータのロードに関して,Cでは以下のようになっています.
int index = load_byte(skb, ETH_HLEN + offsetof(struct iphdr, protocol));
ここで,load_byte()
はbpf_helpers.hで定義されています.
/* llvm builtin functions that eBPF C program may use to * emit BPF_LD_ABS and BPF_LD_IND instructions */ struct sk_buff; unsigned long long load_byte(void *skb, unsigned long long off) asm("llvm.bpf.load.byte");
つまり,この関数はllvm IRのintrinsicな命令を出力します.
実際にllcでeBPFプログラムを出力する際に,R6レジスタにskbuff
を追加する命令が追加されます.
https://github.com/llvm-mirror/llvm/blob/release_50/lib/Target/BPF/BPFISelDAGToDAG.cpp#L179
case ISD::INTRINSIC_W_CHAIN: { unsigned IntNo = cast<ConstantSDNode>(Node->getOperand(1))->getZExtValue(); switch (IntNo) { case Intrinsic::bpf_load_byte: case Intrinsic::bpf_load_half: case Intrinsic::bpf_load_word: { SDLoc DL(Node); SDValue Chain = Node->getOperand(0); SDValue N1 = Node->getOperand(1); SDValue Skb = Node->getOperand(2); SDValue N3 = Node->getOperand(3); SDValue R6Reg = CurDAG->getRegister(BPF::R6, MVT::i64); Chain = CurDAG->getCopyToReg(Chain, DL, R6Reg, Skb, SDValue()); Node = CurDAG->UpdateNodeOperands(Node, Chain, N1, R6Reg, N3); break; } }
bpf_load_byte
は 最終的にBPF_ABS | <size> | BPF_LD
の命令になります.
https://github.com/llvm-mirror/llvm/blob/release_50/lib/Target/BPF/BPFInstrInfo.td#L544
let Defs = [R0, R1, R2, R3, R4, R5], Uses = [R6], hasSideEffects = 1, hasExtraDefRegAllocReq = 1, hasExtraSrcRegAllocReq = 1, mayLoad = 1 in { class LOAD_ABS<bits<2> SizeOp, string OpcodeStr, Intrinsic OpNode> : InstBPF<(outs), (ins GPR:$skb, i64imm:$imm), "r0 = *("#OpcodeStr#" *)skb[$imm]", [(set R0, (OpNode GPR:$skb, i64immSExt32:$imm))]> { bits<3> mode; bits<2> size; bits<32> imm; let Inst{63-61} = mode; let Inst{60-59} = size; let Inst{31-0} = imm; let mode = 1; // BPF_ABS let size = SizeOp; let BPFClass = 0; // BPF_LD } ... def LD_ABS_B : LOAD_ABS<2, "u8", int_bpf_load_byte>; def LD_ABS_H : LOAD_ABS<1, "u16", int_bpf_load_half>; def LD_ABS_W : LOAD_ABS<0, "u32", int_bpf_load_word>;
eBPF mapの処理
次に,eBPF mapの部分に関してですが,ロードされる前のeBPFプログラムは以下のようなオブジェクトファイルになっています.
% objdump -x sockex1_kern.o sockex1_kern.o: file format elf64-little sockex1_kern.o architecture: UNKNOWN!, flags 0x00000011: HAS_RELOC, HAS_SYMS start address 0x0000000000000000 Sections: Idx Name Size VMA LMA File off Algn 0 .text 00000000 0000000000000000 0000000000000000 00000040 2**2 CONTENTS, ALLOC, LOAD, READONLY, CODE 1 socket1 00000078 0000000000000000 0000000000000000 00000040 2**3 CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE 2 maps 00000018 0000000000000000 0000000000000000 000000b8 2**2 CONTENTS, ALLOC, LOAD, DATA 3 license 00000004 0000000000000000 0000000000000000 000000d0 2**0 CONTENTS, ALLOC, LOAD, DATA SYMBOL TABLE: 0000000000000068 l socket1 0000000000000000 LBB0_3 0000000000000000 g license 0000000000000000 _license 0000000000000000 g socket1 0000000000000000 bpf_prog1 0000000000000000 g maps 0000000000000000 my_map RELOCATION RECORDS FOR [socket1]: OFFSET TYPE VALUE 0000000000000038 UNKNOWN my_map
注目すべきは最後のrelocationに関するところで,my_map
の値はrelocatableなシンボルとなっています.
load_bpf_file()
では,bpfプログラムを読み込む際に以下のことをおこなっています.
- mapsセクションからeBPF mapの情報を読み取り,eBPF mapを作成
- ELFのrelocation recordsをチェックし,必要に応じて処理をおこなう
- 最終的にeBPFプログラムをカーネルにロードする
ちなみに,BPF_FUNC_map_lookup_elem
の番号はlinux/bpf.hで定義されています.
BPF Compiler Collection (BCC)
上記の例で分かるように,clangでeBPFプログラムがコンパイルできるものの,実際には何かライブラリがなければカーネル内で動作するeBPFプログラムを作成し動作させることは面倒です.また,eBPFにプログラムがコンパイルできたとしても,それがカーネル内で正しく動くかどうかは別問題です.eBPFプログラムをロードする際のverifierで弾かれる可能性も十分あります. カーネルソースのsamples/bpf以下にいくつかヘルパー関数が存在するものの,自分が作成するプログラムで使用するには少々面倒です.
BPF Compiler Collection (BCC)は,簡単にeBPFによるLinux kernel tracingを実現するためのライブラリ/ツール群です.bccは以下のような機能等があります.
- eBPFプログラムを書くために便利なライブラリの提供
- python bindings
- eBPFによる様々なトレーシングツールの提供
bccは巨大なプロジェクトですが,ここでは今までやってきたパケットの統計量の取得に関してみていきたいと思います.
インストール
本題に入る前に,インストール方法について軽くふれておくと,bccのインストール方法はここに書いてありますが,Ubuntuであれば,
sudo apt-get install binutils bcc bcc-tools libbcc-examples python-bcc
で入ると思います.あるいは,自分でコンパイルする場合は
% git clone https://github.com/iovisor/bcc
% cd bcc
% mkdir build
% cd build
% cmake ..
% make
% make install
でできると思います.最新機能を試したい場合は自分でカーネル含めてコンパイルした方がいいと思います.
bccによるプログラムの例
bccを利用してIPの上位プロトコル別に外向きパケットのバイト数をカウントするプログラムは以下のように書けます.
#include <bcc/proto.h> BPF_ARRAY(my_map, long, 256); int bpf_prog(struct __sk_buff *skb) { u8* cursor = 0; struct ethernet_t *ethhdr = cursor_advance(cursor, sizeof(*ethhdr)); if(!(ethhdr->type == 0x0800)){ // not ipv4 return -1; } struct ip_t *iphdr = cursor_advance(cursor, sizeof(*iphdr)); u32 index = iphdr->nextp; if (skb->pkt_type != PACKET_OUTGOING) return -1; long *value = my_map.lookup(&index); if (value) lock_xadd(value, skb->len); return -1; }
import time from bcc import BPF def main(): b = BPF(src_file="./sockex1.c", debug=0) f = b.load_func("bpf_prog", BPF.SOCKET_FILTER) BPF.attach_raw_socket(f, "lo") my_map = b.get_table("my_map") ICMP = 1 TCP = 6 UDP = 17 for i in range(5): print("TCP {} UDP {} ICMP {} bytes".format(my_map[TCP], my_map[UDP], my_map[ICMP])) time.sleep(1) if __name__ == "__main__": main()
BPF(src_file="...")
でbpfプログラムのロード & コンパイルBPF.attach_raw_socket()
でraw socketにBPFプログラムをアタッチget_table()
でmapにアクセス
しています.
bccで読み込んでいるCプログラムは元のものと似ていますが,以下の特徴があります.
- bpf/proto.hを利用
BPF_ARRAY
マクロを利用してeBPF mapを定義cursor_advance()
関数を利用してskbuffにアクセス
また,eBPFのプログラムでフィルタリングをする場合,戻り値はuserspaceに返すバイトの長さ(の最大)を指定します.普通はパケットをドロップしたい場合は0, そうでない場合は-1にします.といっても今回の例ではユーザプログラム側では特にパケットを受信してないのであまり深い意味は持たないです.(__sk_receive_skb()の中のsk_filter_trim_capからbpfのプログラムが呼ばれ,その戻り値に応じてskbuff
の長さが変更 or dropされます).
BPF.attach_raw_socket(f, "lo")
の後にf.sock
でeBPFプログラムがアタッチされたsocketのdescriptorが取得できます.
cursor_advance()
cursor_advance()
の定義は以下のようになっています.
#define cursor_advance(_cursor, _len) \ ({ void *_tmp = _cursor; _cursor += _len; _tmp; })
ということで,上のcursor_advance()
のコードは以下のようになります.
u8* cursor = 0; struct ethernet_t *ethhdr = ({void *_tmp = cursor; cursor += sizeof(*ethhdr); _tmp;});
これをそのまま解釈すると,最初の状態ではethhdr
には0が代入されて,cursor
はsizeof(*ethhdr)
の分だけ加算されることになります.
さて,それではどうしてこれでskbuff
内のパケットデータにアクセスできるのかというと,まずethernet_t
は以下のように宣言されています.
#define BPF_PACKET_HEADER __attribute__((packed)) __attribute__((deprecated("packet"))) struct ethernet_t { unsigned long long dst:48; unsigned long long src:48; unsigned int type:16; } BPF_PACKET_HEADER;
ethernet_t
には"packet"というattributeがつけられています.bccがプログラムをコンパイルする際に,このattirbuteがついている変数へのassignの場合,bpf_dext_pkt()
を出力します.
StatusTuple CodegenLLVM::visit_packet_expr_node(PacketExprNode *n) { auto p = proto_scopes_->top_struct()->lookup(n->id_->name_, true); VariableDeclStmtNode *offset_decl, *skb_decl; Value *offset_mem, *skb_mem; TRY2(lookup_var(n, "skb", scopes_->current_var(), &skb_decl, &skb_mem)); TRY2(lookup_var(n, "$" + n->id_->name_, scopes_->current_var(), &offset_decl, &offset_mem)); if (p) { auto f = p->field(n->id_->sub_name_); if (f) { size_t bit_offset = f->bit_offset_; size_t bit_width = f->bit_width_; if(n->bitop_){ ... } else { emit("bpf_dext_pkt(pkt, %s + %zu, %zu, %zu)", n->id_->c_str(), bit_offset >> 3, bit_offset & 0x7, bit_width); Function *load_fn = mod_->getFunction("bpf_dext_pkt"); if (!load_fn) return mkstatus_(n, "unable to find function bpf_dext_pkt"); LoadInst *skb_ptr = B.CreateLoad(skb_mem); Value *skb_ptr8 = B.CreateBitCast(skb_ptr, B.getInt8PtrTy()); LoadInst *offset_ptr = B.CreateLoad(offset_mem); Value *skb_hdr_offset = B.CreateAdd(offset_ptr, B.getInt64(bit_offset >> 3)); expr_ = B.CreateCall(load_fn, vector<Value *>({skb_ptr8, skb_hdr_offset, B.getInt64(bit_offset & 0x7), B.getInt64(bit_width)})); // this generates extra trunc insns whereas the bpf.load fns already // trunc the values internally in the bpf interpeter //expr_ = B.CreateTrunc(pop_expr(), B.getIntNTy(bit_width)); } } else { emit("pkt->start + pkt->offset + %s", n->id_->c_str()); return mkstatus_(n, "unsupported"); } } return StatusTuple(0); }
bpf_dext_pkt()
は以下のようになっています.
https://github.com/iovisor/bcc/blob/master/src/cc/export/helpers.h#L404
u64 bpf_dext_pkt(void *pkt, u64 off, u64 bofs, u64 bsz) { if (bofs == 0 && bsz == 8) { return load_byte(pkt, off); } else if (bofs + bsz <= 8) { return load_byte(pkt, off) >> (8 - (bofs + bsz)) & MASK(bsz); } else if (bofs == 0 && bsz == 16) { return load_half(pkt, off); } else if (bofs + bsz <= 16) { return load_half(pkt, off) >> (16 - (bofs + bsz)) & MASK(bsz); } else if (bofs == 0 && bsz == 32) { return load_word(pkt, off); } else if (bofs + bsz <= 32) { return load_word(pkt, off) >> (32 - (bofs + bsz)) & MASK(bsz); } else if (bofs == 0 && bsz == 64) { return load_dword(pkt, off); } else if (bofs + bsz <= 64) { return load_dword(pkt, off) >> (64 - (bofs + bsz)) & MASK(bsz); } return 0; }
load_byte()
から最終的にllvm.bpf.load.byte
が出力されます.
https://github.com/iovisor/bcc/blob/master/src/cc/export/helpers.h#L287
struct sk_buff; unsigned long long load_byte(void *skb, unsigned long long off) asm("llvm.bpf.load.byte");
…というのが自分の理解ですが,自分のllvmの知識は圧倒的に不足しているので違ったらごめんなさい.
BPF_ARRAY
BPF_ARRAY()
マクロは最終的に以下のように展開されます.
https://github.com/iovisor/bcc/blob/master/src/cc/export/helpers.h#L287
// Changes to the macro require changes in BFrontendAction classes #define BPF_F_TABLE(_table_type, _key_type, _leaf_type, _name, _max_entries, _flags) \ struct _name##_table_t { \ _key_type key; \ _leaf_type leaf; \ _leaf_type * (*lookup) (_key_type *); \ _leaf_type * (*lookup_or_init) (_key_type *, _leaf_type *); \ int (*update) (_key_type *, _leaf_type *); \ int (*insert) (_key_type *, _leaf_type *); \ int (*delete) (_key_type *); \ void (*call) (void *, int index); \ void (*increment) (_key_type); \ int (*get_stackid) (void *, u64); \ u32 max_entries; \ int flags; \ }; \ __attribute__((section("maps/" _table_type))) \ struct _name##_table_t _name = { .flags = (_flags), .max_entries = (_max_entries) }
あまり真面目にコードを追ってないですが,どうやらbccがプログラムをパースする際に,mapの定義があったらそのmapを作成しているようです. https://github.com/iovisor/bcc/blob/fc673a9e0cf31e935f53cb69a76fc0668c8ffecc/src/cc/frontends/b/codegen_llvm.cc#L1103
StatusTuple CodegenLLVM::visit_table_decl_stmt_node(TableDeclStmtNode *n) { ... int map_fd = bpf_create_map(map_type, key->bit_width_ / 8, leaf->bit_width_ / 8, n->size_, 0); ... }
lookup()
は最終的にbpf_map_lookup_elem()
が呼ばれます.
StatusTuple CodegenLLVM::emit_table_lookup(MethodCallExprNode *n) { TableDeclStmtNode* table = scopes_->top_table()->lookup(n->id_->name_); IdentExprNode* arg0 = static_cast<IdentExprNode*>(n->args_.at(0).get()); IdentExprNode* arg1; StructVariableDeclStmtNode* arg1_type; auto table_fd_it = table_fds_.find(table); if (table_fd_it == table_fds_.end()) return mkstatus_(n, "unable to find table %s in table_fds_", n->id_->c_str()); Function *pseudo_fn = mod_->getFunction("llvm.bpf.pseudo"); if (!pseudo_fn) return mkstatus_(n, "pseudo fd loader doesn't exist"); Function *lookup_fn = mod_->getFunction("bpf_map_lookup_elem_"); if (!lookup_fn) return mkstatus_(n, "bpf_map_lookup_elem_ undefined"); CallInst *pseudo_call = B.CreateCall(pseudo_fn, vector<Value *>({B.getInt64(BPF_PSEUDO_MAP_FD), B.getInt64(table_fd_it->second)})); Value *pseudo_map_fd = pseudo_call; TRY2(arg0->accept(this)); Value *key_ptr = B.CreateBitCast(pop_expr(), B.getInt8PtrTy()); expr_ = B.CreateCall(lookup_fn, vector<Value *>({pseudo_map_fd, key_ptr})); ... }
最初の例ではbpf_map_lookup_elem()
の引数のdescriptorはプログラムをコンパイル後に書き換えていましたが,bccの場合は最初にmapを作ってそのdescriptorをそのままllvmのIRに埋め込んでいるのだと思います.多分.
このように,bccはeBPFプログラムが書きやすいようなmodified Cを提供しています.
もう少し具体的なパケットフィルタリングに関する応用例はbccの examples/networking 以下にあります.(例えばhttp_filterなど)
まとめ
clangによるeBPFプログラムの作成方法と,bccを利用したeBPFプログラムの作成方法の基礎について書きました.
次回は今まで説明してこなかったeBPFを利用したkernel tracingについて書こうと思います.