bpfsnoop 是一款 bpf 时代的现代化内核函数、内核跟踪点和 bpf 程序的动态追踪工具。

bpfsnoop 发布 v0.4.0 版本,主要更新如下:

  1. 使用 kprobe 动态追踪内核函数的所有指令。
  2. 实现了一个基于 BTF 的 C 表达式编译器。
  3. --output-arg 新增指针内容、buf() 内置函数和 str() 内置函数。
  4. 复用 ringbuf 传递函数参数、函数返回值和所有匹配的 --output-arg

bpfsnoop 官网:bpfsnoop.com

使用 kprobe 动态追踪内核函数的所有指令

bpfsnoop 使用 capstone-engine 来反汇编内核函数的所有指令,然后使用 kprobe 来动态追踪内核函数的所有指令。

效果如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
$ sudo ./bpfsnoop -k '(i)icmp_rcv' --filter-pkt 'host 1.1.1.1' --trace-insn-debug-cnt 10
2025/05/25 16:18:54 bpfsnoop is running..
icmp_rcv args=((struct sk_buff *)skb=0xffff93438b6cfc00) cpu=6 process=(0:swapper/6)
icmp_rcv cpu=6  duration=51.061µs     insn=0xffffffffbc61563b <+11/0xb>:     41 57                  pushq   %r15
icmp_rcv cpu=6  duration=53.741µs     insn=0xffffffffbc61563d <+13/0xd>:     41 56                  pushq   %r14
icmp_rcv cpu=6  duration=57.091µs     insn=0xffffffffbc61563f <+15/0xf>:     41 55                  pushq   %r13
icmp_rcv cpu=6  duration=59.194µs     insn=0xffffffffbc615641 <+17/0x11>:    41 54                  pushq   %r12
icmp_rcv cpu=6  duration=61.633µs     insn=0xffffffffbc615643 <+19/0x13>:    53                     pushq   %rbx
icmp_rcv cpu=6  duration=64.298µs     insn=0xffffffffbc615644 <+20/0x14>:    48 89 fb               movq    %rdi, %rbx
icmp_rcv cpu=6  duration=66.378µs     insn=0xffffffffbc615647 <+23/0x17>:    48 83 ec 08            subq    $8, %rsp
icmp_rcv cpu=6  duration=68.242µs     insn=0xffffffffbc61564b <+27/0x1b>:    4c 8b 67 58            movq    0x58(%rdi), %r12
icmp_rcv cpu=6  duration=70.355µs     insn=0xffffffffbc61564f <+31/0x1f>:    49 83 e4 fe            andq    $0xfffffffffffffffe, %r12
icmp_rcv cpu=6  duration=71.883µs     insn=0xffffffffbc615653 <+35/0x23>:    49 8b 04 24            movq    (%r12), %rax
icmp_rcv args=((struct sk_buff *)skb=0xffff93438b6cfc00) retval=(int)1 cpu=6 process=(0:swapper/6) duration=86.23µs

P.S. BPF 程序不允许使用 kprobe 来动态追踪程序的所有指令。

实现了一个基于 BTF 的 C 表达式编译器

bpfsnoop 实现了一个基于 BTF 的 C 表达式编译器,支持使用复杂的 C 表达式来过滤和获取函数参数的属性。

效果如下:

1
2
3
4
$ sudo ./bpfsnoop -t netif_receive_skb --filter-arg '2 == skb->dev->ifindex && (skb->cb[2]&0x7) == 0' --filter-pkt 'icmp' --output-arg 'skb->dev->name' --output-arg 'skb->cb[2]'
2025/05/25 16:23:21 bpfsnoop is running..
netif_receive_skb[tp] args=((struct sk_buff *)skb=0xffff93438b6ce700) cpu=6 process=(0:swapper/6)
Arg attrs: (array(char[16]))'skb->dev->name'="ens33", (char)'skb->cb[2]'=0x0/0

支持以下操作符:

  1. Limited support for memory access via ‘.’, ‘->’, ‘array[idx]’, ‘& (get addr)’ and ‘* (access data of pointer)’.
  2. Bitwise and ‘&’.
  3. Bool and ‘&&’.
  4. Ternary condition ‘?:’.
  5. Math div ‘/’.
  6. Bool equal ‘==’.
  7. Bool greater than ‘>’.
  8. Bool greater than or equal ‘>=’.
  9. Bitwise left shift ‘«’.
  10. Bool less than ‘<’.
  11. Bool less than or equal ‘<=’.
  12. Unary minus ‘-’.
  13. Math mod ‘%’.
  14. Math multi ‘*’.
  15. Internal constants ’true’, ‘false’ and ‘NULL’.
  16. Variable from function’s arguments.
  17. Unary bool not ‘!’.
  18. Bool not equal ‘!=’.
  19. Constant number, including hex, oct, bin, decimal and char.
  20. Bitwise or ‘|’.
  21. Bool or ‘||’.
  22. Parentheses ‘()’.
  23. Unary plus ‘+’.
  24. Pre-decrement ‘–’.
  25. Pre-increment ‘++’.
  26. Bitwise right shift ‘»’.
  27. Math sub ‘-’.
  28. Bitwise twid ‘~’.
  29. Bitwise xor ‘^’.

而且,它们的优先级遵循 C 语言的优先级规则。

--output-arg 新增指针内容、buf() 内置函数和 str() 内置函数

使用 ringbuf 传递 --output-arg 的数据后,--output-arg 数据的大小不再有限制(理论上最大为 64MiB),因此可以输出指针内容,比如完整的结构体。

效果如下:

1
2
3
4
$ sudo ./bpfsnoop -t netif_receive_skb --filter-pkt 'host 1.1.1.1 and icmp' --output-arg '*skb'
2025/05/25 16:28:27 bpfsnoop is running..
netif_receive_skb[tp] args=((struct sk_buff *)skb=0xffff93438b6ce600) cpu=6 process=(0:swapper/6)
Arg attrs: (struct sk_buff)'*skb'={"": {"": {"next": 0x0, "prev": 0xffff93438c1d2d98, "": {"dev": 0xffff93438c1d2000, "dev_scratch": 18446624517055651840}}, "rbnode": {"__rb_parent_color": 0, "rb_right": 0xffff93438c1d2d98, "rb_left": 0xffff93438c1d2000}, "list": {"next": 0x0, "prev": 0xffff93438c1d2d98}, "ll_node": {"next": 0x0}}, "": {"sk": 0x0, "ip_defrag_offset": 0}, "": {"tstamp": 1748190508585217877, "skb_mstamp_ns": 1748190508585217877}, "cb": "", "": {"": {"_skb_refdst": 0, "destructor": 0x0}, "tcp_tsorted_anchor": {"next": 0x0, "prev": 0x0}, "_sk_redir": 0}, "_nfct": 0, "len": 84, "data_len": 0, "mac_len": 14, "hdr_len": 0, "queue_mapping": 0, "__cloned_offset": [], "cloned": 0x0, "nohdr": 0x0, "fclone": 0x0, "peeked": 0x0, "head_frag": 0x1, "pfmemalloc": 0x0, "pp_recycle": 0x0, "active_extensions": 0, "": {"": {"__pkt_type_offset": [], "pkt_type": 0x0, "ignore_df": 0x0, "dst_pending_confirm": 0x0, "ip_summed": 0x0, "ooo_okay": 0x0, "__mono_tc_offset": [], "mono_delivery_time": 0x0, "tc_at_ingress": 0x0, "tc_skip_classify": 0x0, "remcsum_offload": 0x0, "csum_complete_sw": 0x0, "csum_level": 0x0, "inner_protocol_type": 0x0, "l4_hash": 0x0, "sw_hash": 0x0, "wifi_acked_valid": 0x0, "wifi_acked": 0x0, "no_fcs": 0x0, "encapsulation": 0x0, "encap_hdr_csum": 0x0, "csum_valid": 0x0, "ndisc_nodetype": 0x0, "ipvs_property": 0x0, "nf_trace": 0x0, "offload_fwd_mark": 0x0, "offload_l3_fwd_mark": 0x0, "redirected": 0x0, "from_ingress": 0x0, "nf_skip_egress": 0x0, "decrypted": 0x0, "slow_gro": 0x0, "csum_not_inet": 0x0, "tc_index": 0, "alloc_cpu": 6, "": {"csum": 0, "": {"csum_start": 0, "csum_offset": 0}}, "priority": 0, "skb_iif": 0, "hash": 0, "": {"vlan_all": 0, "": {"vlan_proto": 0, "vlan_tci": 0}}, "": {"napi_id": 8193, "sender_cpu": 8193}, "secmark": 0, "": {"mark": 0, "reserved_tailroom": 0}, "": {"inner_protocol": 0, "inner_ipproto": 0}, "inner_transport_header": 0, "inner_network_header": 0, "inner_mac_header": 0, "protocol": 8, "transport_header": 65535, "network_header": 78, "mac_header": 64}, "headers": {"__pkt_type_offset": [], "pkt_type": 0x0, "ignore_df": 0x0, "dst_pending_confirm": 0x0, "ip_summed": 0x0, "ooo_okay": 0x0, "__mono_tc_offset": [], "mono_delivery_time": 0x0, "tc_at_ingress": 0x0, "tc_skip_classify": 0x0, "remcsum_offload": 0x0, "csum_complete_sw": 0x0, "csum_level": 0x0, "inner_protocol_type": 0x0, "l4_hash": 0x0, "sw_hash": 0x0, "wifi_acked_valid": 0x0, "wifi_acked": 0x0, "no_fcs": 0x0, "encapsulation": 0x0, "encap_hdr_csum": 0x0, "csum_valid": 0x0, "ndisc_nodetype": 0x0, "ipvs_property": 0x0, "nf_trace": 0x0, "offload_fwd_mark": 0x0, "offload_l3_fwd_mark": 0x0, "redirected": 0x0, "from_ingress": 0x0, "nf_skip_egress": 0x0, "decrypted": 0x0, "slow_gro": 0x0, "csum_not_inet": 0x0, "tc_index": 0, "alloc_cpu": 6, "": {"csum": 0, "": {"csum_start": 0, "csum_offset": 0}}, "priority": 0, "skb_iif": 0, "hash": 0, "": {"vlan_all": 0, "": {"vlan_proto": 0, "vlan_tci": 0}}, "": {"napi_id": 8193, "sender_cpu": 8193}, "secmark": 0, "": {"mark": 0, "reserved_tailroom": 0}, "": {"inner_protocol": 0, "inner_ipproto": 0}, "inner_transport_header": 0, "inner_network_header": 0, "inner_mac_header": 0, "protocol": 8, "transport_header": 65535, "network_header": 78, "mac_header": 64}}, "tail": 162, "end": 704, "head": 0xffff9343b14d8400, "data": 0xffff9343b14d844e, "truesize": 1280, "users": {"refs": {"counter": 1}}, "extensions": 0x0}

结构体内容的输出,复用了 eBPF Talk: 谁动了我的 bpf map?mad 的功能。

与此同时,给 --output-arg 新增了 buf() 内置函数和 str() 内置函数。

buf() 内置函数

buf() 内置函数有 2 种用法:

  1. buf(ptr, size):输出指针 ptr 指向的内存区域的内容,大小为 size 字节。
  2. buf(ptr, offset, size):输出指针 ptr 指向的内存区域的内容,偏移量为 offset,大小为 size 字节。

效果如下:

1
2
3
4
$ sudo ./bpfsnoop -t netif_receive_skb --filter-pkt 'host 1.1.1.1 and icmp' --output-arg 'buf(skb->head, 64, 34)' --output-arg 'buf(skb->cb, 4, 4)' --output-arg 'skb->mac_header' --output-arg 'skb->network_header'
2025/05/25 16:32:36 bpfsnoop is running..
netif_receive_skb[tp] args=((struct sk_buff *)skb=0xffff93438b6cf700) cpu=6 process=(0:swapper/6)
Arg attrs: (unsigned char *)'buf(skb->head, 64, 34)'=[0x00,0x0c,0x29,0x81,0x41,0xb2,0x00,0x50,0x56,0xfb,0x9a,0xd0,0x08,0x00,0x45,0x00,0x00,0x54,0x1f,0xc5,0x00,0x00,0x80,0x01,0x66,0xb4,0x01,0x01,0x01,0x01,0xc0,0xa8,0xf1,0x85], (array(char[48]))'buf(skb->cb, 4, 4)'=[0x00,0x00,0x00,0x00], (__u16)'skb->mac_header'=0x40/64, (__u16)'skb->network_header'=0x4e/78

str() 内置函数

str() 内置函数有 2 种用法:

  1. str(array):输出字符串数组 array 的字符串。
  2. str(ptr, size):输出指针 ptr 指向的内存区域的字符串,大小为 size 字节。

效果如下:

1
2
3
4
5
6
7
$ sudo ./bpfsnoop -k dev_xdp_install --output-arg 'str(prog->aux->name, 5)' --output-arg 'str(prog->aux->used_maps[0]->name, 10)'
2025/05/25 16:34:05 bpfsnoop is running..
dev_xdp_install args=((struct net_device *)dev=0xffff93438c1d2000, (enum bpf_xdp_mode)mode=XDP_MODE_SKB, (bpf_op_t)bpf_op=0xffffffffbc5021f0(generic_xdp_install), (struct netlink_ext_ack *)extack=0xffffba9c01017cb8, (u32)flags=0x0/0, (struct bpf_prog *)prog=0xffffba9c0027d000) retval=(int)0 cpu=4 process=(393871:xdp-crc)
Arg attrs: (array(char[16]))'str(prog->aux->name, 5)'="crc", (array(char[16]))'str(prog->aux->used_maps[0]->name, 10)'=".rodata"

dev_xdp_install args=((struct net_device *)dev=0xffff93438c1d2000, (enum bpf_xdp_mode)mode=XDP_MODE_SKB, (bpf_op_t)bpf_op=0xffffffffbc5021f0(generic_xdp_install), (struct netlink_ext_ack *)extack=0x0, (u32)flags=0x0/0, (struct bpf_prog *)prog=0x0) retval=(int)0 cpu=4 process=(393871:xdp-crc)
Arg attrs: (array(char[16]))'str(prog->aux->name, 5)'=[NULL], (array(char[16]))'str(prog->aux->used_maps[0]->name, 10)'=[NULL]

复用 ringbuf 传递函数参数、函数返回值和所有匹配的 --output-arg

这是一次受益巨大的重构,既提升了可扩展性,又简化了代码,还少用了 5 个 bpf map。

因为 bpfsnoop 会为每个被动态追踪的对象都使用独立的 bpf prog,因而可以通过 volatile const struct config {...} 来传递函数参数、函数返回值和所有匹配的 --output-arg 等数据的字节数量;然后,根据 [event | args and retval | output args] 的格式来输出数据。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
    buffer_sz = sizeof(*evt) + cfg->fn_args.buf_size + cfg->fn_args.data_size;
    buffer = bpf_ringbuf_reserve(&bpfsnoop_events, buffer_sz, 0);
    if (!buffer)
        return BPF_OK;

    evt = buffer;
    evt->type = event_type;
    evt->length = sizeof(*evt);
    evt->kernel_ts = (__u32) bpf_ktime_get_ns();
    evt->session_id = session_id;
    evt->func_ip = FUNC_IP;
    evt->cpu = cpu;
    evt->pid = pid;
    bpf_get_current_comm(evt->comm, sizeof(evt->comm));
    // ...
    output_fn_args(args, buffer + sizeof(*evt), retval);
    // ...
    if (cfg->output_arg)
        output_arg(args, buffer + sizeof(*evt) + cfg->fn_args.buf_size);

    bpf_ringbuf_submit(evt, 0);

在 Go 里,按需解析事件数据:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
        err := reader.ReadInto(&record)

        // ...

        withRetval := event.Type == eventTypeFuncExit
        if fnInfo.argsBuf != 0 {
            b := record.RawSample[sizeOfEvent : sizeOfEvent+int(fnInfo.argsBuf)]
            outputFnArgs(&sb, fnInfo, helpers, b, findSymbol, withRetval)
        } else {
            fmt.Fprint(&sb, "=()")
            if withRetval {
                fmt.Fprint(&sb, " retval=(void)")
            }
        }

        // ...

        if fnInfo.argData != 0 {
            off := sizeOfEvent + int(fnInfo.argsBuf)
            b := record.RawSample[off : off+fnInfo.argData]
            err := outputFuncArgAttrs(&sb, fnInfo, b, findSymbol)
            // ...
        }

总结

bpfsnoop v0.4.0 版本的发布,主要是为了提升可扩展性和简化代码,同时也增加了对内核函数所有指令的动态追踪、基于 BTF 的 C 表达式编译器、指针内容输出、buf()str() 内置函数等功能。

bpfsnoop 是一款 bpf 时代的现代化内核函数、内核跟踪点和 bpf 程序的动态追踪工具:

  1. 支持输出 LBR 记录。
  2. 支持反汇编内核函数和 bpf prog。
  3. 支持输出函数调用栈。
  4. 支持输出带类型信息的参数和带类型的返回值。
  5. 支持使用复杂的 C 表达式来过滤函数参数的属性。
  6. 支持根据函数参数来过滤需要动态追踪的内核函数列表。
  7. 支持使用 pcap-filter(7) 语法来过滤网络包。
  8. 支持 --output-pkt 输出网络包里的五元组信息。
  9. 支持动态追踪多达 12 个参数的内核函数。
  10. 支持 --output-arg 来输出函数参数的属性,类似于 --filter-arg
  11. 支持 --output-flamegraph 来输出火焰图的折叠后的数据。
  12. 支持 -t 来动态追踪内核跟踪点。
  13. -k 里支持 ‘(i)’ 前缀来动态追踪内核函数的所有指令。
  14. 支持 --output-arg 来输出指针内容、buf() 内置函数和 str() 内置函数。

未来将支持更多高级功能,敬请期待!

bpfsnoop 项目地址:bpfsnoop