BPF 程序具有超越普通程序的能力。
利用BPF大致分为以下几种方式:
利用现有的 BPF 程序,例如,有 sslsniff-bpcc、bpftool 。
编写少量代码的程序并使用 bpftrace 运行它们。
使用 libbpf 和其他框架编写更复杂的程序。通过 clang 等工具编译成BPF字节码。
直接使用BPF字节码进行开发。
本文主要介绍已有的BPF相关的工具和框架的使用方式。
1.BPFTool
BPFTool是一个用于检查BPF程序和映射的内核工具。
1.1 安装
安装需要下载内核源码
git clone https://github.com/torvalds/linuxgit checkout v4.19cd tools/bpf/bpftoolmake && sudo make install
1.2 特征查看
bpftool featur查看jit是否开启
echo 1 > /proc/sys/net/core/bpf_jit_enable1.3 检查BPF程序
bpftool可以提供内核与BPF程序相关的信息。通过bpftool,我们可以查看系统中已经运行的BPF程序信息。
查看系统中已经加载了一些BPF程序。
bpftool prog show例如,目前系统已加载了某些kprobes。
如下命令可以格式化输出BPF程序相关信息。
bpftool prog show --json id 16 | jq当知道程序id后,还可以使用BPFTool获取整个程序的数据。可以反编译为BPF字节码。
bpftool prog dump xlated id 16可以加上visual关键字,产生特定格式输出。
bpftool prog dump xlated id 16 visual1.4 检查BPF映射
显示映射信息
bpftool map showmap帮助信息,可以对映射做一系列操作,比如更新删除添加等等。
bpftool map help1.5 查看附加到特定接口的程序
BPF可以加载运行在cgroup、Pref事件和网络数据包程序上,bpftool可以查看跟踪在这些接口的附加程序。
bpftool perf showbpftool net showbpftool cgroup tree
1.6 批量加载命令
可以将命令写入文件,批量处理,比如写入文件test.txt。
map showperf show
运行命令,执行批处理。
bpftool batch file ./test.txt1.7 显示BTF信息
显示任何给定的二进制对象的BPF类型格式(BTF)信息。
bpftool btf dump id 35BPFTrace
BPFTrace 是一个BPF开发的前端工具,可用来创建自定义的 BPF 程序,是一种用于 eBPF 的高级跟踪语言,使用 LLVM 作为后端,将脚本编译为 BPF 字节码,语言的灵感来自于awk 和 C,以及诸如 DTrace 和 SystemTap 这样的跟踪器。
2.1 安装
yum install bpftrace2.2 语法
BPFTrace程序语法简洁。程序分为3个部分:
头部(header)
操作块(action block)
尾部(footer)
头部是在加载程序时BPFTrace执行的特殊块。它通常用来打印在输出顶部的一些信息。
尾部是在程序终止前BPFTrace执行的特殊块。
头部和尾部都是BPFTrace程序可选部分。
一个BPFTrace程序必须至少有一个操作块。操作块是指定我们要跟踪的探针的位置,以及基于探针内核触发事件执行的操作。
BEGIN{printf("starting BPFTrace program\n")}kprobe:do_sys_open{printf("opening file descriptor: %s\n", str(arg1))}END{printf("exiting BPFTrace program\n")}
头部用关键字BEGIN标记,尾部调用用关键字END标记。这些关键字是BPFTrace保留关键字。操作块标识符定义要附加的BPF操作的探针。上面示例中,每次内核打开一个文件,都会打印一条日志。
bpftrace test.bt也可以直接写一行:
bpftrace -e "kprobe:do_sys_open {@opens[str(arg1)] = count()}"2.3 过滤
过滤器封装在操作头部后面的两个斜杠内:
BEGIN{printf("starting BPFTrace program\n")}kprobe:do_sys_open /str(arg1) == "~/test.bt"/{printf("opening file descriptor: %s\n", str(arg1))}END{printf("exiting BPFTrace program\n")}
2.4 动态映射
所有映射关联都以字符@开头,后面跟着要创建的映射名。可以通过指定元素的值来更新映射中的元素。
将open系统调用的次数,保存在映射中。
kprobe:do_sys_open{@opens[str(arg1)] = count()}
当程序停止执行时,BPFTrace将打印映射的内容。它聚合计算了内核系统中打开文件的频率。默认情况下,当BPFTrace终止时,总是会打印创建的每个映射的内容。如果需要清除,可以通过使用内置函数clear来清除END块中的映射。
2.5 BPFTrace 单行程序实例
1. CPU跟踪新进程,包括进程参数:bpftrace -e 'tracepoint:syscalls:sys_enter_execve { join(args->argv); }'按进程统计系统调用的数量:bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[pid, comm] = count(); }'以99Hz的频率采样正在运行的进程名:bpftrace -e 'profile:hz:99 { @[comm] = count(); }'以49Hz的频率采样,进程ID为1134的用户态调用栈的信息:bpftrace -e 'profile:hz:49 /pid == 1134 / { @[ustack] = count(); }'跟踪通过pthread_create()创建的新线程:bpftrace -e 'u:/lib/x86_64-linux-gnu/libpthread-2.27.so:pthread_create { printf("%s by %s (%d)\n", probe, comm, pid); }'2. 内存根据用户态调用栈信息统计进程堆内存扩展(brk()):bpftrace -e 'tracepoint:syscalls:sys_enter_brk { @[ustack, comm] = count(); }'按进程统计缺页错误:bpftrace -e 'software:page-fault:1 { @[comm, pid] = count(); }'根据用户态调用栈信息统计缺页错误:bpftrace -e 'tracepoint:exceptions:page_fault_user { @[comm, pid] = count(); }'通过跟踪点来统计vmscan操作:bpftrace -e 'tracepoint:vmscan:* { @[probe]++; }'3. 安全为PID为1234的进程统计安全审计事件数:bpftrace -e 'k:security_* /pid == 1234/ { @[func] = count(); }'跟踪可插入身份验证模块(PAM)会话的开始:bpftrace -e 'u:/lib/x86_64-linux-gnu/libppam.so.0:pam_start { printf("%s: %s\n", str(arg0), str(arg1)); }'跟踪内核模块加载:bpftrace -e 't:module:module_load { printf("load: %s\n", str(args->name)); }'4. 内核按系统调用函数对系统调用进行计数:bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[ksym(*(kaddr("sys_call_table") + args->id * 8))] = count(); }'对以 attach 开头的内核函数调用进行计数:bpftrace -e 'kprobe:attach* { @[probe] = count(); }'为内核函数vfs_read()计时并画直方图:bpftrace -e 'k:vfs_read { @ts[tid] = nsecs; } kr:vfs_read /@ts[tid]/ { @ = hist(nsecs - @ts[tid]); delete(@ts[tid]); }'对内核函数 func1 的第一个参数出现的频率进行计数:bpftrace -e 'kprobe:func1 { @[arg0] = count(); }'对内核函数 func1 的返回值出现的频率进行计数:bpftrace -e 'kprobe:func1 { @[retval] = count(); }'5. 文件系统按进程名统计通过open()打开的文件:bpftrace -e 't:syscalls:sys_enter_open { printf("%s %s\n", comm, str(args->filename)); }'显示read()系统调用的请求大小分布:bpftrace -e 'tracepoint:syscalls:sys_enter_read { @ = hist(args->count); }'显示read()系统调用的实际读取字节数(以及错误):bpftrace -e 'tracepoint:syscalls:sys_exit_read { @ = hist(args->ret); }'统计VFS调用:bpftrace -e 'kprobe:vfs_* { @[probe] = count(); }'统计ext4跟踪点:bpftrace -e 'tracepoint:ext4_* { @[probe] = count(); }'6. 网络按PID和进程名统计套接字accept调用:bpftrace -e 't:syscalls:sys_enter_accept* { @[pid, comm] = count(); }'按PID和进程名统计套接字connect调用:bpftrace -e 't:syscalls:sys_enter_connect { @[pid, comm] = count(); }'按在CPU上运行的PID和进程名统计套接字发送和接受的字节数:bpftrace -e 'kr:sock_sendmsg,kr:sock_recvmsg /retval >0/{ @[pid, comm, retval] = sum(retval); }'统计TCP的发送和接收次数:bpftrace -e 'k:tcp_sendmsg,k:tcp*recvmsg { @[func] = count(); }'以直方图形式统计TCP发送的字节数:bpftrace -e 'k:tcp_sendmsg { @send_bytes = hist(arg2); }'以直方图形式统计TCP接收的字节数:bpftrace -e 'k:tcp*recvmsg /retval >= 0/ { @recv_bytes = hist(retval); }'按类型与远程主机统计TCP重传:bpftrace -e 'k:tcp_retransmit_* { @[probe, ntop(2, args->addr)] = count(); }'统计发送数据包时的内核态调用栈:bpftrace -e 't:net:net_dev_xmit { @[kstack] = count(); }'7. 应用程序按用户态调用栈计算malloc()请求的字节总数(高开销):bpftrace -e 'u:/lib/libc-2.27so:malloc { @[ustack(5)] = sum(arg0); }'跟踪kill()信号,显示发送进程名称、目标PID和信号:bpftrace -e 't:syscalls:sys_enter_kill { printf("%s -> PID %d SIG %d\n", comm, args->pid, args->sig); }'
reference
《Linux内核观测技术BPF》
《BPF之巅洞悉Linux系统和应用性能》