背景

bpftool 是每一个搞 BPF 的人都必须熟练掌握的工具。本文总结了一些常用的命令(参考文档 [2] 列举得更为翔实),以方便大家查询。

本文将不定期持续更新

本文 bpftool 版本信息

1
2
3
# bpftool version
bpftool v5.10.0-rc3
features: libbfd, skeletons

bpftool 的代码源自内核,有兴趣的同学可以参考 bpftool-code

bpftool 目前支持以下几种 subcommand,如下图所示:

从实战角度来看,prog 和 map 最为常用

测试代码

为了验证 bpftool 的一些能力,我们提供了一个极其简单的 BPF 程序,可参考:bpf-workshop

bpftool-prog

  • List 当前系统中所有的 BPF Prog

    1
    2
    3
    4
    5
    
    # bpftool prog
    
    # bpftool prog list
    
    # bpftool prog show
    

    这三种用法等价

    使用 -j 选项可以以 JSON 形式打印,而用 -p 则可以打印出可读性更强的 JSON 格式,不过还是更推荐结合 jq 来使用:

    1
    
    # bpftool prog -j | jq
    
  • 只获取某一个或某一组 BPF Prog

    1
    2
    3
    4
    5
    
    # bpftool prog list id 1734
    1734: lsm  name hello  tag 2649db08deb3a2b3  gpl
    	loaded_at 2021-12-11T14:30:06+0000  uid 0
    	xlated 7848B  jited 4954B  memlock 8192B  map_ids 2201,2203,2207,2204,2205,2208,2206,2202,2209
    	btf_id 1621
    

    根据 Linux 内核版本的不同,对于的输出也会有所不同。

  • 输出 BPF Prog 的二进制指令

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    # bpftool prog dump xlated id 1529
       0: (bf) r6 = r1
       1: (69) r7 = *(u16 *)(r6 +176)
       2: (b4) w8 = 0
       3: (44) w8 |= 2
       4: (b7) r0 = 1
       5: (55) if r8 != 0x2 goto pc+1
       6: (b7) r0 = 0
       7: (95) exit
    

    xlated 即 translated 之意。默认会将指令反汇编之后以可读性方式打印。

    如果想同时打印出二进制,可以加上 opcodes

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    # bpftool prog dump xlated id 1529 opcodes
       0: (bf) r6 = r1
           bf 16 00 00 00 00 00 00
       1: (69) r7 = *(u16 *)(r6 +176)
           69 67 b0 00 00 00 00 00
       2: (b4) w8 = 0
           b4 08 00 00 00 00 00 00
       3: (44) w8 |= 2
           44 08 00 00 02 00 00 00
       4: (b7) r0 = 1
           b7 00 00 00 01 00 00 00
       5: (55) if r8 != 0x2 goto pc+1
           55 08 01 00 02 00 00 00
       6: (b7) r0 = 0
           b7 00 00 00 00 00 00 00
       7: (95) exit
           95 00 00 00 00 00 00 00
    

    如果想将二进制写入文件,可以用:

    1
    
    # bpftool prog dump xlated id 1529 file /tmp/foo
    

    可用一些二进制编辑工具(比如 hexdump)进行查看。

    这个命令其实也等价于用 llvm-objdump

    1
    
    # llvm-objdump -d example.bpf.o
    

    但是二者的输出不完全相同。Linux 内核加载了 BPF 二进制之后估计会对一些跳转指令做重定位,所以与编译好但未加载的 BPF 二进制的反汇编指令会有些差异。

    如果想构建 CFG(至于 BPF 为什么是 CFG,可以看看这篇文章),可以加上 visual 指令:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    
    # bpftool prog dump xlated id 1529 visual
    digraph "DOT graph for eBPF program" {
    subgraph "cluster_0" {
    	style="dashed";
    	color="black";
    	label="func_0 ()";
    	fn_0_bb_0 [shape=Mdiamond,style=filled,label="ENTRY"];
    
    	fn_0_bb_2 [shape=record,style=filled,label="{   0: (bf) r6 = r1\l\
     |    1: (69) r7 = *(u16 *)(r6 +176)\l\
     |    2: (b4) w8 = 0\l\
     |    3: (44) w8 \|= 2\l\
     |    4: (b7) r0 = 1\l\
     |    5: (55) if r8 != 0x2 goto pc+1\l\
    }"];
    
    	fn_0_bb_3 [shape=record,style=filled,label="{   6: (b7) r0 = 0\l\
    }"];
    
    	fn_0_bb_4 [shape=record,style=filled,label="{   7: (95) exit\l\
    }"];
    
    	fn_0_bb_1 [shape=Mdiamond,style=filled,label="EXIT"];
    
    	fn_0_bb_0:s -> fn_0_bb_2:n [style="solid,bold", color=black, weight=10, constraint=true];
    	fn_0_bb_2:s -> fn_0_bb_3:n [style="solid,bold", color=black, weight=10, constraint=true];
    	fn_0_bb_2:s -> fn_0_bb_4:n [style="solid,bold", color=black, weight=10, constraint=true];
    	fn_0_bb_3:s -> fn_0_bb_4:n [style="solid,bold", color=black, weight=10, constraint=true];
    	fn_0_bb_4:s -> fn_0_bb_1:n [style="solid,bold", color=black, weight=10, constraint=true];
    	fn_0_bb_0:s -> fn_0_bb_1:n [style="invis", constraint=true];
    }
    }
    

    可将输出在 GraphvizOnline 进行展示,将获得如下渲染图:

  • 输出 BPF JIT 编译的指令

    为了提升执行 BPF 指令的性能,内核为 BPF 指令设计了一个 JIT,可在运行时将 BPF 指令编译成 native 指令。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    # bpftool prog dump jited id 1529
    bpf_prog_6deef7357e7b4530:
       0:	nopl   0x0(%rax,%rax,1)
       5:	xchg   %ax,%ax
       7:	push   %rbp
       8:	mov    %rsp,%rbp
       b:	push   %rbx
       c:	push   %r13
       e:	push   %r14
      10:	mov    %rdi,%rbx
      13:	movzwq 0xb0(%rbx),%r13
      1b:	xor    %r14d,%r14d
      1e:	or     $0x2,%r14d
      22:	mov    $0x1,%eax
      27:	cmp    $0x2,%r14
      2b:	jne    0x000000000000002f
      2d:	xor    %eax,%eax
      2f:	pop    %r14
      31:	pop    %r13
      33:	pop    %rbx
      34:	leaveq
      35:	retq
    

    同样地也支持 opcodesfile 命令,可参考 dump xlated

  • 将 trace pipe 的日志输出到终端

    1
    
    # bpftool prog tracelog
    

    等价于:

    1
    
    # cat /sys/kernel/debug/tracing/trace_pipe
    
  • 将 object 加载到 bpffs 中

    1
    
    # bpftool prog load ./example.bpf.o /sys/fs/bpf/example
    

    load 只会加载 object file 中第一个 BPF prog。如果 object file 中存在多个 BPF prog,我们需要使用 loadall

    1
    
    # bpftool prog loadall ./example.bpf.o /sys/fs/bpf/example
    

    如果此时没有 bpffs,可执行:

    1
    
    mount -t bpf none /sys/fs/bpf/
    

    我们可以用这种方式简易地加载我们的二进制程序(通常是 ELF 格式)。

    我们也可以使用 -d 来查看更详细的日志打印:

    1
    
    # bpftool -d prog load example.bpf.o  /sys/fs/bpf/example
    

    加载成功后,我们可以用如下命令进行查看

    1
    
    # bpftool prog list pinned /sys/fs/bpf/foo
    

    卸载该程序只需要删除对应的文件:

    1
    
    # rm /sys/fs/bpf/fo
    

    用这种方式加载的 BPF Prog 并不能正常工作,因为还没有 attatch,且相应的 map 也不会自动创建,但是可以测试一下 Verifier 是否能准许我们 BPF Prog 的加载。

bpftool-map

  • List 当前系统中所有的 BPF Map

    1
    2
    3
    4
    5
    
    # bpftool map
    
    # bpftool map list
    
    # bpftool map show
    

    类比于 bpftool-prog

  • 只获取某一个 BPF Map

    1
    
    # bpftool map show id 2369
    
  • 创建一个 BPF Map

    1
    
    # bpftool map create /sys/fs/bpf/example type hash key 4 value 4 entries 8 name example
    

    如上,我们创建了一个 Hash 类型,Key 和 Value 均为 4 bytes,最大 entries 为 8 的 BPF Map,并将其 Pin 在了 /sys/fs/bpf/example

    此时我们利用 pinned <path> 进行查询:

    1
    2
    3
    
    # bpftool map show pinned /sys/fs/bpf/example
    2502: hash  name example  flags 0x0
    	key 4B  value 4B  max_entries 8  memlock 4096B
    

    正如我们所期待的,后面对于 Map 的读写也将以这个 Map 作为例子

  • 读写 BPF Map

    承接上一个例子,如果我们想对 example Map 写入一个 Key Value(注意 KV 大小均为 4 bytes),可以:

    1
    
    # bpftool map update id 2502 key 0x01 0x02 0x03 0x04 value 0x05 0x06 0x07 0x08 
    

    我们还可以在后面加 UPDATE_FLAGS 来控制更新的行为:

    • any:如果 entry 存在则是更新操作,否则为创建操作;
    • exist:在 entry 存在的前提下执行更新操作;
    • noexist:只在 entry 不存在的前提下执行创建操作;

    假如我们此时利用 exist update 一个不存在的 entry:

    1
    2
    
    # bpftool map update id 2502 key 0x00 0x00 0x00 0x00 value 0x05 0x06 0x07 0x08 exist
    Error: update failed: No such file or directory
    

    将会导致写入失败

    此时我们将进行查询

    1
    2
    
    # bpftool map lookup id 2502 key 0x01 0x02 0x03 0x04
    key: 01 02 03 04  value: 05 06 07 08
    

    如果我们想 dump 整个 Map,可用:

    1
    2
    3
    
    # bpftool map dump id 2502
    key: 01 02 03 04  value: 05 06 07 08
    Found 1 element
    

    可配合使用 -j 或者 -p 选项来得到相应的 JSON 输出。

    如果对应的 Map 有相应的 BTF struct,则默认 dump 的时候会结合 BTF 输出更友好的数据结构,否则 Map 的输出一般是比较原始的 Hex 格式。

    我们将对应的 Key 进行删除动作

    1
    
    # bpftool map delete id 2502 key 0x01 0x02 0x03 0x04
    
  • 迭代访问数据

    如果我们依次写入如下测试数据:

    1
    2
    3
    
    # bpftool map update id 2502 key 0x00 0x00 0x00 0x01 value 0x10 0x00 0x00 0x00 any
    # bpftool map update id 2502 key 0x00 0x00 0x00 0x02 value 0x20 0x00 0x00 0x00 any
    # bpftool map update id 2502 key 0x00 0x00 0x00 0x03 value 0x30 0x00 0x00 0x00 any
    

    此时我们可以从 0x00 0x00 0x00 0x03 开始,获取 nextkey:

    1
    2
    3
    4
    5
    
    # bpftool map getnext id 2502 key 0x00 0x00 0x00 0x03
    key:
    00 00 00 03
    next key:
    00 00 00 02
    

    再从 00 00 00 02 继续执行:

    1
    2
    3
    4
    5
    
    # bpftool map getnext id 2502 key 0x00 0x00 0x00 0x02
    key:
    00 00 00 02
    next key:
    00 00 00 01
    

    最后我们从最后一个 key 0x00 0x00 0x00 0x01 执行,将返回错误:

    1
    2
    
    # bpftool map getnext id 2502 key 0x00 0x00 0x00 0x01
    Error: can't get next key: No such file or directory
    
  • 冻结 BPF Map

    我们可以从用户层 freeze BPF Map,即变为只读状态

    1
    
    # bpftool map show id 2502
    

    此时我们在查看 Map 的元数据信息:

    1
    2
    3
    4
    
    bpftool map show id 2502
    2502: hash  name example  flags 0x0
    	key 4B  value 4B  max_entries 8  memlock 4096B
    	frozen
    

    可以看到有 frozen 标记。

    如果我们此时再执行写入动作:

    1
    2
    
    # bpftool map update id 2502 key 0x00 0x00 0x00 0x00 value 0x05 0x06 0x07 0x08 any
    Error: update failed: Operation not permitted
    

    将写入失败。

    注意这是一个不可逆的动作。Map 将会保持 immutable 直到被销毁。

bpftool-btf

  • List 当前系统中所有的 BTF

    1
    
    # bpftool btf show
    

    每个加载入内核的 BTF 信息都会获得一个唯一 ID;

  • 只获取某一个 BPF ID 信息

    1
    
    # bpftool btf show id 1823
    
  • 从支持 BTF 的 Linux 系统出导出 vmlinux.h

    1
    
     # bpftool btf dump file /sys/kernel/btf/vmlinux format c
    
  • 从某个 BPF 目标文件 dump 对应的 BTF 信息

    1
    
    # bpftool btf dump file foo.o
    

bpftool-perf

  • List 当前系统中 raw_tracepoint / tracepoint / kprobe 的 attachment

    1
    2
    
    # bpftool perf
    pid 329190  fd 6: prog_id 1936  tracepoint  sched_process_exec
    

bpftool-gen

  • 生成 skeleton 头文件

    1
    
    # bpftool gen skeleton example.bpf.o > example.skel.h
    

    skeleton 头文件必须和 libbpf 结合使用,为用户层的加载动作提供一些框架性的代码。

参考资料

  1. Package bpftool

  2. Features of bpftool: the thread of tips and examples to work with eBPF objects