https://zhuanlan.zhihu.com/p/488498453
之前的文章介绍了使用cilium工具开发BPF程序的例子。对于较新的系统内核来说,用这样较新的工具很不错,但是对于稍微旧一点的系统,如果不想直接写原生BPF程序的话,我们貌似只有一个选择,使用bcc。
一些常见的发行版的源中已经包含了bcc工具,注意我们在使用旧版本的bcc时,需要也要安装当前系统的内核源码头文件,新版本的bcc好像采用了libbpf的CO-RE特性。
apt install bpfcc-tools linux-headers-`uname -r`
yum install bcc-tools kernel-devel-`uname -r`
安装的内核头文件放置在了lib/modules
目录下,其中包含了一个指向/usr/src的软链接文件。如果我们想要使用容器运行bcc程序对系统进行调试,注意要把系统上的这两个路径挂到容器上去。
安装好bcc后,可以查看bcc自带的一些工具的实现,例如RHEL中这些工具被放置在了/usr/share/bcc/tools/目录下,里面有很多python文件,每个脚本的内容都不是很多,仿照起来写也比较容易。
和其他的BPF开发程序一样,bcc框架也分成了两部分,一部分是BPF程序,加载到内核中使用的,bcc为了方便我们编写这部分的程序,提供了对应的库。另一部分是用户空间处理的部分,接收来自内核空间生成的数据进行处理,还可以方便的加载BPF程序到内核,旧版本的bcc一般使用python来实现这个外围的部分,听说新版本打算要弃用python了。
开发的时候我们需要考虑几个内容:
1. 对于我们想实现的功能,我们应该采用那些位置加载BPF程序。有四个比较常提到的是tracepoint,kprobe,USDT,uprobe。这四个分别是内核中的静态hook点,内核中的动态hook点,用户空间的静态hook点,用户空间的动态hook点。所谓的静态和动态,区别在于在编译程序时,是否一块编译进了一些标记。对于静态点来说,运行到了这块标记就会执行在这个点上加载的BPF程序。而动态点则无需通过标记来判断,而是编译时将函数信息加入到了可执行文件的头部信息中,当运行到这些函数时,就会自动触发对应的加载到这个位置的BPF程序。这也是为什么大家会说,有tracepoint就优先用tracepoint,没有才用kprobe,就是因为tracepoint少,并且一旦加入到内核源码中基本不会被去掉,而内核中的函数可没有这个保证。而一些针对于网络的SOCKET FILTER等还有一些其他的点则不在这四个分类中,这里面有些对内核版本的要求较高,是内核一点一点支持的。
2. 其实看看bcc提供的工具,我们会发现大多数都是采用kprobe的。那么假如我们知道想做什么,大概知道在内核的哪个功能上加载,我们怎么知道加载到kprobe的哪个确切的点呢。有一个方法是,查看/proc/kallsyms
和/sys/kernel/debug/kropbes/blacklist
两个文件,位于第一个文件中,同时又不在第二个文件中的项目,理论上说应该是可以用的。但是我测试的时候发现有些不行,没有定位到原因出在哪里。顺带一提,系统支持的tracepoint位于sys/kernel/debug/tracing/events
下。
3. 然后就是看内核源码了,在源码中找到这个函数传了哪些参数,这样在编写BPF程序的时候,这些参数都可以被我们的BPF程序应用,例如tcp_v4_connect函数接收struct sock *sk参数,那么我们在定义对应的BPF程序时,可以定义
int kprobe__tcp_v4_connect(struct pt_regs *ctx, struct sock *sk){};
如果直接定义成kprobe__
加函数名的方式,后面在加载时不用再指定一次,直接这样即可(这里bpf_text即用字符串保存的c的源码)
b = BPF(text=bpf_text)
如果使用别的名字,就需要显式的指定一下加载的位置
b = BPF(text=bpf_text)
b.attach_kprobe(event="tcp_v4_connect", fn_name="trace_connect_v4_entry")
kprobe支持在内核函数执行返回后触发BPF程序,即kretprobe:
int kretprobe__tcp_v4_connect(struct pt_regs *ctx, struct sock *sk){};
b = BPF(text=bpf_text)
b.attach_kretprobe(event="tcp_v4_connect", fn_name="trace_connect_v4_return")
4. 下一个问题是,内核中采集到的数据,怎么发送到用户空间处理,比如上个函数中的sock包含的某个变量,我们想在python程序中输出出来。可以采用以下的方式
/sys/kernel/debug/tracing/trace_pipe
看到,bcc也提供了函数可以在程序中获取这个文件中的内容,这样直接调用b.trace_print即可以打印出来,也可以通过 b.trace_fields()之前打印的内容读取到变量中
// c程序中
BPF_PERF_OUTPUT(ipv4_events)
...
ipv4_events.perf_submit(ctx, &data4, sizeof(data4));
# 外层的python程序
def print_ipv4_event(cpu, data, size):
...
b["ipv4_events"].open_perf_buffer(print_ipv4_event, page_cnt=64)
// c
BPF_HASH(ipv4_send_bytes, struct ipv4_key_t);
...
ipv4_send_bytes.increment(ipv4_key, size);
# python
ipv4_send_bytes = b["ipv4_send_bytes"]