eBPF学习笔记(二)开发一个程序
eBPF程序开发过程
首先你要了解Linux的基本知识,知道Linux有哪些系统调用,这些调用时干什么用的,以及你着重关注哪些系统操作,在Linux系统中可以到/proc/kallsyms中查询所有的内核系统调用函数,可以参考这个网页 http://blog.chinaunix.net/uid-20485483-id-82923.html. 然后就是开发bpf程序了,这里以文件打开操作open来示例。可以通过命令行 sudo bpftrace -l '*open*'
来查询所有bpf支持的关于open的操作,假设我们关注的函数是do_sys_openat2,而do_sys_openat2有两中挂载方式:
kprobe:do_sys_openat2
kfunc:do_sys_openat2
我们选择kprobe动态内核跟踪方式,另外一个kfunc.kprobe用于在内核函数的入口点或出口点插入自定义eBPF代码,而kfunc用于在eBPF程序内部调用内核中的函数。下面就是写ebpf函数了,众所周知ebpf函数开发方式有bpftrace、bcc和libbpf三种方式,并且这三种方式是从易到难递进的,但是完善性也是从弱到强的。首先,来看bpftrace方式。
说明一下,这里不说明bpftrace、bcc和libbpf的开发环境的安装,因为网上很多。
bpftrace程序
sudo bpftrace -lv kfunc:do_sys_openat2
可以查看到函数的入参,但是 sudo bpftrace -lv kprobe:do_sys_openat2
却没有输出。但我猜测两个函数的入参应该都是一样的。
~# sudo bpftrace -lv kfunc:do_sys_openat2
BTF: using data from /sys/kernel/btf/vmlinux
kfunc:do_sys_openat2
int dfd;
const char * filename;
struct open_how * how;
long int retval;
因为内置的参数只能用在tracepoint/kfunc probes中,这里用kfunc挂载。其中pid、comm是内置函数,分别是PID和进程名称。其他函数的入参都在args中了。
#!/usr/bin/env bpftrace
kfunc:do_sys_openat2
{
$processid = pid;
if ( $processid == 3922 || $processid == 3923 ) {
printf("%-6d %s dfd %d filename: %s\n", pid, comm, args->dfd, str(args->filename));
}
}
如果再添加上标题、时间就是下面这样:
#!/usr/bin/env bpftrace
BEGIN
{
printf("%-9s %-6s %-6s %-8s %-16s\n", "TIME", "PID", "COMM", "DFD", "FILENAME")
}
kfunc:do_sys_openat2
{
time("%H:%M:%S ");
printf("%-6d %-6s %-8d %-16s\n", pid, comm, args->dfd, str(args->filename));
}
END
{
printf("========END=======\n")
}
上面的代码写好后保存文件名为sys-openat2.bt, 运行该文件./sys-openat2.bt
,执行程序后看到dfd一直是一个相同的负数,不知道是怎么回事,看名称字面意思猜测是fd的index. 另外,bpftrace是最简便的ebpf观测工具,可以用一句话直接敲到命令行里,例如上面的功能,可以用下面的一句来简单替代:
# bpftrace -e 'kfunc:do_sys_openat2 { printf("PID %d %s %s\n", pid, comm, str(args->filename)); }'
Attaching 1 probe...
PID 726 irqbalance /proc/interrupts
PID 726 irqbalance /proc/stat
PID 693 vmtoolsd /proc/meminfo
PID 693 vmtoolsd /proc/vmstat
PID 693 vmtoolsd /proc/stat
# bpftrace -e 'kprobe:do_sys_openat2 { @[comm] = count(); }'
Attaching 1 probe...
^C
@[bash]: 1
@[irqbalance]: 4
@[vmtoolsd]: 6
@[ls]: 9
@[smbd]: 52
上面示例中的第二个命令当bpftrace命令结束后才统计结果,计算了不同进程名称的调用次数。更多bpftrace的示例可以到https://github.com/iovisor/bpftrace/tree/master/tools查看,里面有一个文件opensoop.bt跟我这个例子类似,但人家写的更好。
bcc程序
bcc程序开发有固定的步骤:
- 加载BPF程序,这个程序可以用C来写(能不能用python或者golang来写呢?)
- 定义perfevent的回调函数
- 循环访问定义的2中所述的回调函数,意思是当系统调用do_sys_openat2时,就执行hello_world.c文件内容,并按照bcc代码规定的方式显示出来。
还是以do_sys_openat2来举例,bcc函数源码hello_word程序如下所示:
// 包含头文件
#include <uapi/linux/openat2.h>
#include <linux/sched.h>
// 定义数据结构
struct data_t {
u32 pid;
u64 ts;
char comm[TASK_COMM_LEN];
char fname[NAME_MAX];
};
// 定义性能事件映射
BPF_PERF_OUTPUT(events);
// 定义kprobe处理函数
int hello_world(struct pt_regs *ctx, int dfd, const char __user * filename, struct open_how *how)
{
struct data_t data = { };
// 获取PID和时间
data.pid = bpf_get_current_pid_tgid();
data.ts = bpf_ktime_get_ns();
// 获取进程名
if (bpf_get_current_comm(&data.comm, sizeof(data.comm)) == 0)
{
bpf_probe_read(&data.fname, sizeof(data.fname), (void *)filename);
}
// 提交性能事件
events.perf_submit(ctx, &data, sizeof(data));
return 0;
}
这里有两个点需要思考,bfp_打头的这些函数都有哪些?可以用命令bpftool feature probe查看,那具体某一个例如bpf_probe_read函数的用法可以通过man bpf-helpers查询该函数来获得答案。另外一个问题,hello_world的入参是怎么确定的呢? 如果说dfd等后面的参数可以通过上面的sudo bpftrace -lv kfunc:do_sys_openat2
获取的话,那么第一个参数ctx的参数类型是怎么确定的呢?我试过换成void *类型程序是跑不起来的。这个问题,我目前还不知道,知道的大神可否留个言!
另外还需要加载以上BPF源码的函数,如下所示,文件名是hello_world.py
from bcc import BPF
# 1) load BPF program
b = BPF(src_file="trace-open.c")
b.attach_kprobe(event="do_sys_openat2", fn_name="hello_world")
# 2) print header
print("%-18s %-16s %-6s %-16s" % ("TIME(s)", "COMM", "PID", "FILE"))
# 3) define the callback for perf event
start = 0
def print_event(cpu, data, size):
global start
event = b["events"].event(data)
if start == 0:
start = event.ts
time_s = (float(event.ts - start)) / 1000000000
print("%-18.9f %-16s %-6d %-16s" % (time_s, event.comm, event.pid, event.fname))
# 4) loop with callback to print_event
b["events"].open_perf_buffer(print_event)
while 1:
try:
b.perf_buffer_poll()
except KeyboardInterrupt:
exit()
写好后,运行方法是python3 hello_world.py
,由hell_world.py文件去加载eBPF代码并完成事件显示。
另外还有一种方法,就是下面的宏定义方法,从args中取到参数,这里是一个tracepoint的示例,kfunc的还不会写。
/* Tracing execve system call. */
#include <uapi/linux/ptrace.h>
#include <linux/sched.h>
#include <linux/fs.h>
// consts for arguments (ensure below stack size limit 512)
#define ARGSIZE 64
#define TOTAL_MAX_ARGS 5
#define FULL_MAX_ARGS_ARR (TOTAL_MAX_ARGS * ARGSIZE)
#define LAST_ARG (FULL_MAX_ARGS_ARR - ARGSIZE)
// perf event map (sharing data to userspace) and hash map (sharing data between tracepoints)
struct data_t {
u32 pid;
char comm[TASK_COMM_LEN];
int retval;
unsigned int fd;
unsigned int args_size;
char argv[FULL_MAX_ARGS_ARR];
};
BPF_PERF_OUTPUT(events);
BPF_HASH(tasks, u32, struct data_t);
// helper function to read string from userspace.
static int __bpf_read_arg_str(struct data_t *data, const char *ptr)
{
if (data->args_size > LAST_ARG) {
return -1;
}
int ret = bpf_probe_read_user_str(&data->argv[data->args_size], ARGSIZE,
(void *)ptr);
if (ret > ARGSIZE || ret < 0) {
return -1;
}
// increase the args size. the first tailing '\0' is not counted and hence it
// would be overwritten by the next call.
data->args_size += (ret - 1);
return 0;
}
// sys_enter_write tracepoint.
TRACEPOINT_PROBE(syscalls, sys_enter_write)
{
// variables definitions
unsigned int ret = 0;
const char **argv = (const char **)(args->buf);
// get the pid and comm
struct data_t data = { };
u32 pid = bpf_get_current_pid_tgid();
data.pid = pid;
data.fd = args->fd;
bpf_get_current_comm(&data.comm, sizeof(data.comm));
// get the binary name (first argment)
if (__bpf_read_arg_str(&data, (const char *)argv[0]) < 0) {
goto out;
}
// get other arguments (skip first arg because it has already been read)
#pragma unroll
for (int i = 1; i < TOTAL_MAX_ARGS; i++) {
if (__bpf_read_arg_str(&data, (const char *)argv[i]) < 0) {
goto out;
}
}
out:
// store the data in hash map
tasks.update(&pid, &data);
return 0;
}
// sys_exit_write tracepoint
TRACEPOINT_PROBE(syscalls, sys_exit_write)
{
// query the data from hash map
u32 pid = bpf_get_current_pid_tgid();
struct data_t *data = tasks.lookup(&pid);
// submit perf events after getting the retval
if (data != NULL) {
data->retval = args->ret;
events.perf_submit(args, data, sizeof(struct data_t));
// clean up the hash map
tasks.delete(&pid);
}
return 0;
}
要调用上面这个这个writesnoop.c文件,还需要一个writesnoop.py文件,内容如下:
# 引入库函数
from bcc import BPF
from bcc.utils import printb
# 1) 加载eBPF代码
b = BPF(src_file="writesnoop.c")
# 2) 输出头
print("%-6s %-16s %-3s %s %s" % ("PID", "COMM", "RET", "ARGS", "FD"))
# 3) 定义性能事件打印函数
def print_event(cpu, data, size):
# BCC自动根据"struct data_t"生成数据结构
event = b["events"].event(data)
if event.pid == 3922 or event.pid == 3923 :
printb(b"%-6d %-16s %-3d %-16s %d" % (event.pid, event.comm, event.retval, event.argv, event.fd))
# 4) 绑定性能事件映射和输出函数,并从映射中循环读取数据
b["events"].open_perf_buffer(print_event)
while 1:
try:
b.perf_buffer_poll()
except KeyboardInterrupt:
exit()
写好后,程序运行方法是python3 writesnoop.py
.
libbpf程序
libbpf 参考 https://github.com/libbpf/libbpf, 该官方网站说明了例子可以参考 https://github.com/libbpf/libbpf-bootstrap, 注意这个需要下载工程仓库libbpf和bpftool的源码放到libbpf-bootstrap目录下。首先来看第一个例子minimal.
例子minimal包含两个文件:minimal.bpf.c和minimal.c:
- minimal.bpf.c是内核态执行的程序;
- minimal.c是用户态程序,包括 eBPF 程序加载、挂载到内核函数和跟踪点,以及通过 BPF 映射获取和打印执行结果等。
先来看minimal.bpf.c:
点击查看代码
// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
/* Copyright (c) 2020 Facebook */
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
char LICENSE[] SEC("license") = "Dual BSD/GPL";
int my_pid = 0;
SEC("tp/syscalls/sys_enter_write")
int handle_tp(void *ctx)
{
int pid = bpf_get_current_pid_tgid() >> 32;
if (pid != my_pid)
return 0;
bpf_printk("BPF triggered from PID %d.\n", pid);
return 0;
}
- <linux/bpf.h>包含一些基本的BPF相关类型和使用内核端BPF API所必需的常量(例如,BPF helper函数标志),<bpf/bpf_helpers.h>需要该头文件
- <bpf/bpf_helpers.h>由libbpf提供,包含最常用的宏、常量和BPF helper定义,它们几乎被每个现有的BPF应用程序使用。上面的bpf_get_current_pid_tgid()就是这样一个BPF Helper的例子
- LICENSE变量定义BPF代码的许可证。指定许可证是必须的,并且由内核强制执行。一些BPF功能对于非GPL兼容的代码是不可用的
- SEC("tp/syscalls/sys_enter_write") int handle_tp(void *ctx) { ... }定义一个tracepoint BPF程序, which will be called each time a write() syscall is invoked from any user-space application
- handle_tp函数的作用时检查触发write()系统调用的进程是否是我们的minimal进程。这在繁忙的系统中非常重要,因为很可能会有许多不相关的进程发出write()调用。bpf_printk等价于BPF的printf函数,它将格式化的字符串发送 到/sys/kernel/debug/tracing/trace_pipe这个特殊文件中,您可以从控制台中通过命令
sudo cat /sys/kernel/debug/tracing/trace_pipe
查看该文件内容。bpf_printk() Helper和trace_pipe文件不应该在生产中使用,但它对于调试BPF代码和深入了解BPF程序正在做什么是必不可少的。在还没有BPF调试器时,bpf_printk()通常是调试BPF代码中问题的最快和最方便的方法。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
2020-05-22 service与kube-proxy