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程序开发有固定的步骤:

  1. 加载BPF程序,这个程序可以用C来写(能不能用python或者golang来写呢?)
  2. 定义perfevent的回调函数
  3. 循环访问定义的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;
}
  1. <linux/bpf.h>包含一些基本的BPF相关类型和使用内核端BPF API所必需的常量(例如,BPF helper函数标志),<bpf/bpf_helpers.h>需要该头文件
  2. <bpf/bpf_helpers.h>由libbpf提供,包含最常用的宏、常量和BPF helper定义,它们几乎被每个现有的BPF应用程序使用。上面的bpf_get_current_pid_tgid()就是这样一个BPF Helper的例子
  3. LICENSE变量定义BPF代码的许可证。指定许可证是必须的,并且由内核强制执行。一些BPF功能对于非GPL兼容的代码是不可用的
  4. 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
  5. 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代码中问题的最快和最方便的方法。
posted @   JaneySJ  阅读(956)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源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
点击右上角即可分享
微信分享提示