BPF 程序具有超越普通程序的能力。
利用BPF大致分为以下几种方式:
利用现有的 BPF 程序,例如,有 sslsniff-bpcc、bpftool 。
编写少量代码的程序并使用 bpftrace 运行它们。
使用 libbpf 和其他框架编写更复杂的程序。通过 clang 等工具编译成BPF字节码。
直接使用BPF字节码进行开发。
本文主要介绍已有的BPF相关的工具和框架的使用方式。
1.BPFTool
BPFTool是一个用于检查BPF程序和映射的内核工具。
1.1 安装
安装需要下载内核源码
git clone https://github.com/torvalds/linux
git checkout v4.19
cd tools/bpf/bpftool
make && sudo make install
1.2 特征查看
bpftool featur
查看jit是否开启
echo 1 > /proc/sys/net/core/bpf_jit_enable
1.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 visual
1.4 检查BPF映射
显示映射信息
bpftool map show
map帮助信息,可以对映射做一系列操作,比如更新删除添加等等。
bpftool map help
1.5 查看附加到特定接口的程序
BPF可以加载运行在cgroup、Pref事件和网络数据包程序上,bpftool可以查看跟踪在这些接口的附加程序。
bpftool perf show
bpftool net show
bpftool cgroup tree
1.6 批量加载命令
可以将命令写入文件,批量处理,比如写入文件test.txt。
map show
perf show
运行命令,执行批处理。
bpftool batch file ./test.txt
1.7 显示BTF信息
显示任何给定的二进制对象的BPF类型格式(BTF)信息。
bpftool btf dump id 35
BPFTrace
BPFTrace 是一个BPF开发的前端工具,可用来创建自定义的 BPF 程序,是一种用于 eBPF 的高级跟踪语言,使用 LLVM 作为后端,将脚本编译为 BPF 字节码,语言的灵感来自于awk 和 C,以及诸如 DTrace 和 SystemTap 这样的跟踪器。
2.1 安装
yum install bpftrace
2.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系统和应用性能》