eBPF学习实践系列(五) -- 分析tcplife.bpf.c程序

2024-06-20

跟踪分析bcc项目ibbpf-tools中的 tcplife.bpf.c 程序

1. 背景

前面的eBPF学习实践系列主要是跟着别人的文章学习,积累了比较理论的知识。

TCP半连接全连接(三) – eBPF跟踪全连接队列溢出(上) 这篇中,自己要写eBPF程序了,带着实践的目的去参考已有工具代码,发现视角完全变了。

之前的理论知识很多都串起来了,再次体会到如这篇文章:举三反一–从理论知识到实际问题的推导 里说的工程效率知识效率,理论+实践才是学习的捷径。

这篇主要记录自己为了写eBPF跟踪TCP队列溢出程序,而进行的检索和扩展学习过程。

说明:本博客作为个人学习实践笔记,可供参考但非系统教程,可能存在错误或遗漏,欢迎指正。若需系统学习,建议参考原链接。

2. TCP相关tracepoint

查看有哪些可用的TCP跟踪点,检索到Brendan Gregg大佬的这篇文章:tcp-tracepoints

跟踪点不多,比想象中的少得多,可以通过如下方式查看:

1、方式1:通过 /sys/kernel/debug/tracing/available_events 文件查看

可看到直接相关的就8个

1
2
3
4
5
6
7
8
9
10
11
12
[root@localhost tracing]# grep -E "tcp:|sock:inet" /sys/kernel/debug/tracing/available_events
# 4.16新增
tcp:tcp_probe
tcp:tcp_retransmit_synack
tcp:tcp_rcv_space_adjust
tcp:tcp_destroy_sock
tcp:tcp_receive_reset
tcp:tcp_send_reset
tcp:tcp_retransmit_skb
# 4.16新增,可用于TCP分析的socket跟踪点
# 4.15中新增过tcp:tcp_set_state,不过4.16加的下述tracepoint是其超集,所以就把tcp:tcp_set_state去掉了
sock:inet_sock_set_state

2、方式2:perf list 'tcp:*' 'sock:inet*'

1
2
3
4
5
6
7
8
9
10
11
12
13
[root@localhost tracing]# perf list 'tcp:*' 'sock:inet*'

List of pre-defined events (to be used in -e):

  tcp:tcp_destroy_sock                               [Tracepoint event]
  tcp:tcp_probe                                      [Tracepoint event]
  tcp:tcp_rcv_space_adjust                           [Tracepoint event]
  tcp:tcp_receive_reset                              [Tracepoint event]
  tcp:tcp_retransmit_skb                             [Tracepoint event]
  tcp:tcp_retransmit_synack                          [Tracepoint event]
  tcp:tcp_send_reset                                 [Tracepoint event]

  sock:inet_sock_set_state                           [Tracepoint event]

3、方式3:通过bcc tools里的tplist查看

1
2
3
4
5
6
7
8
9
[root@localhost tracing]# /usr/share/bcc/tools/tplist | grep -E "tcp:|sock:inet" 
sock:inet_sock_set_state
tcp:tcp_retransmit_skb
tcp:tcp_send_reset
tcp:tcp_receive_reset
tcp:tcp_destroy_sock
tcp:tcp_rcv_space_adjust
tcp:tcp_retransmit_synack
tcp:tcp_probe

3. 具体分析 tcplife.bpf.c

tcp-tracepoints 中举例提及了tcplife在BCC和libbpf前后的对比。

这里重点跟踪一下:

并结合这篇文章译文:BCC 到 libbpf 的转换指南【译】,后续碰到libbpf程序应该都可以顺利拆解了。

3.1. 追踪点sock:inet_sock_set_state的参数

查看参数格式:(tcptracer工具也是用的这个追踪点)

可以看到这里还很友好地把TCP状态的枚举名称也对应起来了。

(关于TCP_NEW_SYN_RECV状态,我们在TCP半连接全连接(二) – 半连接队列代码逻辑中还分析过netstat里是追踪不到的)

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
[root@xdlinux ➜ ~ ]$ cat /sys/kernel/debug/tracing/events/sock/inet_sock_set_state/format 
name: inet_sock_set_state
ID: 1250
format:
    field:unsigned short common_type;	offset:0;	size:2;	signed:0;
    field:unsigned char common_flags;	offset:2;	size:1;	signed:0;
    field:unsigned char common_preempt_count;	offset:3;	size:1;	signed:0;
    field:int common_pid;	offset:4;	size:4;	signed:1;

    field:const void * skaddr;	offset:8;	size:8;	signed:0;
    field:int oldstate;	offset:16;	size:4;	signed:1;
    field:int newstate;	offset:20;	size:4;	signed:1;
    field:__u16 sport;	offset:24;	size:2;	signed:0;
    field:__u16 dport;	offset:26;	size:2;	signed:0;
    field:__u16 family;	offset:28;	size:2;	signed:0;
    field:__u8 protocol;	offset:30;	size:1;	signed:0;
    field:__u8 saddr[4];	offset:31;	size:4;	signed:0;
    field:__u8 daddr[4];	offset:35;	size:4;	signed:0;
    field:__u8 saddr_v6[16];	offset:39;	size:16;	signed:0;
    field:__u8 daddr_v6[16];	offset:55;	size:16;	signed:0;

print fmt: "family=%s protocol=%s sport=%hu dport=%hu saddr=%pI4 daddr=%pI4 saddrv6=%pI6c daddrv6=%pI6c oldstate=%s newstate=%s", 
    __print_symbolic(REC->family, { 2, "AF_INET" }, { 10, "AF_INET6" }), 
    __print_symbolic(REC->protocol, { 6, "IPPROTO_TCP" }, { 33, "IPPROTO_DCCP" }, { 132, "IPPROTO_SCTP" }, { 262, "IPPROTO_MPTCP" }), 
    REC->sport, REC->dport, REC->saddr, REC->daddr, REC->saddr_v6, REC->daddr_v6, 
    __print_symbolic(REC->oldstate, { 1, "TCP_ESTABLISHED" }, { 2, "TCP_SYN_SENT" }, { 3, "TCP_SYN_RECV" }, { 4, "TCP_FIN_WAIT1" }, { 5, "TCP_FIN_WAIT2" }, { 6, "TCP_TIME_WAIT" }, { 7, "TCP_CLOSE" }, { 8, "TCP_CLOSE_WAIT" }, { 9, "TCP_LAST_ACK" }, { 10, "TCP_LISTEN" }, { 11, "TCP_CLOSING" }, { 12, "TCP_NEW_SYN_RECV" }), 
    __print_symbolic(REC->newstate, { 1, "TCP_ESTABLISHED" }, { 2, "TCP_SYN_SENT" }, { 3, "TCP_SYN_RECV" }, { 4, "TCP_FIN_WAIT1" }, { 5, "TCP_FIN_WAIT2" }, { 6, "TCP_TIME_WAIT" }, { 7, "TCP_CLOSE" }, { 8, "TCP_CLOSE_WAIT" }, { 9, "TCP_LAST_ACK" }, { 10, "TCP_LISTEN" }, { 11, "TCP_CLOSING" }, { 12, "TCP_NEW_SYN_RECV" })
[root@xdlinux ➜ ~ ]$

3.2. 如何查看eBPF helper函数说明

系统libbpf的include下的bpf.h里可以看到各helper函数功能介绍

(linux-5.10.10\include\uapi\linux\bpf.h或者bcc-master\src\cc\libbpf\include\uapi\linux\bpf.h)

3.3. 代码分析

代码整体贴过来:(helper函数的功能和参数说明,均可在bpf.h查看)

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
// bcc/libbpf-tools/tcplife.bpf.c

// SPDX-License-Identifier: GPL-2.0
/* Copyright (c) 2022 Hengqi Chen */
#include <vmlinux.h>
// 在 BCC 中使用BPF_CORE_READ,请确保 bpf_core_read.h 头文件包含在最终 BPF 程序中
#include <bpf/bpf_core_read.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include "tcplife.h"

#define MAX_ENTRIES	10240
#define AF_INET		2
#define AF_INET6	10

const volatile bool filter_sport = false;
const volatile bool filter_dport = false;
const volatile __u16 target_sports[MAX_PORTS] = {};
const volatile __u16 target_dports[MAX_PORTS] = {};
const volatile pid_t target_pid = 0;
const volatile __u16 target_family = 0;

// 这里是libbpf里面使用BPF map的方式
/*
    BCC 中 Map 的默认大小是10240 。使用 libbpf,你必须明确指定大小
    作为对比,按参考译文的规则,BCC里面的方式是这样:
        BPF_HASH(birth, struct sock *, __u64);
    看 tcplife.py里的bcc代码段确实如此:
        BPF_HASH(birth, struct sock *, u64);
*/
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, MAX_ENTRIES);
    __type(key, struct sock *);
    __type(value, __u64);
} birth SEC(".maps");

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, MAX_ENTRIES);
    __type(key, struct sock *);
    __type(value, struct ident);
} idents SEC(".maps");

/*
    这里也做一下BCC和libbpf的对比,BPF_MAP_TYPE_PERF_EVENT_ARRAY 对应 BPF_PERF_OUTPUT
        reference_guide.md 里,可以看到 BPF_PERF_OUTPUT 的解释说明
            创建一个BPF表来通过 perf环形缓冲区 将自定义事件数据推送到用户空间。这是将每事件数据推送到用户空间的首选方法。
            Perf环形缓冲区是Linux内核中的一个高性能事件收集机制,它可以收集各种硬件和软件性能事件,并将它们存储在一个高效的环形缓冲区中。
            然后,用户空间的应用程序可以通过读取这个环形缓冲区来获取事件数据,进行进一步的分析和处理。
        另外,kernel-versions.md 里可以查看各BPF类型是哪个内核版本引入的,比如 BPF_PROG_TYPE_KPROBE 就是 4.1 引入
    BCC中的方式如下:
        BPF_PERF_OUTPUT(events)
*/
struct {
    __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
    __uint(key_size, sizeof(__u32));
    __uint(value_size, sizeof(__u32));
} events SEC(".maps");

SEC("tracepoint/sock/inet_sock_set_state")
int inet_sock_set_state(struct trace_event_raw_inet_sock_set_state *args)
{
    __u64 ts, *start, delta_us, rx_b, tx_b;
    struct ident ident = {}, *identp;
    __u16 sport, dport, family;
    struct event event = {};
    struct tcp_sock *tp;
    struct sock *sk;
    bool found;
    __u32 pid;
    int i;

    // 通过上面cat查看`sock:inet_sock_set_state`这个tracepoint的format,就更容易看懂下面的逻辑了(tcp状态枚举值还做了名称的对应)
    // bcc到libbpf的转变:原来的 tsk->parent->pid 方式,替换为 BPF_CORE_READ(tsk, parent, pid) 方式
        // BCC 会默默地重写你的 BPF 代码,并将诸如 tsk->parent->pid 之类的字段访问转换为一系列 bpf_probe_read() 调用
    //  各转变方式可以看这篇译文:https://www.ebpf.top/post/bcc-to-libbpf-guid/
        // BPF_CORE_READ 宏也可在 BCC 模式下工作
    if (BPF_CORE_READ(args, protocol) != IPPROTO_TCP)
        return 0;

    family = BPF_CORE_READ(args, family);
    if (target_family && family != target_family)
        return 0;

    sport = BPF_CORE_READ(args, sport);
    if (filter_sport) {
        found = false;
        for (i = 0; i < MAX_PORTS; i++) {
            if (!target_sports[i])
                return 0;
            if (sport != target_sports[i])
                continue;
            found = true;
            break;
        }
        if (!found)
            return 0;
    }

    dport = BPF_CORE_READ(args, dport);
    if (filter_dport) {
        found = false;
        for (i = 0; i < MAX_PORTS; i++) {
            if (!target_dports[i])
                return 0;
            if (dport != target_dports[i])
                continue;
            found = true;
            break;
        }
        if (!found)
            return 0;
    }

    // format里是一个void*
    sk = (struct sock *)BPF_CORE_READ(args, skaddr);
    if (BPF_CORE_READ(args, newstate) < TCP_FIN_WAIT1) {
        // helper函数,获取系统启动到现在的时间
        // 系统libbpf的include下的bpf.h里可以看到各helper函数功能介绍
        // (linux-5.10.10\include\uapi\linux\bpf.h或者bcc-master\src\cc\libbpf\include\uapi\linux\bpf.h)
        ts = bpf_ktime_get_ns();
        /*
            helper函数,可以bpftool feature |less里面查找各程序类型支持的helper函数,比如下面tracepoint类型:
            eBPF helpers supported for program type tracepoint:
            - bpf_map_lookup_elem
            - bpf_map_update_elem
            - bpf_map_delete_elem
        */
        // 声明和参数说明到bpf.h里看:long bpf_map_update_elem(struct bpf_map *map, const void *key, const void *value, u64 flags)
            // 功能是向 map 里面添加或者更新元素(key,value)
            // map:指向要更新的eBPF映射的指针。这个映射必须已经通过bpf()系统调用或其他方式在**内核中**创建。
            // key:指向要更新的元素的键的指针。键的类型和大小取决于映射的定义。
            // value:指向新值的指针。这个值将替换映射中与给定键关联的旧值。值的类型和大小同样取决于映射的定义。
        // ~~这里是个错误声明示例,通过代码跳转到了:int bpf_map_update_elem(int fd, const void *key, const void *value, __u64 flags),实际应该看bpf.h~~
        // birth是上面定义的BPF map,相当于bcc里面的BPF_HASH(birth, struct sock *, u64); 这个是创建在内核中的
        bpf_map_update_elem(&birth, &sk, &ts, BPF_ANY);
    }

    if (BPF_CORE_READ(args, newstate) == TCP_SYN_SENT || BPF_CORE_READ(args, newstate) == TCP_LAST_ACK) {
        pid = bpf_get_current_pid_tgid() >> 32;
        if (target_pid && pid != target_pid)
            return 0;
        ident.pid = pid;
        // 声明:long bpf_get_current_comm(void *buf, u32 size_of_buf)
        // 拷贝 comm 属性到 buf,具体是啥待定,format前面几个字段?
        bpf_get_current_comm(ident.comm, sizeof(ident.comm));
        // map<struct sock *, struct indent>,这个map也是创建在内核中的
        bpf_map_update_elem(&idents, &sk, &ident, BPF_ANY);
    }

    if (BPF_CORE_READ(args, newstate) != TCP_CLOSE)
        return 0;

    // void *bpf_map_lookup_elem(struct bpf_map *map, const void *key)
    // 根据 key 查找 map,返回是key映射value的地址,这是一个内核态的指针
    start = bpf_map_lookup_elem(&birth, &sk);
    if (!start) {
        bpf_map_delete_elem(&idents, &sk);
        return 0;
    }
    ts = bpf_ktime_get_ns();
    delta_us = (ts - *start) / 1000;

    identp = bpf_map_lookup_elem(&idents, &sk);
    pid = identp ? identp->pid : bpf_get_current_pid_tgid() >> 32;
    if (target_pid && pid != target_pid)
        goto cleanup;

    tp = (struct tcp_sock *)sk;
    rx_b = BPF_CORE_READ(tp, bytes_received);
    tx_b = BPF_CORE_READ(tp, bytes_acked);

    // 从内核态读取出来的内容,组合成一个 struct event (tcplife.h里自己定义了一个struct event)
    event.ts_us = ts / 1000;
    event.span_us = delta_us;
    event.rx_b = rx_b;
    event.tx_b = tx_b;
    event.pid = pid;
    event.sport = sport;
    event.dport = dport;
    event.family = family;
    if (!identp)
        bpf_get_current_comm(event.comm, sizeof(event.comm));
    else
        // long bpf_probe_read_kernel(void *dst, u32 size, const void *unsafe_ptr)
        // 安全地尝试从内核空间地址 unsafe_ptr 读取size大小的数据到dst
        bpf_probe_read_kernel(event.comm, sizeof(event.comm), (void *)identp->comm);
    if (family == AF_INET) {
        bpf_probe_read_kernel(&event.saddr, sizeof(args->saddr), BPF_CORE_READ(args, saddr));
        bpf_probe_read_kernel(&event.daddr, sizeof(args->daddr), BPF_CORE_READ(args, daddr));
    } else {	/*  AF_INET6 */
        bpf_probe_read_kernel(&event.saddr, sizeof(args->saddr_v6), BPF_CORE_READ(args, saddr_v6));
        bpf_probe_read_kernel(&event.daddr, sizeof(args->daddr_v6), BPF_CORE_READ(args, daddr_v6));
    }
    // 声明:long bpf_perf_event_output(void *ctx, struct bpf_map *map, u64 flags, void *data, u64 size)
        // 向性能事件缓冲区发送数据。性能事件缓冲区是内核中的环形缓冲区,可以被各种类型的事件触发,如硬件计数器或软件事件(如函数的进入和退出)
        // 要求把上下文一起传入,此处即args
        // tcplife.h里自己定义了一个struct event,此处是将 &event 发送给 events(前面创建的性能事件环形缓冲区)
            // BPF_F_CURRENT_CPU用来指定数据应该输出到当前 CPU 的本地性能事件缓冲区。在多核系统中,每个 CPU 可能都有自己的性能事件缓冲区
            // 使用 BPF_F_CURRENT_CPU 标志可以确保数据被发送到处理 eBPF 程序的当前 CPU 的缓冲区,这样可以避免跨 CPU 的数据传输,从而提高效率并减少竞争条件。
    bpf_perf_event_output(args, &events, BPF_F_CURRENT_CPU, &event, sizeof(event));

cleanup:
    // 声明:long bpf_map_delete_elem(struct bpf_map *map, const void *key)
    // 从 map 里清理 key对应的元素
    bpf_map_delete_elem(&birth, &sk);
    bpf_map_delete_elem(&idents, &sk);
    /*
        所以整体逻辑是
        1) 在内核态创建自定义全局数据结构:map或array,并创建性能事件ring buffer
        2) 通过ebpf(BPF_CORE_READ或bcc)从上下文中读取内容放在上述内核态结构中。这里可以做一些想要的判断处理逻辑,比如根据(key,value)写入
        3) 从上述内核态自定义结构中,提取信息组合后,投递到性能事件ring buffer里
        4)最后想要的是性能事件数组,可以清理单次的(key,value)
    */
    return 0;
}

char LICENSE[] SEC("license") = "GPL";

4. 小结

分析tcplife.bpf.c程序,并跟tcplife.py中的实现方式对比,熟悉了BCC和libbpf常见几个结构的转换方式

5. 参考

1、tcp-tracepoints

2、BCC 到 libbpf 的转换指南【译】

3、GPT



Comments